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
17 retA=>intA,retB=>intB,...
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:
25 second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
28 See {manpage}`znapzendzetup(1)` for more info.
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}";
38 enabledFeatures = lib.concatLists (
39 lib.mapAttrsToList (name: enabled: lib.optional enabled name) cfg.features
42 # Type for a string that must contain certain other strings (the list parameter).
43 # Note that these would need regex escaping.
44 stringContainingStrings =
47 matching = s: map (str: builtins.match ".*${str}.*" s) list;
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}";
55 timestampType = stringContainingStrings [
71 label = lib.mkOption {
73 description = "Label for this destination. Defaults to the attribute name.";
78 description = planDescription;
79 example = planExample;
82 dataset = lib.mkOption {
84 description = "Dataset name to send snapshots to.";
85 example = "tank/main";
89 type = lib.types.nullOr lib.types.str;
91 Host to use for the destination dataset. Can be prefixed with
92 `user@` to specify the ssh user.
95 example = "john@example.com";
98 presend = lib.mkOption {
99 type = lib.types.nullOr lib.types.str;
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`.
107 example = "ssh root@bserv zpool import -Nf tank";
110 postsend = lib.mkOption {
111 type = lib.types.nullOr lib.types.str;
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`.
119 example = "ssh root@bserv zpool export tank";
124 label = lib.mkDefault name;
125 plan = lib.mkDefault srcConfig.plan;
130 srcType = lib.types.submodule (
131 { name, config, ... }:
135 enable = lib.mkOption {
136 type = lib.types.bool;
137 description = "Whether to enable this source.";
141 recursive = lib.mkOption {
142 type = lib.types.bool;
143 description = "Whether to do recursive snapshots.";
148 enable = lib.mkOption {
149 type = lib.types.bool;
150 description = "Whether to use {command}`mbuffer`.";
154 port = lib.mkOption {
155 type = lib.types.nullOr lib.types.ints.u16;
157 Port to use for {command}`mbuffer`.
159 If this is null, it will run {command}`mbuffer` through
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.
169 size = lib.mkOption {
170 type = mbufferSizeType;
172 The size for {command}`mbuffer`.
173 Supports the units b, k, M, G.
180 presnap = lib.mkOption {
181 type = lib.types.nullOr lib.types.str;
183 Command to run before snapshots are taken on the source dataset,
184 e.g. for database locking/flushing. See also
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'''
193 postsnap = lib.mkOption {
194 type = lib.types.nullOr lib.types.str;
196 Command to run after snapshots are taken on the source dataset,
197 e.g. for database unlocking. See also {option}`presnap`.
200 example = lib.literalExpression ''
201 "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
205 timestampFormat = lib.mkOption {
206 type = timestampType;
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.
215 default = "%Y-%m-%d-%H%M%S";
216 example = "znapzend-%m.%d.%Y-%H%M%SZ";
219 sendDelay = lib.mkOption {
220 type = lib.types.int;
222 Specify delay (in seconds) before sending snaps to the destination.
223 May be useful if you want to control sending time.
229 plan = lib.mkOption {
230 type = lib.types.str;
231 description = planDescription;
232 example = planExample;
235 dataset = lib.mkOption {
236 type = lib.types.str;
237 description = "The dataset to use for this source.";
238 example = "tank/home";
241 destinations = lib.mkOption {
242 type = lib.types.attrsOf (destType config);
243 description = "Additional destinations.";
245 example = lib.literalExpression ''
248 dataset = "btank/backup";
249 presend = "zpool import -N btank";
250 postsend = "zpool export btank";
253 host = "john@example.com";
254 dataset = "tank/john";
262 dataset = lib.mkDefault name;
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 [ "/" ] [ "." ];
277 config: lib.concatStringsSep "\n" (builtins.attrValues (lib.mapAttrs (n: v: "${n}=${v}") config));
282 lib.mapAttrs' (n: v: lib.nameValuePair "dst_${label}${n}" v) (
284 "" = lib.optionalString (host != null) "${host}:" + dataset;
287 // lib.optionalAttrs (presend != null) {
290 // lib.optionalAttrs (postsend != null) {
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
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;
310 tsformat = timestampFormat;
311 zend_delay = toString sendDelay;
313 // lib.foldr (a: b: a // b) { } (map mkDestAttrs (builtins.attrValues destinations));
315 files = lib.mapAttrs' (
318 fileText = attrsToFile (mkSrcAttrs srcCfg);
321 name = srcCfg.dataset;
322 value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
329 services.znapzend = {
330 enable = lib.mkEnableOption "ZnapZend ZFS backup daemon";
332 logLevel = lib.mkOption {
335 type = lib.types.enum [
343 The log level when logging to file. Any of debug, info, warning, err,
344 alert. Default in daemonized form is debug.
348 logTo = lib.mkOption {
349 type = lib.types.str;
350 default = "syslog::daemon";
351 example = "/var/log/znapzend.log";
353 Where to log to (syslog::\<facility\> or \<filepath\>).
357 mailErrorSummaryTo = lib.mkOption {
358 type = lib.types.singleLineStr;
361 Email address to send a summary to if "send task(s) failed".
365 noDestroy = lib.mkOption {
366 type = lib.types.bool;
368 description = "Does all changes to the filesystem except destroy.";
371 autoCreation = lib.mkOption {
372 type = lib.types.bool;
374 description = "Automatically create the destination dataset if it does not exist.";
377 zetup = lib.mkOption {
378 type = lib.types.attrsOf srcType;
379 description = "Znapzend configuration.";
381 example = lib.literalExpression ''
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";
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";
398 pure = lib.mkOption {
399 type = lib.types.bool;
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.
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
414 features.recvu = lib.mkEnableOption ''
415 recvu feature which uses `-u` on the receiving end to keep the destination
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)`
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
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
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
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
467 config = lib.mkIf cfg.enable {
468 environment.systemPackages = [ pkgs.znapzend ];
472 description = "ZnapZend - ZFS Backup System";
473 wantedBy = [ "zfs.target" ];
474 after = [ "zfs.target" ];
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 "{}"
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} &
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
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}")
521 "${pkgs.znapzend}/bin/znapzend ${args}";
522 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
523 Restart = "on-failure";
529 meta.maintainers = with lib.maintainers; [ SlothOfAnarchy ];