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