···11{ config, lib, pkgs, ... }:
2233with lib;
44+with types;
4556let
77+88+ # Converts a plan like
99+ # { "1d" = "1h"; "1w" = "1d"; }
1010+ # into
1111+ # "1d=>1h,1w=>1d"
1212+ attrToPlan = attrs: concatStringsSep "," (builtins.attrValues (
1313+ mapAttrs (n: v: "${n}=>${v}") attrs));
1414+1515+ planDescription = ''
1616+ The znapzend backup plan to use for the source.
1717+ </para>
1818+ <para>
1919+ The plan specifies how often to backup and for how long to keep the
2020+ backups. It consists of a series of retention periodes to interval
2121+ associations:
2222+ </para>
2323+ <para>
2424+ <literal>
2525+ retA=>intA,retB=>intB,...
2626+ </literal>
2727+ </para>
2828+ <para>
2929+ Both intervals and retention periods are expressed in standard units
3030+ of time or multiples of them. You can use both the full name or a
3131+ shortcut according to the following listing:
3232+ </para>
3333+ <para>
3434+ <literal>
3535+ second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
3636+ </literal>
3737+ </para>
3838+ <para>
3939+ See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
4040+ '';
4141+ planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
4242+4343+ # A type for a string of the form number{b|k|M|G}
4444+ mbufferSizeType = str // {
4545+ check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
4646+ description = "string of the form number{b|k|M|G}";
4747+ };
4848+4949+ # Type for a string that must contain certain other strings (the list parameter).
5050+ # Note that these would need regex escaping.
5151+ stringContainingStrings = list: let
5252+ matching = s: map (str: builtins.match ".*${str}.*" s) list;
5353+ in str // {
5454+ check = x: str.check x && all isList (matching x);
5555+ description = "string containing all of the characters ${concatStringsSep ", " list}";
5656+ };
5757+5858+ timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
5959+6060+ destType = srcConfig: submodule ({ name, ... }: {
6161+ options = {
6262+6363+ label = mkOption {
6464+ type = str;
6565+ description = "Label for this destination. Defaults to the attribute name.";
6666+ };
6767+6868+ plan = mkOption {
6969+ type = str;
7070+ description = planDescription;
7171+ example = planExample;
7272+ };
7373+7474+ dataset = mkOption {
7575+ type = str;
7676+ description = "Dataset name to send snapshots to.";
7777+ example = "tank/main";
7878+ };
7979+8080+ host = mkOption {
8181+ type = nullOr str;
8282+ description = ''
8383+ Host to use for the destination dataset. Can be prefixed with
8484+ <literal>user@</literal> to specify the ssh user.
8585+ '';
8686+ default = null;
8787+ example = "john@example.com";
8888+ };
8989+9090+ presend = mkOption {
9191+ type = nullOr str;
9292+ description = ''
9393+ Command to run before sending the snapshot to the destination.
9494+ Intended to run a remote script via <command>ssh</command> on the
9595+ destination, e.g. to bring up a backup disk or server or to put a
9696+ zpool online/offline. See also <option>postsend</option>.
9797+ '';
9898+ default = null;
9999+ example = "ssh root@bserv zpool import -Nf tank";
100100+ };
101101+102102+ postsend = mkOption {
103103+ type = nullOr str;
104104+ description = ''
105105+ Command to run after sending the snapshot to the destination.
106106+ Intended to run a remote script via <command>ssh</command> on the
107107+ destination, e.g. to bring up a backup disk or server or to put a
108108+ zpool online/offline. See also <option>presend</option>.
109109+ '';
110110+ default = null;
111111+ example = "ssh root@bserv zpool export tank";
112112+ };
113113+ };
114114+115115+ config = {
116116+ label = mkDefault name;
117117+ plan = mkDefault srcConfig.plan;
118118+ };
119119+ });
120120+121121+122122+123123+ srcType = submodule ({ name, config, ... }: {
124124+ options = {
125125+126126+ enable = mkOption {
127127+ type = bool;
128128+ description = "Whether to enable this source.";
129129+ default = true;
130130+ };
131131+132132+ recursive = mkOption {
133133+ type = bool;
134134+ description = "Whether to do recursive snapshots.";
135135+ default = false;
136136+ };
137137+138138+ mbuffer = {
139139+ enable = mkOption {
140140+ type = bool;
141141+ description = "Whether to use <command>mbuffer</command>.";
142142+ default = false;
143143+ };
144144+145145+ port = mkOption {
146146+ type = nullOr ints.u16;
147147+ description = ''
148148+ Port to use for <command>mbuffer</command>.
149149+ </para>
150150+ <para>
151151+ If this is null, it will run <command>mbuffer</command> through
152152+ ssh.
153153+ </para>
154154+ <para>
155155+ If this is not null, it will run <command>mbuffer</command>
156156+ directly through TCP, which is not encrypted but faster. In that
157157+ case the given port needs to be open on the destination host.
158158+ '';
159159+ default = null;
160160+ };
161161+162162+ size = mkOption {
163163+ type = mbufferSizeType;
164164+ description = ''
165165+ The size for <command>mbuffer</command>.
166166+ Supports the units b, k, M, G.
167167+ '';
168168+ default = "1G";
169169+ example = "128M";
170170+ };
171171+ };
172172+173173+ presnap = mkOption {
174174+ type = nullOr str;
175175+ description = ''
176176+ Command to run before snapshots are taken on the source dataset,
177177+ e.g. for database locking/flushing. See also
178178+ <option>postsnap</option>.
179179+ '';
180180+ default = null;
181181+ example = literalExample ''
182182+ ''${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
183183+ '';
184184+ };
185185+186186+ postsnap = mkOption {
187187+ type = nullOr str;
188188+ description = ''
189189+ Command to run after snapshots are taken on the source dataset,
190190+ e.g. for database unlocking. See also <option>presnap</option>.
191191+ '';
192192+ default = null;
193193+ example = literalExample ''
194194+ ''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid
195195+ '';
196196+ };
197197+198198+ timestampFormat = mkOption {
199199+ type = timestampType;
200200+ description = ''
201201+ The timestamp format to use for constructing snapshot names.
202202+ The syntax is <literal>strftime</literal>-like. The string must
203203+ consist of the mandatory <literal>%Y %m %d %H %M %S</literal>.
204204+ Optionally <literal>- _ . :</literal> characters as well as any
205205+ alphanumeric character are allowed. If suffixed by a
206206+ <literal>Z</literal>, times will be in UTC.
207207+ '';
208208+ default = "%Y-%m-%d-%H%M%S";
209209+ example = "znapzend-%m.%d.%Y-%H%M%SZ";
210210+ };
211211+212212+ sendDelay = mkOption {
213213+ type = int;
214214+ description = ''
215215+ Specify delay (in seconds) before sending snaps to the destination.
216216+ May be useful if you want to control sending time.
217217+ '';
218218+ default = 0;
219219+ example = 60;
220220+ };
221221+222222+ plan = mkOption {
223223+ type = str;
224224+ description = planDescription;
225225+ example = planExample;
226226+ };
227227+228228+ dataset = mkOption {
229229+ type = str;
230230+ description = "The dataset to use for this source.";
231231+ example = "tank/home";
232232+ };
233233+234234+ destinations = mkOption {
235235+ type = loaOf (destType config);
236236+ description = "Additional destinations.";
237237+ default = {};
238238+ example = literalExample ''
239239+ {
240240+ local = {
241241+ dataset = "btank/backup";
242242+ presend = "zpool import -N btank";
243243+ postsend = "zpool export btank";
244244+ };
245245+ remote = {
246246+ host = "john@example.com";
247247+ dataset = "tank/john";
248248+ };
249249+ };
250250+ '';
251251+ };
252252+ };
253253+254254+ config = {
255255+ dataset = mkDefault name;
256256+ };
257257+258258+ });
259259+260260+ ### Generating the configuration from here
261261+6262 cfg = config.services.znapzend;
263263+264264+ onOff = b: if b then "on" else "off";
265265+ nullOff = b: if isNull b then "off" else toString b;
266266+ stripSlashes = replaceStrings [ "/" ] [ "." ];
267267+268268+ attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
269269+ mapAttrs (n: v: "${n}=${v}") config));
270270+271271+ mkDestAttrs = dst: with dst;
272272+ mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
273273+ "" = optionalString (! isNull host) "${host}:" + dataset;
274274+ _plan = plan;
275275+ } // optionalAttrs (presend != null) {
276276+ _precmd = presend;
277277+ } // optionalAttrs (postsend != null) {
278278+ _pstcmd = postsend;
279279+ });
280280+281281+ mkSrcAttrs = srcCfg: with srcCfg; {
282282+ enabled = onOff enable;
283283+ mbuffer = with mbuffer; if enable then "${pkgs.mbuffer}/bin/mbuffer"
284284+ + optionalString (port != null) ":${toString port}" else "off";
285285+ mbuffer_size = mbuffer.size;
286286+ post_znap_cmd = nullOff postsnap;
287287+ pre_znap_cmd = nullOff presnap;
288288+ recursive = onOff recursive;
289289+ src = dataset;
290290+ src_plan = plan;
291291+ tsformat = timestampFormat;
292292+ zend_delay = toString sendDelay;
293293+ } // fold (a: b: a // b) {} (
294294+ map mkDestAttrs (builtins.attrValues destinations)
295295+ );
296296+297297+ files = mapAttrs' (n: srcCfg: let
298298+ fileText = attrsToFile (mkSrcAttrs srcCfg);
299299+ in {
300300+ name = srcCfg.dataset;
301301+ value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
302302+ }) cfg.zetup;
303303+7304in
8305{
9306 options = {
10307 services.znapzend = {
1111- enable = mkEnableOption "ZnapZend daemon";
308308+ enable = mkEnableOption "ZnapZend ZFS backup daemon";
1230913310 logLevel = mkOption {
14311 default = "debug";
15312 example = "warning";
1616- type = lib.types.enum ["debug" "info" "warning" "err" "alert"];
1717- description = "The log level when logging to file. Any of debug, info, warning, err, alert. Default in daemonized form is debug.";
313313+ type = enum ["debug" "info" "warning" "err" "alert"];
314314+ description = ''
315315+ The log level when logging to file. Any of debug, info, warning, err,
316316+ alert. Default in daemonized form is debug.
317317+ '';
18318 };
1931920320 logTo = mkOption {
2121- type = types.str;
321321+ type = str;
22322 default = "syslog::daemon";
23323 example = "/var/log/znapzend.log";
2424- description = "Where to log to (syslog::<facility> or <filepath>).";
324324+ description = ''
325325+ Where to log to (syslog::<facility> or <filepath>).
326326+ '';
25327 };
2632827329 noDestroy = mkOption {
2828- type = types.bool;
330330+ type = bool;
29331 default = false;
30332 description = "Does all changes to the filesystem except destroy.";
31333 };
3233433335 autoCreation = mkOption {
3434- type = types.bool;
336336+ type = bool;
35337 default = false;
3636- description = "Automatically create the dataset on dest if it does not exists.";
338338+ description = "Automatically create the destination dataset if it does not exists.";
339339+ };
340340+341341+ zetup = mkOption {
342342+ type = loaOf srcType;
343343+ description = "Znapzend configuration.";
344344+ default = {};
345345+ example = literalExample ''
346346+ {
347347+ "tank/home" = {
348348+ # Make snapshots of tank/home every hour, keep those for 1 day,
349349+ # keep every days snapshot for 1 month, etc.
350350+ plan = "1d=>1h,1m=>1d,1y=>1m";
351351+ recursive = true;
352352+ # Send all those snapshots to john@example.com:rtank/john as well
353353+ destinations.remote = {
354354+ host = "john@example.com";
355355+ dataset = "rtank/john";
356356+ };
357357+ };
358358+ };
359359+ '';
360360+ };
361361+362362+ pure = mkOption {
363363+ type = bool;
364364+ description = ''
365365+ Do not persist any stateful znapzend setups. If this option is
366366+ enabled, your previously set znapzend setups will be cleared and only
367367+ the ones defined with this module will be applied.
368368+ '';
369369+ default = false;
37370 };
38371 };
39372 };
···4938250383 path = with pkgs; [ zfs mbuffer openssh ];
51384385385+ preStart = optionalString cfg.pure ''
386386+ echo Resetting znapzend zetups
387387+ ${pkgs.znapzend}/bin/znapzendzetup list \
388388+ | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
389389+ | xargs ${pkgs.znapzend}/bin/znapzendzetup delete
390390+ '' + concatStringsSep "\n" (mapAttrsToList (dataset: config: ''
391391+ echo Importing znapzend zetup ${config} for dataset ${dataset}
392392+ ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config}
393393+ '') files);
394394+52395 serviceConfig = {
5353- ExecStart = "${pkgs.znapzend}/bin/znapzend --logto=${cfg.logTo} --loglevel=${cfg.logLevel} ${optionalString cfg.noDestroy "--nodestroy"} ${optionalString cfg.autoCreation "--autoCreation"}";
396396+ ExecStart = let
397397+ args = concatStringsSep " " [
398398+ "--logto=${cfg.logTo}"
399399+ "--loglevel=${cfg.logLevel}"
400400+ (optionalString cfg.noDestroy "--nodestroy")
401401+ (optionalString cfg.autoCreation "--autoCreation")
402402+ ]; in "${pkgs.znapzend}/bin/znapzend ${args}";
54403 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
55404 Restart = "on-failure";
56405 };
57406 };
58407 };
59408 };
409409+410410+ meta.maintainers = with maintainers; [ infinisil ];
60411}