1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.tarsnap;
7
8 configFile = cfg: ''
9 cachedir ${config.services.tarsnap.cachedir}
10 keyfile ${config.services.tarsnap.keyfile}
11 ${optionalString cfg.nodump "nodump"}
12 ${optionalString cfg.printStats "print-stats"}
13 ${optionalString cfg.printStats "humanize-numbers"}
14 ${optionalString (cfg.checkpointBytes != null) ("checkpoint-bytes "+cfg.checkpointBytes)}
15 ${optionalString cfg.aggressiveNetworking "aggressive-networking"}
16 ${concatStringsSep "\n" (map (v: "exclude "+v) cfg.excludes)}
17 ${concatStringsSep "\n" (map (v: "include "+v) cfg.includes)}
18 ${optionalString cfg.lowmem "lowmem"}
19 ${optionalString cfg.verylowmem "verylowmem"}
20 ${optionalString (cfg.maxbw != null) ("maxbw "+toString cfg.maxbw)}
21 ${optionalString (cfg.maxbwRateUp != null) ("maxbw-rate-up "+toString cfg.maxbwRateUp)}
22 ${optionalString (cfg.maxbwRateDown != null) ("maxbw-rate-down "+toString cfg.maxbwRateDown)}
23 '';
24in
25{
26 options = {
27 services.tarsnap = {
28 enable = mkOption {
29 type = types.bool;
30 default = false;
31 description = ''
32 Enable periodic tarsnap backups.
33 '';
34 };
35
36 keyfile = mkOption {
37 type = types.str;
38 default = "/root/tarsnap.key";
39 description = ''
40 The keyfile which associates this machine with your tarsnap
41 account.
42 Create the keyfile with <command>tarsnap-keygen</command>.
43
44 The keyfile name should be given as a string and not a path, to
45 avoid the key being copied into the Nix store.
46 '';
47 };
48
49 cachedir = mkOption {
50 type = types.nullOr types.path;
51 default = "/var/cache/tarsnap";
52 description = ''
53 The cache allows tarsnap to identify previously stored data
54 blocks, reducing archival time and bandwidth usage.
55
56 Should the cache become desynchronized or corrupted, tarsnap
57 will refuse to run until you manually rebuild the cache with
58 <command>tarsnap --fsck</command>.
59
60 Set to <literal>null</literal> to disable caching.
61 '';
62 };
63
64 archives = mkOption {
65 type = types.attrsOf (types.submodule (
66 {
67 options = {
68 nodump = mkOption {
69 type = types.bool;
70 default = true;
71 description = ''
72 Exclude files with the <literal>nodump</literal> flag.
73 '';
74 };
75
76 printStats = mkOption {
77 type = types.bool;
78 default = true;
79 description = ''
80 Print global archive statistics upon completion.
81 The output is available via
82 <command>systemctl status tarsnap@archive-name</command>.
83 '';
84 };
85
86 checkpointBytes = mkOption {
87 type = types.nullOr types.str;
88 default = "1GB";
89 description = ''
90 Create a checkpoint every <literal>checkpointBytes</literal>
91 of uploaded data (optionally specified using an SI prefix).
92
93 1GB is the minimum value. A higher value is recommended,
94 as checkpointing is expensive.
95
96 Set to <literal>null</literal> to disable checkpointing.
97 '';
98 };
99
100 period = mkOption {
101 type = types.str;
102 default = "01:15";
103 example = "hourly";
104 description = ''
105 Create archive at this interval.
106
107 The format is described in
108 <citerefentry><refentrytitle>systemd.time</refentrytitle>
109 <manvolnum>7</manvolnum></citerefentry>.
110 '';
111 };
112
113 aggressiveNetworking = mkOption {
114 type = types.bool;
115 default = false;
116 description = ''
117 Upload data over multiple TCP connections, potentially
118 increasing tarsnap's bandwidth utilisation at the cost
119 of slowing down all other network traffic. Not
120 recommended unless TCP congestion is the dominant
121 limiting factor.
122 '';
123 };
124
125 directories = mkOption {
126 type = types.listOf types.path;
127 default = [];
128 description = "List of filesystem paths to archive.";
129 };
130
131 excludes = mkOption {
132 type = types.listOf types.str;
133 default = [];
134 description = ''
135 Exclude files and directories matching these patterns.
136 '';
137 };
138
139 includes = mkOption {
140 type = types.listOf types.str;
141 default = [];
142 description = ''
143 Include only files and directories matching these
144 patterns (the empty list includes everything).
145
146 Exclusions have precedence over inclusions.
147 '';
148 };
149
150 lowmem = mkOption {
151 type = types.bool;
152 default = false;
153 description = ''
154 Reduce memory consumption by not caching small files.
155 Possibly beneficial if the average file size is smaller
156 than 1 MB and the number of files is lower than the
157 total amount of RAM in KB.
158 '';
159 };
160
161 verylowmem = mkOption {
162 type = types.bool;
163 default = false;
164 description = ''
165 Reduce memory consumption by a factor of 2 beyond what
166 <literal>lowmem</literal> does, at the cost of significantly
167 slowing down the archiving process.
168 '';
169 };
170
171 maxbw = mkOption {
172 type = types.nullOr types.int;
173 default = null;
174 description = ''
175 Abort archival if upstream bandwidth usage in bytes
176 exceeds this threshold.
177 '';
178 };
179
180 maxbwRateUp = mkOption {
181 type = types.nullOr types.int;
182 default = null;
183 example = literalExample "25 * 1000";
184 description = ''
185 Upload bandwidth rate limit in bytes.
186 '';
187 };
188
189 maxbwRateDown = mkOption {
190 type = types.nullOr types.int;
191 default = null;
192 example = literalExample "50 * 1000";
193 description = ''
194 Download bandwidth rate limit in bytes.
195 '';
196 };
197 };
198 }
199 ));
200
201 default = {};
202
203 example = literalExample ''
204 {
205 nixos =
206 { directories = [ "/home" "/root/ssl" ];
207 };
208
209 gamedata =
210 { directories = [ "/var/lib/minecraft "];
211 period = "*:30";
212 };
213 }
214 '';
215
216 description = ''
217 Tarsnap archive configurations. Each attribute names an archive
218 to be created at a given time interval, according to the options
219 associated with it. When uploading to the tarsnap server,
220 archive names are suffixed by a 1 second resolution timestamp.
221
222 For each member of the set is created a timer which triggers the
223 instanced <literal>tarsnap@</literal> service unit. You may use
224 <command>systemctl start tarsnap@archive-name</command> to
225 manually trigger creation of <literal>archive-name</literal> at
226 any time.
227 '';
228 };
229 };
230 };
231
232 config = mkIf cfg.enable {
233 assertions =
234 (mapAttrsToList (name: cfg:
235 { assertion = cfg.directories != [];
236 message = "Must specify paths for tarsnap to back up";
237 }) cfg.archives) ++
238 (mapAttrsToList (name: cfg:
239 { assertion = !(cfg.lowmem && cfg.verylowmem);
240 message = "You cannot set both lowmem and verylowmem";
241 }) cfg.archives);
242
243 systemd.services."tarsnap@" = {
244 description = "Tarsnap archive '%i'";
245 requires = [ "network.target" ];
246
247 path = [ pkgs.tarsnap pkgs.coreutils ];
248 scriptArgs = "%i";
249 script = ''
250 mkdir -p -m 0755 ${dirOf cfg.cachedir}
251 mkdir -p -m 0700 ${cfg.cachedir}
252 chown root:root ${cfg.cachedir}
253 chmod 0700 ${cfg.cachedir}
254 DIRS=`cat /etc/tarsnap/$1.dirs`
255 exec tarsnap --configfile /etc/tarsnap/$1.conf -c -f $1-$(date +"%Y%m%d%H%M%S") $DIRS
256 '';
257
258 serviceConfig = {
259 IOSchedulingClass = "idle";
260 NoNewPrivileges = "true";
261 CapabilityBoundingSet = "CAP_DAC_READ_SEARCH";
262 };
263 };
264
265 systemd.timers = mapAttrs' (name: cfg: nameValuePair "tarsnap@${name}"
266 { timerConfig.OnCalendar = cfg.period;
267 wantedBy = [ "timers.target" ];
268 }) cfg.archives;
269
270 environment.etc =
271 (mapAttrs' (name: cfg: nameValuePair "tarsnap/${name}.conf"
272 { text = configFile cfg;
273 }) cfg.archives) //
274 (mapAttrs' (name: cfg: nameValuePair "tarsnap/${name}.dirs"
275 { text = concatStringsSep " " cfg.directories;
276 }) cfg.archives);
277
278 environment.systemPackages = [ pkgs.tarsnap ];
279 };
280}