]> Git — Sourcephile - julm/julm-nix.git/blob - nixos/modules/services/backup/sanoid.nix
syncoid: import module isntead of patching nixpkgs
[julm/julm-nix.git] / nixos / modules / services / backup / sanoid.nix
1 {
2 config,
3 lib,
4 pkgs,
5 ...
6 }:
7 let
8 cfg = config.services.sanoid;
9
10 datasetSettingsType =
11 with lib.types;
12 (attrsOf (
13 nullOr (oneOf [
14 str
15 int
16 bool
17 (listOf str)
18 ])
19 ))
20 // {
21 description = "dataset/template options";
22 };
23
24 commonOptions = {
25 hourly = lib.mkOption {
26 description = "Number of hourly snapshots.";
27 type = with lib.types; nullOr ints.unsigned;
28 default = null;
29 };
30
31 daily = lib.mkOption {
32 description = "Number of daily snapshots.";
33 type = with lib.types; nullOr ints.unsigned;
34 default = null;
35 };
36
37 monthly = lib.mkOption {
38 description = "Number of monthly snapshots.";
39 type = with lib.types; nullOr ints.unsigned;
40 default = null;
41 };
42
43 yearly = lib.mkOption {
44 description = "Number of yearly snapshots.";
45 type = with lib.types; nullOr ints.unsigned;
46 default = null;
47 };
48
49 autoprune = lib.mkOption {
50 description = "Whether to automatically prune old snapshots.";
51 type = with lib.types; nullOr bool;
52 default = null;
53 };
54
55 autosnap = lib.mkOption {
56 description = "Whether to automatically take snapshots.";
57 type = with lib.types; nullOr bool;
58 default = null;
59 };
60
61 pre_snapshot_script = lib.mkOption {
62 description = "Script to run before taking snapshot.";
63 type = with lib.types; nullOr str;
64 default = null;
65 };
66
67 post_snapshot_script = lib.mkOption {
68 description = "Script to run after taking snapshot.";
69 type = with lib.types; nullOr str;
70 default = null;
71 };
72
73 pruning_script = lib.mkOption {
74 description = "Script to run after pruning snapshot.";
75 type = with lib.types; nullOr str;
76 default = null;
77 };
78
79 no_inconsistent_snapshot = lib.mkOption {
80 description = "Whether to take a snapshot if the pre script fails";
81 type = with lib.types; nullOr bool;
82 default = null;
83 };
84
85 force_post_snapshot_script = lib.mkOption {
86 description = "Whether to run the post script if the pre script fails";
87 type = with lib.types; nullOr bool;
88 default = null;
89 };
90
91 script_timeout = lib.mkOption {
92 description = "Time limit for pre/post/pruning script execution time (<=0 for infinite).";
93 type = with lib.types; nullOr int;
94 default = null;
95 };
96 };
97
98 datasetOptions = rec {
99 use_template = lib.mkOption {
100 description = "Names of the templates to use for this dataset.";
101 type = lib.types.listOf (
102 lib.types.str
103 // {
104 check = (lib.types.enum (lib.attrNames cfg.templates)).check;
105 description = "configured template name";
106 }
107 );
108 default = [ ];
109 };
110 useTemplate = use_template;
111
112 recursive = lib.mkOption {
113 description = ''
114 Whether to recursively snapshot dataset children.
115 You can also set this to `"zfs"` to handle datasets
116 recursively in an atomic way without the possibility to
117 override settings for child datasets.
118 '';
119 type =
120 with lib.types;
121 oneOf [
122 bool
123 (enum [ "zfs" ])
124 ];
125 default = false;
126 };
127
128 process_children_only = lib.mkOption {
129 description = "Whether to only snapshot child datasets if recursing.";
130 type = lib.types.bool;
131 default = false;
132 };
133 processChildrenOnly = process_children_only;
134 };
135
136 # Extract unique dataset names
137 datasets = lib.unique (lib.attrNames cfg.datasets);
138
139 # Function to build "zfs allow" and "zfs unallow" commands for the
140 # filesystems we've delegated permissions to.
141 buildAllowCommand =
142 zfsAction: permissions: dataset:
143 lib.escapeShellArgs [
144 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
145 "-+/run/booted-system/sw/bin/zfs"
146 zfsAction
147 "sanoid"
148 (lib.concatStringsSep "," permissions)
149 dataset
150 ];
151
152 configFile =
153 let
154 mkValueString =
155 v: if lib.isList v then lib.concatStringsSep "," v else lib.generators.mkValueStringDefault { } v;
156
157 mkKeyValue =
158 k: v:
159 if v == null then
160 ""
161 else if k == "processChildrenOnly" then
162 ""
163 else if k == "useTemplate" then
164 ""
165 else
166 lib.generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
167 in
168 lib.generators.toINI { inherit mkKeyValue; } cfg.settings;
169
170 in
171 {
172
173 # Interface
174
175 options.services.sanoid = {
176 enable = lib.mkEnableOption "Sanoid ZFS snapshotting service";
177
178 package = lib.mkPackageOption pkgs "sanoid" { };
179
180 interval = lib.mkOption {
181 type = lib.types.str;
182 default = "hourly";
183 example = "daily";
184 description = ''
185 Run sanoid at this interval. The default is to run hourly.
186
187 The format is described in
188 {manpage}`systemd.time(7)`.
189 '';
190 };
191
192 datasets = lib.mkOption {
193 type = lib.types.attrsOf (
194 lib.types.submodule (
195 { config, options, ... }:
196 {
197 freeformType = datasetSettingsType;
198 options = commonOptions // datasetOptions;
199 config.use_template = lib.modules.mkAliasAndWrapDefsWithPriority lib.id (
200 options.useTemplate or { }
201 );
202 config.process_children_only = lib.modules.mkAliasAndWrapDefsWithPriority lib.id (
203 options.processChildrenOnly or { }
204 );
205 }
206 )
207 );
208 default = { };
209 description = "Datasets to snapshot.";
210 };
211
212 templates = lib.mkOption {
213 type = lib.types.attrsOf (
214 lib.types.submodule {
215 freeformType = datasetSettingsType;
216 options = commonOptions;
217 }
218 );
219 default = { };
220 description = "Templates for datasets.";
221 };
222
223 settings = lib.mkOption {
224 type = lib.types.attrsOf datasetSettingsType;
225 description = ''
226 Free-form settings written directly to the config file. See
227 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf>
228 for allowed values.
229 '';
230 };
231
232 extraArgs = lib.mkOption {
233 type = lib.types.listOf lib.types.str;
234 default = [ ];
235 example = [
236 "--verbose"
237 "--readonly"
238 "--debug"
239 ];
240 description = ''
241 Extra arguments to pass to sanoid. See
242 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options>
243 for allowed options.
244 '';
245 };
246 };
247
248 # Implementation
249
250 config = lib.mkIf cfg.enable {
251 services.sanoid.settings = lib.mkMerge [
252 (lib.mapAttrs' (d: v: lib.nameValuePair ("template_" + d) v) cfg.templates)
253 (lib.mapAttrs (d: v: v) cfg.datasets)
254 ];
255
256 systemd.services.sanoid = {
257 description = "Sanoid snapshot service";
258 serviceConfig = {
259 ExecStartPre = (
260 map (buildAllowCommand "allow" [
261 "snapshot"
262 "mount"
263 "destroy"
264 ]) datasets
265 );
266 ExecStopPost = (
267 map (buildAllowCommand "unallow" [
268 "snapshot"
269 "mount"
270 "destroy"
271 ]) datasets
272 );
273 ExecStart = lib.escapeShellArgs (
274 [
275 "${cfg.package}/bin/sanoid"
276 "--cron"
277 "--configdir"
278 (pkgs.writeTextDir "sanoid.conf" configFile)
279 ]
280 ++ cfg.extraArgs
281 );
282 User = "sanoid";
283 Group = "sanoid";
284 DynamicUser = true;
285 RuntimeDirectory = "sanoid";
286 CacheDirectory = "sanoid";
287 };
288 # Prevents missing snapshots during DST changes
289 environment.TZ = "UTC";
290 after = [ "zfs.target" ];
291 startAt = cfg.interval;
292 };
293
294 # Put those packages in the global environment
295 # so that syncoid can find them when targeting this host through ssh.
296 environment.systemPackages = with pkgs; [
297 lzop
298 mbuffer
299 ];
300 };
301
302 meta.maintainers = with lib.maintainers; [ lopsided98 ];
303 }