1{ config, lib, pkgs, ... }:
2
3with lib;
4with types;
5
6let
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";
309
310 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 };
319
320 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 };
328
329 noDestroy = mkOption {
330 type = bool;
331 default = false;
332 description = "Does all changes to the filesystem except destroy.";
333 };
334
335 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 };
373
374 config = mkIf cfg.enable {
375 environment.systemPackages = [ pkgs.znapzend ];
376
377 systemd.services = {
378 "znapzend" = {
379 description = "ZnapZend - ZFS Backup System";
380 wantedBy = [ "zfs.target" ];
381 after = [ "zfs.target" ];
382
383 path = with pkgs; [ zfs mbuffer openssh ];
384
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}
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}