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