vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / backup / tarsnap.nix
blob43493997931c16106494871eaaefd44b5a3e83d6
1 { config, lib, options, pkgs, utils, ... }:
2 let
3   gcfg = config.services.tarsnap;
4   opt = options.services.tarsnap;
6   configFile = name: cfg: ''
7     keyfile ${cfg.keyfile}
8     ${lib.optionalString (cfg.cachedir != null) "cachedir ${cfg.cachedir}"}
9     ${lib.optionalString cfg.nodump "nodump"}
10     ${lib.optionalString cfg.printStats "print-stats"}
11     ${lib.optionalString cfg.printStats "humanize-numbers"}
12     ${lib.optionalString (cfg.checkpointBytes != null) ("checkpoint-bytes "+cfg.checkpointBytes)}
13     ${lib.optionalString cfg.aggressiveNetworking "aggressive-networking"}
14     ${lib.concatStringsSep "\n" (map (v: "exclude ${v}") cfg.excludes)}
15     ${lib.concatStringsSep "\n" (map (v: "include ${v}") cfg.includes)}
16     ${lib.optionalString cfg.lowmem "lowmem"}
17     ${lib.optionalString cfg.verylowmem "verylowmem"}
18     ${lib.optionalString (cfg.maxbw != null) "maxbw ${toString cfg.maxbw}"}
19     ${lib.optionalString (cfg.maxbwRateUp != null) "maxbw-rate-up ${toString cfg.maxbwRateUp}"}
20     ${lib.optionalString (cfg.maxbwRateDown != null) "maxbw-rate-down ${toString cfg.maxbwRateDown}"}
21   '';
24   imports = [
25     (lib.mkRemovedOptionModule [ "services" "tarsnap" "cachedir" ] "Use services.tarsnap.archives.<name>.cachedir")
26   ];
28   options = {
29     services.tarsnap = {
30       enable = lib.mkEnableOption "periodic tarsnap backups";
32       package = lib.mkPackageOption pkgs "tarsnap" { };
34       keyfile = lib.mkOption {
35         type = lib.types.str;
36         default = "/root/tarsnap.key";
37         description = ''
38           The keyfile which associates this machine with your tarsnap
39           account.
40           Create the keyfile with {command}`tarsnap-keygen`.
42           Note that each individual archive (specified below) may also have its
43           own individual keyfile specified. Tarsnap does not allow multiple
44           concurrent backups with the same cache directory and key (starting a
45           new backup will cause another one to fail). If you have multiple
46           archives specified, you should either spread out your backups to be
47           far apart, or specify a separate key for each archive. By default
48           every archive defaults to using
49           `"/root/tarsnap.key"`.
51           It's recommended for backups that you generate a key for every archive
52           using `tarsnap-keygen(1)`, and then generate a
53           write-only tarsnap key using `tarsnap-keymgmt(1)`,
54           and keep your master key(s) for a particular machine off-site.
56           The keyfile name should be given as a string and not a path, to
57           avoid the key being copied into the Nix store.
58         '';
59       };
61       archives = lib.mkOption {
62         type = lib.types.attrsOf (lib.types.submodule ({ config, options, ... }:
63           {
64             options = {
65               keyfile = lib.mkOption {
66                 type = lib.types.str;
67                 default = gcfg.keyfile;
68                 defaultText = lib.literalExpression "config.${opt.keyfile}";
69                 description = ''
70                   Set a specific keyfile for this archive. This defaults to
71                   `"/root/tarsnap.key"` if left unspecified.
73                   Use this option if you want to run multiple backups
74                   concurrently - each archive must have a unique key. You can
75                   generate a write-only key derived from your master key (which
76                   is recommended) using `tarsnap-keymgmt(1)`.
78                   Note: every archive must have an individual master key. You
79                   must generate multiple keys with
80                   `tarsnap-keygen(1)`, and then generate write
81                   only keys from those.
83                   The keyfile name should be given as a string and not a path, to
84                   avoid the key being copied into the Nix store.
85                 '';
86               };
88               cachedir = lib.mkOption {
89                 type = lib.types.nullOr lib.types.path;
90                 default = "/var/cache/tarsnap/${utils.escapeSystemdPath config.keyfile}";
91                 defaultText = lib.literalExpression ''
92                   "/var/cache/tarsnap/''${utils.escapeSystemdPath config.${options.keyfile}}"
93                 '';
94                 description = ''
95                   The cache allows tarsnap to identify previously stored data
96                   blocks, reducing archival time and bandwidth usage.
98                   Should the cache become desynchronized or corrupted, tarsnap
99                   will refuse to run until you manually rebuild the cache with
100                   {command}`tarsnap --fsck`.
102                   Set to `null` to disable caching.
103                 '';
104               };
106               nodump = lib.mkOption {
107                 type = lib.types.bool;
108                 default = true;
109                 description = ''
110                   Exclude files with the `nodump` flag.
111                 '';
112               };
114               printStats = lib.mkOption {
115                 type = lib.types.bool;
116                 default = true;
117                 description = ''
118                   Print global archive statistics upon completion.
119                   The output is available via
120                   {command}`systemctl status tarsnap-archive-name`.
121                 '';
122               };
124               checkpointBytes = lib.mkOption {
125                 type = lib.types.nullOr lib.types.str;
126                 default = "1GB";
127                 description = ''
128                   Create a checkpoint every `checkpointBytes`
129                   of uploaded data (optionally specified using an SI prefix).
131                   1GB is the minimum value. A higher value is recommended,
132                   as checkpointing is expensive.
134                   Set to `null` to disable checkpointing.
135                 '';
136               };
138               period = lib.mkOption {
139                 type = lib.types.str;
140                 default = "01:15";
141                 example = "hourly";
142                 description = ''
143                   Create archive at this interval.
145                   The format is described in
146                   {manpage}`systemd.time(7)`.
147                 '';
148               };
150               aggressiveNetworking = lib.mkOption {
151                 type = lib.types.bool;
152                 default = false;
153                 description = ''
154                   Upload data over multiple TCP connections, potentially
155                   increasing tarsnap's bandwidth utilisation at the cost
156                   of slowing down all other network traffic. Not
157                   recommended unless TCP congestion is the dominant
158                   limiting factor.
159                 '';
160               };
162               directories = lib.mkOption {
163                 type = lib.types.listOf lib.types.path;
164                 default = [];
165                 description = "List of filesystem paths to archive.";
166               };
168               excludes = lib.mkOption {
169                 type = lib.types.listOf lib.types.str;
170                 default = [];
171                 description = ''
172                   Exclude files and directories matching these patterns.
173                 '';
174               };
176               includes = lib.mkOption {
177                 type = lib.types.listOf lib.types.str;
178                 default = [];
179                 description = ''
180                   Include only files and directories matching these
181                   patterns (the empty list includes everything).
183                   Exclusions have precedence over inclusions.
184                 '';
185               };
187               lowmem = lib.mkOption {
188                 type = lib.types.bool;
189                 default = false;
190                 description = ''
191                   Reduce memory consumption by not caching small files.
192                   Possibly beneficial if the average file size is smaller
193                   than 1 MB and the number of files is lower than the
194                   total amount of RAM in KB.
195                 '';
196               };
198               verylowmem = lib.mkOption {
199                 type = lib.types.bool;
200                 default = false;
201                 description = ''
202                   Reduce memory consumption by a factor of 2 beyond what
203                   `lowmem` does, at the cost of significantly
204                   slowing down the archiving process.
205                 '';
206               };
208               maxbw = lib.mkOption {
209                 type = lib.types.nullOr lib.types.int;
210                 default = null;
211                 description = ''
212                   Abort archival if upstream bandwidth usage in bytes
213                   exceeds this threshold.
214                 '';
215               };
217               maxbwRateUp = lib.mkOption {
218                 type = lib.types.nullOr lib.types.int;
219                 default = null;
220                 example = lib.literalExpression "25 * 1000";
221                 description = ''
222                   Upload bandwidth rate limit in bytes.
223                 '';
224               };
226               maxbwRateDown = lib.mkOption {
227                 type = lib.types.nullOr lib.types.int;
228                 default = null;
229                 example = lib.literalExpression "50 * 1000";
230                 description = ''
231                   Download bandwidth rate limit in bytes.
232                 '';
233               };
235               verbose = lib.mkOption {
236                 type = lib.types.bool;
237                 default = false;
238                 description = ''
239                   Whether to produce verbose logging output.
240                 '';
241               };
242               explicitSymlinks = lib.mkOption {
243                 type = lib.types.bool;
244                 default = false;
245                 description = ''
246                   Whether to follow symlinks specified as archives.
247                 '';
248               };
249               followSymlinks = lib.mkOption {
250                 type = lib.types.bool;
251                 default = false;
252                 description = ''
253                   Whether to follow all symlinks in archive trees.
254                 '';
255               };
256             };
257           }
258         ));
260         default = {};
262         example = lib.literalExpression ''
263           {
264             nixos =
265               { directories = [ "/home" "/root/ssl" ];
266               };
268             gamedata =
269               { directories = [ "/var/lib/minecraft" ];
270                 period      = "*:30";
271               };
272           }
273         '';
275         description = ''
276           Tarsnap archive configurations. Each attribute names an archive
277           to be created at a given time interval, according to the options
278           associated with it. When uploading to the tarsnap server,
279           archive names are suffixed by a 1 second resolution timestamp,
280           with the format `%Y%m%d%H%M%S`.
282           For each member of the set is created a timer which triggers the
283           instanced `tarsnap-archive-name` service unit. You may use
284           {command}`systemctl start tarsnap-archive-name` to
285           manually trigger creation of `archive-name` at
286           any time.
287         '';
288       };
289     };
290   };
292   config = lib.mkIf gcfg.enable {
293     assertions =
294       (lib.mapAttrsToList (name: cfg:
295         { assertion = cfg.directories != [];
296           message = "Must specify paths for tarsnap to back up";
297         }) gcfg.archives) ++
298       (lib.mapAttrsToList (name: cfg:
299         { assertion = !(cfg.lowmem && cfg.verylowmem);
300           message = "You cannot set both lowmem and verylowmem";
301         }) gcfg.archives);
303     systemd.services =
304       (lib.mapAttrs' (name: cfg: lib.nameValuePair "tarsnap-${name}" {
305         description = "Tarsnap archive '${name}'";
306         requires    = [ "network-online.target" ];
307         after       = [ "network-online.target" ];
309         path = with pkgs; [ iputils gcfg.package util-linux ];
311         # In order for the persistent tarsnap timer to work reliably, we have to
312         # make sure that the tarsnap server is reachable after systemd starts up
313         # the service - therefore we sleep in a loop until we can ping the
314         # endpoint.
315         preStart = ''
316           while ! ping -4 -q -c 1 v1-0-0-server.tarsnap.com &> /dev/null; do sleep 3; done
317         '';
319         script = let
320           tarsnap = ''${lib.getExe gcfg.package} --configfile "/etc/tarsnap/${name}.conf"'';
321           run = ''${tarsnap} -c -f "${name}-$(date +"%Y%m%d%H%M%S")" \
322                         ${lib.optionalString cfg.verbose "-v"} \
323                         ${lib.optionalString cfg.explicitSymlinks "-H"} \
324                         ${lib.optionalString cfg.followSymlinks "-L"} \
325                         ${lib.concatStringsSep " " cfg.directories}'';
326           cachedir = lib.escapeShellArg cfg.cachedir;
327           in if (cfg.cachedir != null) then ''
328             mkdir -p ${cachedir}
329             chmod 0700 ${cachedir}
331             ( flock 9
332               if [ ! -e ${cachedir}/firstrun ]; then
333                 ( flock 10
334                   flock -u 9
335                   ${tarsnap} --fsck
336                   flock 9
337                 ) 10>${cachedir}/firstrun
338               fi
339             ) 9>${cachedir}/lockf
341              exec flock ${cachedir}/firstrun ${run}
342           '' else "exec ${run}";
344         serviceConfig = {
345           Type = "oneshot";
346           IOSchedulingClass = "idle";
347           NoNewPrivileges = "true";
348           CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
349           PermissionsStartOnly = "true";
350         };
351       }) gcfg.archives) //
353       (lib.mapAttrs' (name: cfg: lib.nameValuePair "tarsnap-restore-${name}"{
354         description = "Tarsnap restore '${name}'";
355         requires    = [ "network-online.target" ];
357         path = with pkgs; [ iputils gcfg.package util-linux ];
359         script = let
360           tarsnap = ''${lib.getExe gcfg.package} --configfile "/etc/tarsnap/${name}.conf"'';
361           lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
362           run = ''${tarsnap} -x -f "${lastArchive}" ${lib.optionalString cfg.verbose "-v"}'';
363           cachedir = lib.escapeShellArg cfg.cachedir;
365         in if (cfg.cachedir != null) then ''
366           mkdir -p ${cachedir}
367           chmod 0700 ${cachedir}
369           ( flock 9
370             if [ ! -e ${cachedir}/firstrun ]; then
371               ( flock 10
372                 flock -u 9
373                 ${tarsnap} --fsck
374                 flock 9
375               ) 10>${cachedir}/firstrun
376             fi
377           ) 9>${cachedir}/lockf
379            exec flock ${cachedir}/firstrun ${run}
380         '' else "exec ${run}";
382         serviceConfig = {
383           Type = "oneshot";
384           IOSchedulingClass = "idle";
385           NoNewPrivileges = "true";
386           CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
387           PermissionsStartOnly = "true";
388         };
389       }) gcfg.archives);
391     # Note: the timer must be Persistent=true, so that systemd will start it even
392     # if e.g. your laptop was asleep while the latest interval occurred.
393     systemd.timers = lib.mapAttrs' (name: cfg: lib.nameValuePair "tarsnap-${name}"
394       { timerConfig.OnCalendar = cfg.period;
395         timerConfig.Persistent = "true";
396         wantedBy = [ "timers.target" ];
397       }) gcfg.archives;
399     environment.etc =
400       lib.mapAttrs' (name: cfg: lib.nameValuePair "tarsnap/${name}.conf"
401         { text = configFile name cfg;
402         }) gcfg.archives;
404     environment.systemPackages = [ gcfg.package ];
405   };