1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.sanoid;
7
8 datasetSettingsType = with types;
9 (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
10 description = "dataset/template options";
11 };
12
13 commonOptions = {
14 hourly = mkOption {
15 description = "Number of hourly snapshots.";
16 type = with types; nullOr ints.unsigned;
17 default = null;
18 };
19
20 daily = mkOption {
21 description = "Number of daily snapshots.";
22 type = with types; nullOr ints.unsigned;
23 default = null;
24 };
25
26 monthly = mkOption {
27 description = "Number of monthly snapshots.";
28 type = with types; nullOr ints.unsigned;
29 default = null;
30 };
31
32 yearly = mkOption {
33 description = "Number of yearly snapshots.";
34 type = with types; nullOr ints.unsigned;
35 default = null;
36 };
37
38 autoprune = mkOption {
39 description = "Whether to automatically prune old snapshots.";
40 type = with types; nullOr bool;
41 default = null;
42 };
43
44 autosnap = mkOption {
45 description = "Whether to automatically take snapshots.";
46 type = with types; nullOr bool;
47 default = null;
48 };
49 };
50
51 datasetOptions = rec {
52 use_template = mkOption {
53 description = "Names of the templates to use for this dataset.";
54 type = types.listOf (types.enum (attrNames cfg.templates));
55 default = [ ];
56 };
57 useTemplate = use_template;
58
59 recursive = mkOption {
60 description = ''
61 Whether to recursively snapshot dataset children.
62 You can also set this to <literal>"zfs"</literal> to handle datasets
63 recursively in an atomic way without the possibility to
64 override settings for child datasets.
65 '';
66 type = with types; oneOf [ bool (enum [ "zfs" ]) ];
67 default = false;
68 };
69
70 process_children_only = mkOption {
71 description = "Whether to only snapshot child datasets if recursing.";
72 type = types.bool;
73 default = false;
74 };
75 processChildrenOnly = process_children_only;
76 };
77
78 # Extract unique dataset names
79 datasets = unique (attrNames cfg.datasets);
80
81 # Function to build "zfs allow" and "zfs unallow" commands for the
82 # filesystems we've delegated permissions to.
83 buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
84 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
85 "-+/run/booted-system/sw/bin/zfs"
86 zfsAction
87 "sanoid"
88 (concatStringsSep "," permissions)
89 dataset
90 ];
91
92 configFile =
93 let
94 mkValueString = v:
95 if builtins.isList v then concatStringsSep "," v
96 else generators.mkValueStringDefault { } v;
97
98 mkKeyValue = k: v:
99 if v == null then ""
100 else if k == "processChildrenOnly" then ""
101 else if k == "useTemplate" then ""
102 else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
103 in
104 generators.toINI { inherit mkKeyValue; } cfg.settings;
105
106in
107{
108
109 # Interface
110
111 options.services.sanoid = {
112 enable = mkEnableOption "Sanoid ZFS snapshotting service";
113
114 interval = mkOption {
115 type = types.str;
116 default = "hourly";
117 example = "daily";
118 description = ''
119 Run sanoid at this interval. The default is to run hourly.
120
121 The format is described in
122 <citerefentry><refentrytitle>systemd.time</refentrytitle>
123 <manvolnum>7</manvolnum></citerefentry>.
124 '';
125 };
126
127 datasets = mkOption {
128 type = types.attrsOf (types.submodule ({ config, options, ... }: {
129 freeformType = datasetSettingsType;
130 options = commonOptions // datasetOptions;
131 config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
132 config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
133 }));
134 default = { };
135 description = "Datasets to snapshot.";
136 };
137
138 templates = mkOption {
139 type = types.attrsOf (types.submodule {
140 freeformType = datasetSettingsType;
141 options = commonOptions;
142 });
143 default = { };
144 description = "Templates for datasets.";
145 };
146
147 settings = mkOption {
148 type = types.attrsOf datasetSettingsType;
149 description = ''
150 Free-form settings written directly to the config file. See
151 <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
152 for allowed values.
153 '';
154 };
155
156 extraArgs = mkOption {
157 type = types.listOf types.str;
158 default = [ ];
159 example = [ "--verbose" "--readonly" "--debug" ];
160 description = ''
161 Extra arguments to pass to sanoid. See
162 <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
163 for allowed options.
164 '';
165 };
166 };
167
168 # Implementation
169
170 config = mkIf cfg.enable {
171 services.sanoid.settings = mkMerge [
172 (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
173 (mapAttrs (d: v: v) cfg.datasets)
174 ];
175
176 systemd.services.sanoid = {
177 description = "Sanoid snapshot service";
178 serviceConfig = {
179 ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
180 ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
181 ExecStart = lib.escapeShellArgs ([
182 "${pkgs.sanoid}/bin/sanoid"
183 "--cron"
184 "--configdir"
185 (pkgs.writeTextDir "sanoid.conf" configFile)
186 ] ++ cfg.extraArgs);
187 User = "sanoid";
188 Group = "sanoid";
189 DynamicUser = true;
190 RuntimeDirectory = "sanoid";
191 CacheDirectory = "sanoid";
192 };
193 # Prevents missing snapshots during DST changes
194 environment.TZ = "UTC";
195 after = [ "zfs.target" ];
196 startAt = cfg.interval;
197 };
198 };
199
200 meta.maintainers = with maintainers; [ lopsided98 ];
201}