1 { config, lib, pkgs, ... }:
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
16 retA=>intA,retB=>intB,...
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:
24 second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
27 See {manpage}`znapzendzetup(1)` for more info.
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}";
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;
44 check = x: str.check x && all isList (matching x);
45 description = "string containing all of the characters ${concatStringsSep ", " list}";
48 timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
50 destType = srcConfig: submodule ({ name, ... }: {
55 description = lib.mdDoc "Label for this destination. Defaults to the attribute name.";
60 description = lib.mdDoc planDescription;
61 example = planExample;
66 description = lib.mdDoc "Dataset name to send snapshots to.";
67 example = "tank/main";
72 description = lib.mdDoc ''
73 Host to use for the destination dataset. Can be prefixed with
74 `user@` to specify the ssh user.
77 example = "john@example.com";
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`.
89 example = "ssh root@bserv zpool import -Nf tank";
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`.
101 example = "ssh root@bserv zpool export tank";
106 label = mkDefault name;
107 plan = mkDefault srcConfig.plan;
113 srcType = submodule ({ name, config, ... }: {
118 description = lib.mdDoc "Whether to enable this source.";
122 recursive = mkOption {
124 description = lib.mdDoc "Whether to do recursive snapshots.";
131 description = lib.mdDoc "Whether to use {command}`mbuffer`.";
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
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.
151 type = mbufferSizeType;
152 description = lib.mdDoc ''
153 The size for {command}`mbuffer`.
154 Supports the units b, k, M, G.
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
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'''
174 postsnap = mkOption {
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`.
181 example = literalExpression ''
182 "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
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.
196 default = "%Y-%m-%d-%H%M%S";
197 example = "znapzend-%m.%d.%Y-%H%M%SZ";
200 sendDelay = mkOption {
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.
212 description = lib.mdDoc planDescription;
213 example = planExample;
218 description = lib.mdDoc "The dataset to use for this source.";
219 example = "tank/home";
222 destinations = mkOption {
223 type = attrsOf (destType config);
224 description = lib.mdDoc "Additional destinations.";
226 example = literalExpression ''
229 dataset = "btank/backup";
230 presend = "zpool import -N btank";
231 postsend = "zpool export btank";
234 host = "john@example.com";
235 dataset = "tank/john";
243 dataset = mkDefault name;
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;
263 } // optionalAttrs (presend != null) {
265 } // optionalAttrs (postsend != null) {
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;
280 tsformat = timestampFormat;
281 zend_delay = toString sendDelay;
282 } // foldr (a: b: a // b) {} (
283 map mkDestAttrs (builtins.attrValues destinations)
286 files = mapAttrs' (n: srcCfg: let
287 fileText = attrsToFile (mkSrcAttrs srcCfg);
289 name = srcCfg.dataset;
290 value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
296 services.znapzend = {
297 enable = mkEnableOption (lib.mdDoc "ZnapZend ZFS backup daemon");
299 logLevel = mkOption {
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.
311 default = "syslog::daemon";
312 example = "/var/log/znapzend.log";
313 description = lib.mdDoc ''
314 Where to log to (syslog::\<facility\> or \<filepath\>).
318 noDestroy = mkOption {
321 description = lib.mdDoc "Does all changes to the filesystem except destroy.";
324 autoCreation = mkOption {
327 description = lib.mdDoc "Automatically create the destination dataset if it does not exist.";
331 type = attrsOf srcType;
332 description = lib.mdDoc "Znapzend configuration.";
334 example = literalExpression ''
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";
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";
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.
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
367 features.recvu = mkEnableOption (lib.mdDoc ''
368 recvu feature which uses `-u` on the receiving end to keep the destination
369 filesystem unmounted.
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)`
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.
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.
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
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.
420 config = mkIf cfg.enable {
421 environment.systemPackages = [ pkgs.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} &
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
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";
468 meta.maintainers = with maintainers; [ infinisil SlothOfAnarchy ];