···
{ config, lib, pkgs, ... }:
8
+
# Converts a plan like
9
+
# { "1d" = "1h"; "1w" = "1d"; }
12
+
attrToPlan = attrs: concatStringsSep "," (builtins.attrValues (
13
+
mapAttrs (n: v: "${n}=>${v}") attrs));
15
+
planDescription = ''
16
+
The znapzend backup plan to use for the source.
19
+
The plan specifies how often to backup and for how long to keep the
20
+
backups. It consists of a series of retention periodes to interval
25
+
retA=>intA,retB=>intB,...
29
+
Both intervals and retention periods are expressed in standard units
30
+
of time or multiples of them. You can use both the full name or a
31
+
shortcut according to the following listing:
35
+
second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
39
+
See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
41
+
planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
43
+
# A type for a string of the form number{b|k|M|G}
44
+
mbufferSizeType = str // {
45
+
check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
46
+
description = "string of the form number{b|k|M|G}";
49
+
# Type for a string that must contain certain other strings (the list parameter).
50
+
# Note that these would need regex escaping.
51
+
stringContainingStrings = list: let
52
+
matching = s: map (str: builtins.match ".*${str}.*" s) list;
54
+
check = x: str.check x && all isList (matching x);
55
+
description = "string containing all of the characters ${concatStringsSep ", " list}";
58
+
timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
60
+
destType = srcConfig: submodule ({ name, ... }: {
65
+
description = "Label for this destination. Defaults to the attribute name.";
70
+
description = planDescription;
71
+
example = planExample;
74
+
dataset = mkOption {
76
+
description = "Dataset name to send snapshots to.";
77
+
example = "tank/main";
83
+
Host to use for the destination dataset. Can be prefixed with
84
+
<literal>user@</literal> to specify the ssh user.
87
+
example = "john@example.com";
90
+
presend = mkOption {
93
+
Command to run before sending the snapshot to the destination.
94
+
Intended to run a remote script via <command>ssh</command> on the
95
+
destination, e.g. to bring up a backup disk or server or to put a
96
+
zpool online/offline. See also <option>postsend</option>.
99
+
example = "ssh root@bserv zpool import -Nf tank";
102
+
postsend = mkOption {
105
+
Command to run after sending the snapshot to the destination.
106
+
Intended to run a remote script via <command>ssh</command> on the
107
+
destination, e.g. to bring up a backup disk or server or to put a
108
+
zpool online/offline. See also <option>presend</option>.
111
+
example = "ssh root@bserv zpool export tank";
116
+
label = mkDefault name;
117
+
plan = mkDefault srcConfig.plan;
123
+
srcType = submodule ({ name, config, ... }: {
126
+
enable = mkOption {
128
+
description = "Whether to enable this source.";
132
+
recursive = mkOption {
134
+
description = "Whether to do recursive snapshots.";
139
+
enable = mkOption {
141
+
description = "Whether to use <command>mbuffer</command>.";
146
+
type = nullOr ints.u16;
148
+
Port to use for <command>mbuffer</command>.
151
+
If this is null, it will run <command>mbuffer</command> through
155
+
If this is not null, it will run <command>mbuffer</command>
156
+
directly through TCP, which is not encrypted but faster. In that
157
+
case the given port needs to be open on the destination host.
163
+
type = mbufferSizeType;
165
+
The size for <command>mbuffer</command>.
166
+
Supports the units b, k, M, G.
173
+
presnap = mkOption {
176
+
Command to run before snapshots are taken on the source dataset,
177
+
e.g. for database locking/flushing. See also
178
+
<option>postsnap</option>.
181
+
example = literalExample ''
182
+
''${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
186
+
postsnap = mkOption {
189
+
Command to run after snapshots are taken on the source dataset,
190
+
e.g. for database unlocking. See also <option>presnap</option>.
193
+
example = literalExample ''
194
+
''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid
198
+
timestampFormat = mkOption {
199
+
type = timestampType;
201
+
The timestamp format to use for constructing snapshot names.
202
+
The syntax is <literal>strftime</literal>-like. The string must
203
+
consist of the mandatory <literal>%Y %m %d %H %M %S</literal>.
204
+
Optionally <literal>- _ . :</literal> characters as well as any
205
+
alphanumeric character are allowed. If suffixed by a
206
+
<literal>Z</literal>, times will be in UTC.
208
+
default = "%Y-%m-%d-%H%M%S";
209
+
example = "znapzend-%m.%d.%Y-%H%M%SZ";
212
+
sendDelay = mkOption {
215
+
Specify delay (in seconds) before sending snaps to the destination.
216
+
May be useful if you want to control sending time.
224
+
description = planDescription;
225
+
example = planExample;
228
+
dataset = mkOption {
230
+
description = "The dataset to use for this source.";
231
+
example = "tank/home";
234
+
destinations = mkOption {
235
+
type = loaOf (destType config);
236
+
description = "Additional destinations.";
238
+
example = literalExample ''
241
+
dataset = "btank/backup";
242
+
presend = "zpool import -N btank";
243
+
postsend = "zpool export btank";
246
+
host = "john@example.com";
247
+
dataset = "tank/john";
255
+
dataset = mkDefault name;
260
+
### Generating the configuration from here
cfg = config.services.znapzend;
264
+
onOff = b: if b then "on" else "off";
265
+
nullOff = b: if isNull b then "off" else toString b;
266
+
stripSlashes = replaceStrings [ "/" ] [ "." ];
268
+
attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
269
+
mapAttrs (n: v: "${n}=${v}") config));
271
+
mkDestAttrs = dst: with dst;
272
+
mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
273
+
"" = optionalString (! isNull host) "${host}:" + dataset;
275
+
} // optionalAttrs (presend != null) {
277
+
} // optionalAttrs (postsend != null) {
278
+
_pstcmd = postsend;
281
+
mkSrcAttrs = srcCfg: with srcCfg; {
282
+
enabled = onOff enable;
283
+
mbuffer = with mbuffer; if enable then "${pkgs.mbuffer}/bin/mbuffer"
284
+
+ optionalString (port != null) ":${toString port}" else "off";
285
+
mbuffer_size = mbuffer.size;
286
+
post_znap_cmd = nullOff postsnap;
287
+
pre_znap_cmd = nullOff presnap;
288
+
recursive = onOff recursive;
291
+
tsformat = timestampFormat;
292
+
zend_delay = toString sendDelay;
293
+
} // fold (a: b: a // b) {} (
294
+
map mkDestAttrs (builtins.attrValues destinations)
297
+
files = mapAttrs' (n: srcCfg: let
298
+
fileText = attrsToFile (mkSrcAttrs srcCfg);
300
+
name = srcCfg.dataset;
301
+
value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
11
-
enable = mkEnableOption "ZnapZend daemon";
308
+
enable = mkEnableOption "ZnapZend ZFS backup daemon";
16
-
type = lib.types.enum ["debug" "info" "warning" "err" "alert"];
17
-
description = "The log level when logging to file. Any of debug, info, warning, err, alert. Default in daemonized form is debug.";
313
+
type = enum ["debug" "info" "warning" "err" "alert"];
315
+
The log level when logging to file. Any of debug, info, warning, err,
316
+
alert. Default in daemonized form is debug.
default = "syslog::daemon";
example = "/var/log/znapzend.log";
24
-
description = "Where to log to (syslog::<facility> or <filepath>).";
325
+
Where to log to (syslog::<facility> or <filepath>).
description = "Does all changes to the filesystem except destroy.";
autoCreation = mkOption {
36
-
description = "Automatically create the dataset on dest if it does not exists.";
338
+
description = "Automatically create the destination dataset if it does not exists.";
342
+
type = loaOf srcType;
343
+
description = "Znapzend configuration.";
345
+
example = literalExample ''
348
+
# Make snapshots of tank/home every hour, keep those for 1 day,
349
+
# keep every days snapshot for 1 month, etc.
350
+
plan = "1d=>1h,1m=>1d,1y=>1m";
352
+
# Send all those snapshots to john@example.com:rtank/john as well
353
+
destinations.remote = {
354
+
host = "john@example.com";
355
+
dataset = "rtank/john";
365
+
Do not persist any stateful znapzend setups. If this option is
366
+
enabled, your previously set znapzend setups will be cleared and only
367
+
the ones defined with this module will be applied.
···
path = with pkgs; [ zfs mbuffer openssh ];
385
+
preStart = optionalString cfg.pure ''
386
+
echo Resetting znapzend zetups
387
+
${pkgs.znapzend}/bin/znapzendzetup list \
388
+
| grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
389
+
| xargs ${pkgs.znapzend}/bin/znapzendzetup delete
390
+
'' + concatStringsSep "\n" (mapAttrsToList (dataset: config: ''
391
+
echo Importing znapzend zetup ${config} for dataset ${dataset}
392
+
${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config}
53
-
ExecStart = "${pkgs.znapzend}/bin/znapzend --logto=${cfg.logTo} --loglevel=${cfg.logLevel} ${optionalString cfg.noDestroy "--nodestroy"} ${optionalString cfg.autoCreation "--autoCreation"}";
397
+
args = concatStringsSep " " [
398
+
"--logto=${cfg.logTo}"
399
+
"--loglevel=${cfg.logLevel}"
400
+
(optionalString cfg.noDestroy "--nodestroy")
401
+
(optionalString cfg.autoCreation "--autoCreation")
402
+
]; in "${pkgs.znapzend}/bin/znapzend ${args}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
410
+
meta.maintainers = with maintainers; [ infinisil ];