1{ config, lib, pkgs, ... }:
2
3with lib;
4with types;
5
6let
7
8 planDescription = ''
9 The znapzend backup plan to use for the source.
10 </para>
11 <para>
12 The plan specifies how often to backup and for how long to keep the
13 backups. It consists of a series of retention periodes to interval
14 associations:
15 </para>
16 <para>
17 <literal>
18 retA=>intA,retB=>intB,...
19 </literal>
20 </para>
21 <para>
22 Both intervals and retention periods are expressed in standard units
23 of time or multiples of them. You can use both the full name or a
24 shortcut according to the following listing:
25 </para>
26 <para>
27 <literal>
28 second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
29 </literal>
30 </para>
31 <para>
32 See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
33 '';
34 planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
35
36 # A type for a string of the form number{b|k|M|G}
37 mbufferSizeType = str // {
38 check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
39 description = "string of the form number{b|k|M|G}";
40 };
41
42 # Type for a string that must contain certain other strings (the list parameter).
43 # Note that these would need regex escaping.
44 stringContainingStrings = list: let
45 matching = s: map (str: builtins.match ".*${str}.*" s) list;
46 in str // {
47 check = x: str.check x && all isList (matching x);
48 description = "string containing all of the characters ${concatStringsSep ", " list}";
49 };
50
51 timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
52
53 destType = srcConfig: submodule ({ name, ... }: {
54 options = {
55
56 label = mkOption {
57 type = str;
58 description = "Label for this destination. Defaults to the attribute name.";
59 };
60
61 plan = mkOption {
62 type = str;
63 description = planDescription;
64 example = planExample;
65 };
66
67 dataset = mkOption {
68 type = str;
69 description = "Dataset name to send snapshots to.";
70 example = "tank/main";
71 };
72
73 host = mkOption {
74 type = nullOr str;
75 description = ''
76 Host to use for the destination dataset. Can be prefixed with
77 <literal>user@</literal> to specify the ssh user.
78 '';
79 default = null;
80 example = "john@example.com";
81 };
82
83 presend = mkOption {
84 type = nullOr str;
85 description = ''
86 Command to run before sending the snapshot to the destination.
87 Intended to run a remote script via <command>ssh</command> on the
88 destination, e.g. to bring up a backup disk or server or to put a
89 zpool online/offline. See also <option>postsend</option>.
90 '';
91 default = null;
92 example = "ssh root@bserv zpool import -Nf tank";
93 };
94
95 postsend = mkOption {
96 type = nullOr str;
97 description = ''
98 Command to run after sending the snapshot to the destination.
99 Intended to run a remote script via <command>ssh</command> on the
100 destination, e.g. to bring up a backup disk or server or to put a
101 zpool online/offline. See also <option>presend</option>.
102 '';
103 default = null;
104 example = "ssh root@bserv zpool export tank";
105 };
106 };
107
108 config = {
109 label = mkDefault name;
110 plan = mkDefault srcConfig.plan;
111 };
112 });
113
114
115
116 srcType = submodule ({ name, config, ... }: {
117 options = {
118
119 enable = mkOption {
120 type = bool;
121 description = "Whether to enable this source.";
122 default = true;
123 };
124
125 recursive = mkOption {
126 type = bool;
127 description = "Whether to do recursive snapshots.";
128 default = false;
129 };
130
131 mbuffer = {
132 enable = mkOption {
133 type = bool;
134 description = "Whether to use <command>mbuffer</command>.";
135 default = false;
136 };
137
138 port = mkOption {
139 type = nullOr ints.u16;
140 description = ''
141 Port to use for <command>mbuffer</command>.
142 </para>
143 <para>
144 If this is null, it will run <command>mbuffer</command> through
145 ssh.
146 </para>
147 <para>
148 If this is not null, it will run <command>mbuffer</command>
149 directly through TCP, which is not encrypted but faster. In that
150 case the given port needs to be open on the destination host.
151 '';
152 default = null;
153 };
154
155 size = mkOption {
156 type = mbufferSizeType;
157 description = ''
158 The size for <command>mbuffer</command>.
159 Supports the units b, k, M, G.
160 '';
161 default = "1G";
162 example = "128M";
163 };
164 };
165
166 presnap = mkOption {
167 type = nullOr str;
168 description = ''
169 Command to run before snapshots are taken on the source dataset,
170 e.g. for database locking/flushing. See also
171 <option>postsnap</option>.
172 '';
173 default = null;
174 example = literalExample ''
175 ''${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
176 '';
177 };
178
179 postsnap = mkOption {
180 type = nullOr str;
181 description = ''
182 Command to run after snapshots are taken on the source dataset,
183 e.g. for database unlocking. See also <option>presnap</option>.
184 '';
185 default = null;
186 example = literalExample ''
187 ''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid
188 '';
189 };
190
191 timestampFormat = mkOption {
192 type = timestampType;
193 description = ''
194 The timestamp format to use for constructing snapshot names.
195 The syntax is <literal>strftime</literal>-like. The string must
196 consist of the mandatory <literal>%Y %m %d %H %M %S</literal>.
197 Optionally <literal>- _ . :</literal> characters as well as any
198 alphanumeric character are allowed. If suffixed by a
199 <literal>Z</literal>, times will be in UTC.
200 '';
201 default = "%Y-%m-%d-%H%M%S";
202 example = "znapzend-%m.%d.%Y-%H%M%SZ";
203 };
204
205 sendDelay = mkOption {
206 type = int;
207 description = ''
208 Specify delay (in seconds) before sending snaps to the destination.
209 May be useful if you want to control sending time.
210 '';
211 default = 0;
212 example = 60;
213 };
214
215 plan = mkOption {
216 type = str;
217 description = planDescription;
218 example = planExample;
219 };
220
221 dataset = mkOption {
222 type = str;
223 description = "The dataset to use for this source.";
224 example = "tank/home";
225 };
226
227 destinations = mkOption {
228 type = loaOf (destType config);
229 description = "Additional destinations.";
230 default = {};
231 example = literalExample ''
232 {
233 local = {
234 dataset = "btank/backup";
235 presend = "zpool import -N btank";
236 postsend = "zpool export btank";
237 };
238 remote = {
239 host = "john@example.com";
240 dataset = "tank/john";
241 };
242 };
243 '';
244 };
245 };
246
247 config = {
248 dataset = mkDefault name;
249 };
250
251 });
252
253 ### Generating the configuration from here
254
255 cfg = config.services.znapzend;
256
257 onOff = b: if b then "on" else "off";
258 nullOff = b: if isNull b then "off" else toString b;
259 stripSlashes = replaceStrings [ "/" ] [ "." ];
260
261 attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
262 mapAttrs (n: v: "${n}=${v}") config));
263
264 mkDestAttrs = dst: with dst;
265 mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
266 "" = optionalString (! isNull host) "${host}:" + dataset;
267 _plan = plan;
268 } // optionalAttrs (presend != null) {
269 _precmd = presend;
270 } // optionalAttrs (postsend != null) {
271 _pstcmd = postsend;
272 });
273
274 mkSrcAttrs = srcCfg: with srcCfg; {
275 enabled = onOff enable;
276 mbuffer = with mbuffer; if enable then "${pkgs.mbuffer}/bin/mbuffer"
277 + optionalString (port != null) ":${toString port}" else "off";
278 mbuffer_size = mbuffer.size;
279 post_znap_cmd = nullOff postsnap;
280 pre_znap_cmd = nullOff presnap;
281 recursive = onOff recursive;
282 src = dataset;
283 src_plan = plan;
284 tsformat = timestampFormat;
285 zend_delay = toString sendDelay;
286 } // fold (a: b: a // b) {} (
287 map mkDestAttrs (builtins.attrValues destinations)
288 );
289
290 files = mapAttrs' (n: srcCfg: let
291 fileText = attrsToFile (mkSrcAttrs srcCfg);
292 in {
293 name = srcCfg.dataset;
294 value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
295 }) cfg.zetup;
296
297in
298{
299 options = {
300 services.znapzend = {
301 enable = mkEnableOption "ZnapZend ZFS backup daemon";
302
303 logLevel = mkOption {
304 default = "debug";
305 example = "warning";
306 type = enum ["debug" "info" "warning" "err" "alert"];
307 description = ''
308 The log level when logging to file. Any of debug, info, warning, err,
309 alert. Default in daemonized form is debug.
310 '';
311 };
312
313 logTo = mkOption {
314 type = str;
315 default = "syslog::daemon";
316 example = "/var/log/znapzend.log";
317 description = ''
318 Where to log to (syslog::<facility> or <filepath>).
319 '';
320 };
321
322 noDestroy = mkOption {
323 type = bool;
324 default = false;
325 description = "Does all changes to the filesystem except destroy.";
326 };
327
328 autoCreation = mkOption {
329 type = bool;
330 default = false;
331 description = "Automatically create the destination dataset if it does not exists.";
332 };
333
334 zetup = mkOption {
335 type = loaOf srcType;
336 description = "Znapzend configuration.";
337 default = {};
338 example = literalExample ''
339 {
340 "tank/home" = {
341 # Make snapshots of tank/home every hour, keep those for 1 day,
342 # keep every days snapshot for 1 month, etc.
343 plan = "1d=>1h,1m=>1d,1y=>1m";
344 recursive = true;
345 # Send all those snapshots to john@example.com:rtank/john as well
346 destinations.remote = {
347 host = "john@example.com";
348 dataset = "rtank/john";
349 };
350 };
351 };
352 '';
353 };
354
355 pure = mkOption {
356 type = bool;
357 description = ''
358 Do not persist any stateful znapzend setups. If this option is
359 enabled, your previously set znapzend setups will be cleared and only
360 the ones defined with this module will be applied.
361 '';
362 default = false;
363 };
364 };
365 };
366
367 config = mkIf cfg.enable {
368 environment.systemPackages = [ pkgs.znapzend ];
369
370 systemd.services = {
371 "znapzend" = {
372 description = "ZnapZend - ZFS Backup System";
373 wantedBy = [ "zfs.target" ];
374 after = [ "zfs.target" ];
375
376 path = with pkgs; [ zfs mbuffer openssh ];
377
378 preStart = optionalString cfg.pure ''
379 echo Resetting znapzend zetups
380 ${pkgs.znapzend}/bin/znapzendzetup list \
381 | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
382 | xargs -I{} ${pkgs.znapzend}/bin/znapzendzetup delete "{}"
383 '' + concatStringsSep "\n" (mapAttrsToList (dataset: config: ''
384 echo Importing znapzend zetup ${config} for dataset ${dataset}
385 ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config}
386 '') files);
387
388 serviceConfig = {
389 ExecStart = let
390 args = concatStringsSep " " [
391 "--logto=${cfg.logTo}"
392 "--loglevel=${cfg.logLevel}"
393 (optionalString cfg.noDestroy "--nodestroy")
394 (optionalString cfg.autoCreation "--autoCreation")
395 ]; in "${pkgs.znapzend}/bin/znapzend ${args}";
396 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
397 Restart = "on-failure";
398 };
399 };
400 };
401 };
402
403 meta.maintainers = with maintainers; [ infinisil ];
404}