1 { config, lib, pkgs, ... }:
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
12 retA=>intA,retB=>intB,...
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:
20 second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
23 See {manpage}`znapzendzetup(1)` for more info.
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}";
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;
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}";
44 timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
46 destType = srcConfig: lib.types.submodule ({ name, ... }: {
49 label = lib.mkOption {
51 description = "Label for this destination. Defaults to the attribute name.";
56 description = planDescription;
57 example = planExample;
60 dataset = lib.mkOption {
62 description = "Dataset name to send snapshots to.";
63 example = "tank/main";
67 type = lib.types.nullOr lib.types.str;
69 Host to use for the destination dataset. Can be prefixed with
70 `user@` to specify the ssh user.
73 example = "john@example.com";
76 presend = lib.mkOption {
77 type = lib.types.nullOr lib.types.str;
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`.
85 example = "ssh root@bserv zpool import -Nf tank";
88 postsend = lib.mkOption {
89 type = lib.types.nullOr lib.types.str;
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`.
97 example = "ssh root@bserv zpool export tank";
102 label = lib.mkDefault name;
103 plan = lib.mkDefault srcConfig.plan;
109 srcType = lib.types.submodule ({ name, config, ... }: {
112 enable = lib.mkOption {
113 type = lib.types.bool;
114 description = "Whether to enable this source.";
118 recursive = lib.mkOption {
119 type = lib.types.bool;
120 description = "Whether to do recursive snapshots.";
125 enable = lib.mkOption {
126 type = lib.types.bool;
127 description = "Whether to use {command}`mbuffer`.";
131 port = lib.mkOption {
132 type = lib.types.nullOr lib.types.ints.u16;
134 Port to use for {command}`mbuffer`.
136 If this is null, it will run {command}`mbuffer` through
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.
146 size = lib.mkOption {
147 type = mbufferSizeType;
149 The size for {command}`mbuffer`.
150 Supports the units b, k, M, G.
157 presnap = lib.mkOption {
158 type = lib.types.nullOr lib.types.str;
160 Command to run before snapshots are taken on the source dataset,
161 e.g. for database locking/flushing. See also
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'''
170 postsnap = lib.mkOption {
171 type = lib.types.nullOr lib.types.str;
173 Command to run after snapshots are taken on the source dataset,
174 e.g. for database unlocking. See also {option}`presnap`.
177 example = lib.literalExpression ''
178 "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
182 timestampFormat = lib.mkOption {
183 type = timestampType;
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.
192 default = "%Y-%m-%d-%H%M%S";
193 example = "znapzend-%m.%d.%Y-%H%M%SZ";
196 sendDelay = lib.mkOption {
197 type = lib.types.int;
199 Specify delay (in seconds) before sending snaps to the destination.
200 May be useful if you want to control sending time.
206 plan = lib.mkOption {
207 type = lib.types.str;
208 description = planDescription;
209 example = planExample;
212 dataset = lib.mkOption {
213 type = lib.types.str;
214 description = "The dataset to use for this source.";
215 example = "tank/home";
218 destinations = lib.mkOption {
219 type = lib.types.attrsOf (destType config);
220 description = "Additional destinations.";
222 example = lib.literalExpression ''
225 dataset = "btank/backup";
226 presend = "zpool import -N btank";
227 postsend = "zpool export btank";
230 host = "john@example.com";
231 dataset = "tank/john";
239 dataset = lib.mkDefault name;
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;
259 } // lib.optionalAttrs (presend != null) {
261 } // lib.optionalAttrs (postsend != null) {
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;
276 tsformat = timestampFormat;
277 zend_delay = toString sendDelay;
278 } // lib.foldr (a: b: a // b) {} (
279 map mkDestAttrs (builtins.attrValues destinations)
282 files = lib.mapAttrs' (n: srcCfg: let
283 fileText = attrsToFile (mkSrcAttrs srcCfg);
285 name = srcCfg.dataset;
286 value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
292 services.znapzend = {
293 enable = lib.mkEnableOption "ZnapZend ZFS backup daemon";
295 logLevel = lib.mkOption {
298 type = lib.types.enum ["debug" "info" "warning" "err" "alert"];
300 The log level when logging to file. Any of debug, info, warning, err,
301 alert. Default in daemonized form is debug.
305 logTo = lib.mkOption {
306 type = lib.types.str;
307 default = "syslog::daemon";
308 example = "/var/log/znapzend.log";
310 Where to log to (syslog::\<facility\> or \<filepath\>).
314 mailErrorSummaryTo = lib.mkOption {
315 type = lib.types.singleLineStr;
318 Email address to send a summary to if "send task(s) failed".
322 noDestroy = lib.mkOption {
323 type = lib.types.bool;
325 description = "Does all changes to the filesystem except destroy.";
328 autoCreation = lib.mkOption {
329 type = lib.types.bool;
331 description = "Automatically create the destination dataset if it does not exist.";
334 zetup = lib.mkOption {
335 type = lib.types.attrsOf srcType;
336 description = "Znapzend configuration.";
338 example = lib.literalExpression ''
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";
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";
355 pure = lib.mkOption {
356 type = lib.types.bool;
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.
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
371 features.recvu = lib.mkEnableOption ''
372 recvu feature which uses `-u` on the receiving end to keep the destination
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)`
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
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
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
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
424 config = lib.mkIf cfg.enable {
425 environment.systemPackages = [ pkgs.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} &
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
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";
474 meta.maintainers = with lib.maintainers; [ SlothOfAnarchy ];