vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / backup / znapzend.nix
bloba3ea8b209bb66bd9202f59ec4dc03d26816031aa
1 { config, lib, pkgs, ... }:
2 let
4   planDescription = ''
5       The znapzend backup plan to use for the source.
7       The plan specifies how often to backup and for how long to keep the
8       backups. It consists of a series of retention periods to interval
9       associations:
11       ```
12         retA=>intA,retB=>intB,...
13       ```
15       Both intervals and retention periods are expressed in standard units
16       of time or multiples of them. You can use both the full name or a
17       shortcut according to the following listing:
19       ```
20         second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
21       ```
23       See {manpage}`znapzendzetup(1)` for more info.
24   '';
25   planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
27   # A type for a string of the form number{b|k|M|G}
28   mbufferSizeType = lib.types.str // {
29     check = x: lib.types.str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
30     description = "string of the form number{b|k|M|G}";
31   };
33   enabledFeatures = lib.concatLists (lib.mapAttrsToList (name: enabled: lib.optional enabled name) cfg.features);
35   # Type for a string that must contain certain other strings (the list parameter).
36   # Note that these would need regex escaping.
37   stringContainingStrings = list: let
38     matching = s: map (str: builtins.match ".*${str}.*" s) list;
39   in lib.types.str // {
40     check = x: lib.types.str.check x && lib.all lib.isList (matching x);
41     description = "string containing all of the characters ${lib.concatStringsSep ", " list}";
42   };
44   timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
46   destType = srcConfig: lib.types.submodule ({ name, ... }: {
47     options = {
49       label = lib.mkOption {
50         type = lib.types.str;
51         description = "Label for this destination. Defaults to the attribute name.";
52       };
54       plan = lib.mkOption {
55         type = lib.types.str;
56         description = planDescription;
57         example = planExample;
58       };
60       dataset = lib.mkOption {
61         type = lib.types.str;
62         description = "Dataset name to send snapshots to.";
63         example = "tank/main";
64       };
66       host = lib.mkOption {
67         type = lib.types.nullOr lib.types.str;
68         description = ''
69           Host to use for the destination dataset. Can be prefixed with
70           `user@` to specify the ssh user.
71         '';
72         default = null;
73         example = "john@example.com";
74       };
76       presend = lib.mkOption {
77         type = lib.types.nullOr lib.types.str;
78         description = ''
79           Command to run before sending the snapshot to the destination.
80           Intended to run a remote script via {command}`ssh` on the
81           destination, e.g. to bring up a backup disk or server or to put a
82           zpool online/offline. See also {option}`postsend`.
83         '';
84         default = null;
85         example = "ssh root@bserv zpool import -Nf tank";
86       };
88       postsend = lib.mkOption {
89         type = lib.types.nullOr lib.types.str;
90         description = ''
91           Command to run after sending the snapshot to the destination.
92           Intended to run a remote script via {command}`ssh` on the
93           destination, e.g. to bring up a backup disk or server or to put a
94           zpool online/offline. See also {option}`presend`.
95         '';
96         default = null;
97         example = "ssh root@bserv zpool export tank";
98       };
99     };
101     config = {
102       label = lib.mkDefault name;
103       plan = lib.mkDefault srcConfig.plan;
104     };
105   });
109   srcType = lib.types.submodule ({ name, config, ... }: {
110     options = {
112       enable = lib.mkOption {
113         type = lib.types.bool;
114         description = "Whether to enable this source.";
115         default = true;
116       };
118       recursive = lib.mkOption {
119         type = lib.types.bool;
120         description = "Whether to do recursive snapshots.";
121         default = false;
122       };
124       mbuffer = {
125         enable = lib.mkOption {
126           type = lib.types.bool;
127           description = "Whether to use {command}`mbuffer`.";
128           default = false;
129         };
131         port = lib.mkOption {
132           type = lib.types.nullOr lib.types.ints.u16;
133           description = ''
134               Port to use for {command}`mbuffer`.
136               If this is null, it will run {command}`mbuffer` through
137               ssh.
139               If this is not null, it will run {command}`mbuffer`
140               directly through TCP, which is not encrypted but faster. In that
141               case the given port needs to be open on the destination host.
142           '';
143           default = null;
144         };
146         size = lib.mkOption {
147           type = mbufferSizeType;
148           description = ''
149             The size for {command}`mbuffer`.
150             Supports the units b, k, M, G.
151           '';
152           default = "1G";
153           example = "128M";
154         };
155       };
157       presnap = lib.mkOption {
158         type = lib.types.nullOr lib.types.str;
159         description = ''
160           Command to run before snapshots are taken on the source dataset,
161           e.g. for database locking/flushing. See also
162           {option}`postsnap`.
163         '';
164         default = null;
165         example = lib.literalExpression ''
166           '''''${pkgs.mariadb}/bin/mysql -e "set autocommit=0;flush tables with read lock;\\! ''${pkgs.coreutils}/bin/sleep 600" &  ''${pkgs.coreutils}/bin/echo $! > /tmp/mariadblock.pid ; sleep 10'''
167         '';
168       };
170       postsnap = lib.mkOption {
171         type = lib.types.nullOr lib.types.str;
172         description = ''
173           Command to run after snapshots are taken on the source dataset,
174           e.g. for database unlocking. See also {option}`presnap`.
175         '';
176         default = null;
177         example = lib.literalExpression ''
178           "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
179         '';
180       };
182       timestampFormat = lib.mkOption {
183         type = timestampType;
184         description = ''
185           The timestamp format to use for constructing snapshot names.
186           The syntax is `strftime`-like. The string must
187           consist of the mandatory `%Y %m %d %H %M %S`.
188           Optionally  `- _ . :`  characters as well as any
189           alphanumeric character are allowed. If suffixed by a
190           `Z`, times will be in UTC.
191         '';
192         default = "%Y-%m-%d-%H%M%S";
193         example = "znapzend-%m.%d.%Y-%H%M%SZ";
194       };
196       sendDelay = lib.mkOption {
197         type = lib.types.int;
198         description = ''
199           Specify delay (in seconds) before sending snaps to the destination.
200           May be useful if you want to control sending time.
201         '';
202         default = 0;
203         example = 60;
204       };
206       plan = lib.mkOption {
207         type = lib.types.str;
208         description = planDescription;
209         example = planExample;
210       };
212       dataset = lib.mkOption {
213         type = lib.types.str;
214         description = "The dataset to use for this source.";
215         example = "tank/home";
216       };
218       destinations = lib.mkOption {
219         type = lib.types.attrsOf (destType config);
220         description = "Additional destinations.";
221         default = {};
222         example = lib.literalExpression ''
223           {
224             local = {
225               dataset = "btank/backup";
226               presend = "zpool import -N btank";
227               postsend = "zpool export btank";
228             };
229             remote = {
230               host = "john@example.com";
231               dataset = "tank/john";
232             };
233           };
234         '';
235       };
236     };
238     config = {
239       dataset = lib.mkDefault name;
240     };
242   });
244   ### Generating the configuration from here
246   cfg = config.services.znapzend;
248   onOff = b: if b then "on" else "off";
249   nullOff = b: if b == null then "off" else toString b;
250   stripSlashes = lib.replaceStrings [ "/" ] [ "." ];
252   attrsToFile = config: lib.concatStringsSep "\n" (builtins.attrValues (
253     lib.mapAttrs (n: v: "${n}=${v}") config));
255   mkDestAttrs = dst: with dst;
256     lib.mapAttrs' (n: v: lib.nameValuePair "dst_${label}${n}" v) ({
257       "" = lib.optionalString (host != null) "${host}:" + dataset;
258       _plan = plan;
259     } // lib.optionalAttrs (presend != null) {
260       _precmd = presend;
261     } // lib.optionalAttrs (postsend != null) {
262       _pstcmd = postsend;
263     });
265   mkSrcAttrs = srcCfg: with srcCfg; {
266     enabled = onOff enable;
267     # mbuffer is not referenced by its full path to accommodate non-NixOS systems or differing mbuffer versions between source and target
268     mbuffer = with mbuffer; if enable then "mbuffer"
269         + lib.optionalString (port != null) ":${toString port}" else "off";
270     mbuffer_size = mbuffer.size;
271     post_znap_cmd = nullOff postsnap;
272     pre_znap_cmd = nullOff presnap;
273     recursive = onOff recursive;
274     src = dataset;
275     src_plan = plan;
276     tsformat = timestampFormat;
277     zend_delay = toString sendDelay;
278   } // lib.foldr (a: b: a // b) {} (
279     map mkDestAttrs (builtins.attrValues destinations)
280   );
282   files = lib.mapAttrs' (n: srcCfg: let
283     fileText = attrsToFile (mkSrcAttrs srcCfg);
284   in {
285     name = srcCfg.dataset;
286     value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
287   }) cfg.zetup;
291   options = {
292     services.znapzend = {
293       enable = lib.mkEnableOption "ZnapZend ZFS backup daemon";
295       logLevel = lib.mkOption {
296         default = "debug";
297         example = "warning";
298         type = lib.types.enum ["debug" "info" "warning" "err" "alert"];
299         description = ''
300           The log level when logging to file. Any of debug, info, warning, err,
301           alert. Default in daemonized form is debug.
302         '';
303       };
305       logTo = lib.mkOption {
306         type = lib.types.str;
307         default = "syslog::daemon";
308         example = "/var/log/znapzend.log";
309         description = ''
310           Where to log to (syslog::\<facility\> or \<filepath\>).
311         '';
312       };
314       mailErrorSummaryTo = lib.mkOption {
315         type = lib.types.singleLineStr;
316         default = "";
317         description = ''
318           Email address to send a summary to if "send task(s) failed".
319         '';
320       };
322       noDestroy = lib.mkOption {
323         type = lib.types.bool;
324         default = false;
325         description = "Does all changes to the filesystem except destroy.";
326       };
328       autoCreation = lib.mkOption {
329         type = lib.types.bool;
330         default = false;
331         description = "Automatically create the destination dataset if it does not exist.";
332       };
334       zetup = lib.mkOption {
335         type = lib.types.attrsOf srcType;
336         description = "Znapzend configuration.";
337         default = {};
338         example = lib.literalExpression ''
339           {
340             "tank/home" = {
341               # Make snapshots of tank/home every hour, keep those for 1 day,
342               # keep every days snapshot for 1 month, etc.
343               plan = "1d=>1h,1m=>1d,1y=>1m";
344               recursive = true;
345               # Send all those snapshots to john@example.com:rtank/john as well
346               destinations.remote = {
347                 host = "john@example.com";
348                 dataset = "rtank/john";
349               };
350             };
351           };
352         '';
353       };
355       pure = lib.mkOption {
356         type = lib.types.bool;
357         description = ''
358           Do not persist any stateful znapzend setups. If this option is
359           enabled, your previously set znapzend setups will be cleared and only
360           the ones defined with this module will be applied.
361         '';
362         default = false;
363       };
365       features.oracleMode = lib.mkEnableOption ''
366         destroying snapshots one by one instead of using one long argument list.
367         If source and destination are out of sync for a long time, you may have
368         so many snapshots to destroy that the argument gets is too long and the
369         command fails
370       '';
371       features.recvu = lib.mkEnableOption ''
372         recvu feature which uses `-u` on the receiving end to keep the destination
373         filesystem unmounted
374       '';
375       features.compressed = lib.mkEnableOption ''
376         compressed feature which adds the options `-Lce` to
377         the {command}`zfs send` command. When this is enabled, make
378         sure that both the sending and receiving pool have the same relevant
379         features enabled. Using `-c` will skip unnecessary
380         decompress-compress stages, `-L` is for large block
381         support and -e is for embedded data support. see
382         {manpage}`znapzend(1)`
383         and {manpage}`zfs(8)`
384         for more info
385       '';
386       features.sendRaw = lib.mkEnableOption ''
387         sendRaw feature which adds the options `-w` to the
388         {command}`zfs send` command. For encrypted source datasets this
389         instructs zfs not to decrypt before sending which results in a remote
390         backup that can't be read without the encryption key/passphrase, useful
391         when the remote isn't fully trusted or not physically secure. This
392         option must be used consistently, raw incrementals cannot be based on
393         non-raw snapshots and vice versa
394       '';
395       features.skipIntermediates = lib.mkEnableOption ''
396         the skipIntermediates feature to send a single increment
397         between latest common snapshot and the newly made one. It may skip
398         several source snaps if the destination was offline for some time, and
399         it should skip snapshots not managed by znapzend. Normally for online
400         destinations, the new snapshot is sent as soon as it is created on the
401         source, so there are no automatic increments to skip
402       '';
403       features.lowmemRecurse = lib.mkEnableOption ''
404         use lowmemRecurse on systems where you have too many datasets, so a
405         recursive listing of attributes to find backup plans exhausts the
406         memory available to {command}`znapzend`: instead, go the slower
407         way to first list all impacted dataset names, and then query their
408         configs one by one
409       '';
410       features.zfsGetType = lib.mkEnableOption ''
411         using zfsGetType if your {command}`zfs get` supports a
412         `-t` argument for filtering by dataset type at all AND
413         lists properties for snapshots by default when recursing, so that there
414         is too much data to process while searching for backup plans.
415         If these two conditions apply to your system, the time needed for a
416         `--recursive` search for backup plans can literally
417         differ by hundreds of times (depending on the amount of snapshots in
418         that dataset tree... and a decent backup plan will ensure you have a lot
419         of those), so you would benefit from requesting this feature
420       '';
421     };
422   };
424   config = lib.mkIf cfg.enable {
425     environment.systemPackages = [ pkgs.znapzend ];
427     systemd.services = {
428       znapzend = {
429         description = "ZnapZend - ZFS Backup System";
430         wantedBy    = [ "zfs.target" ];
431         after       = [ "zfs.target" ];
433         path = with pkgs; [ zfs mbuffer openssh ];
435         preStart = lib.optionalString cfg.pure ''
436           echo Resetting znapzend zetups
437           ${pkgs.znapzend}/bin/znapzendzetup list \
438             | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
439             | xargs -I{} ${pkgs.znapzend}/bin/znapzendzetup delete "{}"
440         '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (dataset: config: ''
441           echo Importing znapzend zetup ${config} for dataset ${dataset}
442           ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config} &
443         '') files) + ''
444           wait
445         '';
447         serviceConfig = {
448           # znapzendzetup --import apparently tries to connect to the backup
449           # host 3 times with a timeout of 30 seconds, leading to a startup
450           # delay of >90s when the host is down, which is just above the default
451           # service timeout of 90 seconds. Increase the timeout so it doesn't
452           # make the service fail in that case.
453           TimeoutStartSec = 180;
454           # Needs to have write access to ZFS
455           User = "root";
456           ExecStart = let
457             args = lib.concatStringsSep " " [
458               "--logto=${cfg.logTo}"
459               "--loglevel=${cfg.logLevel}"
460               (lib.optionalString cfg.noDestroy "--nodestroy")
461               (lib.optionalString cfg.autoCreation "--autoCreation")
462               (lib.optionalString (cfg.mailErrorSummaryTo != "")
463                 "--mailErrorSummaryTo=${cfg.mailErrorSummaryTo}")
464               (lib.optionalString (enabledFeatures != [])
465                 "--features=${lib.concatStringsSep "," enabledFeatures}")
466             ]; in "${pkgs.znapzend}/bin/znapzend ${args}";
467           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
468           Restart = "on-failure";
469         };
470       };
471     };
472   };
474   meta.maintainers = with lib.maintainers; [ SlothOfAnarchy ];