1 { config, lib, options, pkgs, utils, ... }:
3 gcfg = config.services.tarsnap;
4 opt = options.services.tarsnap;
6 configFile = name: cfg: ''
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}"}
25 (lib.mkRemovedOptionModule [ "services" "tarsnap" "cachedir" ] "Use services.tarsnap.archives.<name>.cachedir")
30 enable = lib.mkEnableOption "periodic tarsnap backups";
32 package = lib.mkPackageOption pkgs "tarsnap" { };
34 keyfile = lib.mkOption {
36 default = "/root/tarsnap.key";
38 The keyfile which associates this machine with your tarsnap
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.
61 archives = lib.mkOption {
62 type = lib.types.attrsOf (lib.types.submodule ({ config, options, ... }:
65 keyfile = lib.mkOption {
67 default = gcfg.keyfile;
68 defaultText = lib.literalExpression "config.${opt.keyfile}";
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
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.
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}}"
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.
106 nodump = lib.mkOption {
107 type = lib.types.bool;
110 Exclude files with the `nodump` flag.
114 printStats = lib.mkOption {
115 type = lib.types.bool;
118 Print global archive statistics upon completion.
119 The output is available via
120 {command}`systemctl status tarsnap-archive-name`.
124 checkpointBytes = lib.mkOption {
125 type = lib.types.nullOr lib.types.str;
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.
138 period = lib.mkOption {
139 type = lib.types.str;
143 Create archive at this interval.
145 The format is described in
146 {manpage}`systemd.time(7)`.
150 aggressiveNetworking = lib.mkOption {
151 type = lib.types.bool;
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
162 directories = lib.mkOption {
163 type = lib.types.listOf lib.types.path;
165 description = "List of filesystem paths to archive.";
168 excludes = lib.mkOption {
169 type = lib.types.listOf lib.types.str;
172 Exclude files and directories matching these patterns.
176 includes = lib.mkOption {
177 type = lib.types.listOf lib.types.str;
180 Include only files and directories matching these
181 patterns (the empty list includes everything).
183 Exclusions have precedence over inclusions.
187 lowmem = lib.mkOption {
188 type = lib.types.bool;
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.
198 verylowmem = lib.mkOption {
199 type = lib.types.bool;
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.
208 maxbw = lib.mkOption {
209 type = lib.types.nullOr lib.types.int;
212 Abort archival if upstream bandwidth usage in bytes
213 exceeds this threshold.
217 maxbwRateUp = lib.mkOption {
218 type = lib.types.nullOr lib.types.int;
220 example = lib.literalExpression "25 * 1000";
222 Upload bandwidth rate limit in bytes.
226 maxbwRateDown = lib.mkOption {
227 type = lib.types.nullOr lib.types.int;
229 example = lib.literalExpression "50 * 1000";
231 Download bandwidth rate limit in bytes.
235 verbose = lib.mkOption {
236 type = lib.types.bool;
239 Whether to produce verbose logging output.
242 explicitSymlinks = lib.mkOption {
243 type = lib.types.bool;
246 Whether to follow symlinks specified as archives.
249 followSymlinks = lib.mkOption {
250 type = lib.types.bool;
253 Whether to follow all symlinks in archive trees.
262 example = lib.literalExpression ''
265 { directories = [ "/home" "/root/ssl" ];
269 { directories = [ "/var/lib/minecraft" ];
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
292 config = lib.mkIf gcfg.enable {
294 (lib.mapAttrsToList (name: cfg:
295 { assertion = cfg.directories != [];
296 message = "Must specify paths for tarsnap to back up";
298 (lib.mapAttrsToList (name: cfg:
299 { assertion = !(cfg.lowmem && cfg.verylowmem);
300 message = "You cannot set both lowmem and verylowmem";
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
316 while ! ping -4 -q -c 1 v1-0-0-server.tarsnap.com &> /dev/null; do sleep 3; done
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 ''
329 chmod 0700 ${cachedir}
332 if [ ! -e ${cachedir}/firstrun ]; then
337 ) 10>${cachedir}/firstrun
339 ) 9>${cachedir}/lockf
341 exec flock ${cachedir}/firstrun ${run}
342 '' else "exec ${run}";
346 IOSchedulingClass = "idle";
347 NoNewPrivileges = "true";
348 CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
349 PermissionsStartOnly = "true";
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 ];
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 ''
367 chmod 0700 ${cachedir}
370 if [ ! -e ${cachedir}/firstrun ]; then
375 ) 10>${cachedir}/firstrun
377 ) 9>${cachedir}/lockf
379 exec flock ${cachedir}/firstrun ${run}
380 '' else "exec ${run}";
384 IOSchedulingClass = "idle";
385 NoNewPrivileges = "true";
386 CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
387 PermissionsStartOnly = "true";
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" ];
400 lib.mapAttrs' (name: cfg: lib.nameValuePair "tarsnap/${name}.conf"
401 { text = configFile name cfg;
404 environment.systemPackages = [ gcfg.package ];