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