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