···1{ config, lib, pkgs, ... }:
23with lib;
045let
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006 cfg = config.services.znapzend;
000000000000000000000000000000000000000007in
8{
9 options = {
10 services.znapzend = {
11- enable = mkEnableOption "ZnapZend daemon";
1213 logLevel = mkOption {
14 default = "debug";
15 example = "warning";
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.";
00018 };
1920 logTo = mkOption {
21- type = types.str;
22 default = "syslog::daemon";
23 example = "/var/log/znapzend.log";
24- description = "Where to log to (syslog::<facility> or <filepath>).";
0025 };
2627 noDestroy = mkOption {
28- type = types.bool;
29 default = false;
30 description = "Does all changes to the filesystem except destroy.";
31 };
3233 autoCreation = mkOption {
34- type = types.bool;
35 default = false;
36- description = "Automatically create the dataset on dest if it does not exists.";
000000000000000000000000000000037 };
38 };
39 };
···4950 path = with pkgs; [ zfs mbuffer openssh ];
51000000000052 serviceConfig = {
53- ExecStart = "${pkgs.znapzend}/bin/znapzend --logto=${cfg.logTo} --loglevel=${cfg.logLevel} ${optionalString cfg.noDestroy "--nodestroy"} ${optionalString cfg.autoCreation "--autoCreation"}";
00000054 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
55 Restart = "on-failure";
56 };
57 };
58 };
59 };
0060}
···1{ config, lib, pkgs, ... }:
23with lib;
4+with types;
56let
7+8+ # Converts a plan like
9+ # { "1d" = "1h"; "1w" = "1d"; }
10+ # into
11+ # "1d=>1h,1w=>1d"
12+ attrToPlan = attrs: concatStringsSep "," (builtins.attrValues (
13+ mapAttrs (n: v: "${n}=>${v}") attrs));
14+15+ planDescription = ''
16+ The znapzend backup plan to use for the source.
17+ </para>
18+ <para>
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
21+ associations:
22+ </para>
23+ <para>
24+ <literal>
25+ retA=>intA,retB=>intB,...
26+ </literal>
27+ </para>
28+ <para>
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:
32+ </para>
33+ <para>
34+ <literal>
35+ second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
36+ </literal>
37+ </para>
38+ <para>
39+ See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
40+ '';
41+ planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
42+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}";
47+ };
48+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;
53+ in str // {
54+ check = x: str.check x && all isList (matching x);
55+ description = "string containing all of the characters ${concatStringsSep ", " list}";
56+ };
57+58+ timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
59+60+ destType = srcConfig: submodule ({ name, ... }: {
61+ options = {
62+63+ label = mkOption {
64+ type = str;
65+ description = "Label for this destination. Defaults to the attribute name.";
66+ };
67+68+ plan = mkOption {
69+ type = str;
70+ description = planDescription;
71+ example = planExample;
72+ };
73+74+ dataset = mkOption {
75+ type = str;
76+ description = "Dataset name to send snapshots to.";
77+ example = "tank/main";
78+ };
79+80+ host = mkOption {
81+ type = nullOr str;
82+ description = ''
83+ Host to use for the destination dataset. Can be prefixed with
84+ <literal>user@</literal> to specify the ssh user.
85+ '';
86+ default = null;
87+ example = "john@example.com";
88+ };
89+90+ presend = mkOption {
91+ type = nullOr str;
92+ description = ''
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>.
97+ '';
98+ default = null;
99+ example = "ssh root@bserv zpool import -Nf tank";
100+ };
101+102+ postsend = mkOption {
103+ type = nullOr str;
104+ description = ''
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>.
109+ '';
110+ default = null;
111+ example = "ssh root@bserv zpool export tank";
112+ };
113+ };
114+115+ config = {
116+ label = mkDefault name;
117+ plan = mkDefault srcConfig.plan;
118+ };
119+ });
120+121+122+123+ srcType = submodule ({ name, config, ... }: {
124+ options = {
125+126+ enable = mkOption {
127+ type = bool;
128+ description = "Whether to enable this source.";
129+ default = true;
130+ };
131+132+ recursive = mkOption {
133+ type = bool;
134+ description = "Whether to do recursive snapshots.";
135+ default = false;
136+ };
137+138+ mbuffer = {
139+ enable = mkOption {
140+ type = bool;
141+ description = "Whether to use <command>mbuffer</command>.";
142+ default = false;
143+ };
144+145+ port = mkOption {
146+ type = nullOr ints.u16;
147+ description = ''
148+ Port to use for <command>mbuffer</command>.
149+ </para>
150+ <para>
151+ If this is null, it will run <command>mbuffer</command> through
152+ ssh.
153+ </para>
154+ <para>
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.
158+ '';
159+ default = null;
160+ };
161+162+ size = mkOption {
163+ type = mbufferSizeType;
164+ description = ''
165+ The size for <command>mbuffer</command>.
166+ Supports the units b, k, M, G.
167+ '';
168+ default = "1G";
169+ example = "128M";
170+ };
171+ };
172+173+ presnap = mkOption {
174+ type = nullOr str;
175+ description = ''
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>.
179+ '';
180+ default = null;
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
183+ '';
184+ };
185+186+ postsnap = mkOption {
187+ type = nullOr str;
188+ description = ''
189+ Command to run after snapshots are taken on the source dataset,
190+ e.g. for database unlocking. See also <option>presnap</option>.
191+ '';
192+ default = null;
193+ example = literalExample ''
194+ ''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid
195+ '';
196+ };
197+198+ timestampFormat = mkOption {
199+ type = timestampType;
200+ description = ''
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.
207+ '';
208+ default = "%Y-%m-%d-%H%M%S";
209+ example = "znapzend-%m.%d.%Y-%H%M%SZ";
210+ };
211+212+ sendDelay = mkOption {
213+ type = int;
214+ description = ''
215+ Specify delay (in seconds) before sending snaps to the destination.
216+ May be useful if you want to control sending time.
217+ '';
218+ default = 0;
219+ example = 60;
220+ };
221+222+ plan = mkOption {
223+ type = str;
224+ description = planDescription;
225+ example = planExample;
226+ };
227+228+ dataset = mkOption {
229+ type = str;
230+ description = "The dataset to use for this source.";
231+ example = "tank/home";
232+ };
233+234+ destinations = mkOption {
235+ type = loaOf (destType config);
236+ description = "Additional destinations.";
237+ default = {};
238+ example = literalExample ''
239+ {
240+ local = {
241+ dataset = "btank/backup";
242+ presend = "zpool import -N btank";
243+ postsend = "zpool export btank";
244+ };
245+ remote = {
246+ host = "john@example.com";
247+ dataset = "tank/john";
248+ };
249+ };
250+ '';
251+ };
252+ };
253+254+ config = {
255+ dataset = mkDefault name;
256+ };
257+258+ });
259+260+ ### Generating the configuration from here
261+262 cfg = config.services.znapzend;
263+264+ onOff = b: if b then "on" else "off";
265+ nullOff = b: if isNull b then "off" else toString b;
266+ stripSlashes = replaceStrings [ "/" ] [ "." ];
267+268+ attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
269+ mapAttrs (n: v: "${n}=${v}") config));
270+271+ mkDestAttrs = dst: with dst;
272+ mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
273+ "" = optionalString (! isNull host) "${host}:" + dataset;
274+ _plan = plan;
275+ } // optionalAttrs (presend != null) {
276+ _precmd = presend;
277+ } // optionalAttrs (postsend != null) {
278+ _pstcmd = postsend;
279+ });
280+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;
289+ src = dataset;
290+ src_plan = plan;
291+ tsformat = timestampFormat;
292+ zend_delay = toString sendDelay;
293+ } // fold (a: b: a // b) {} (
294+ map mkDestAttrs (builtins.attrValues destinations)
295+ );
296+297+ files = mapAttrs' (n: srcCfg: let
298+ fileText = attrsToFile (mkSrcAttrs srcCfg);
299+ in {
300+ name = srcCfg.dataset;
301+ value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
302+ }) cfg.zetup;
303+304in
305{
306 options = {
307 services.znapzend = {
308+ enable = mkEnableOption "ZnapZend ZFS backup daemon";
309310 logLevel = mkOption {
311 default = "debug";
312 example = "warning";
313+ type = enum ["debug" "info" "warning" "err" "alert"];
314+ description = ''
315+ The log level when logging to file. Any of debug, info, warning, err,
316+ alert. Default in daemonized form is debug.
317+ '';
318 };
319320 logTo = mkOption {
321+ type = str;
322 default = "syslog::daemon";
323 example = "/var/log/znapzend.log";
324+ description = ''
325+ Where to log to (syslog::<facility> or <filepath>).
326+ '';
327 };
328329 noDestroy = mkOption {
330+ type = bool;
331 default = false;
332 description = "Does all changes to the filesystem except destroy.";
333 };
334335 autoCreation = mkOption {
336+ type = bool;
337 default = false;
338+ description = "Automatically create the destination dataset if it does not exists.";
339+ };
340+341+ zetup = mkOption {
342+ type = loaOf srcType;
343+ description = "Znapzend configuration.";
344+ default = {};
345+ example = literalExample ''
346+ {
347+ "tank/home" = {
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";
351+ recursive = true;
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";
356+ };
357+ };
358+ };
359+ '';
360+ };
361+362+ pure = mkOption {
363+ type = bool;
364+ description = ''
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.
368+ '';
369+ default = false;
370 };
371 };
372 };
···382383 path = with pkgs; [ zfs mbuffer openssh ];
384385+ 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}
393+ '') files);
394+395 serviceConfig = {
396+ ExecStart = let
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}";
403 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
404 Restart = "on-failure";
405 };
406 };
407 };
408 };
409+410+ meta.maintainers = with maintainers; [ infinisil ];
411}