at 25.11-pre 17 kB view raw
1{ 2 config, 3 lib, 4 options, 5 pkgs, 6 utils, 7 ... 8}: 9let 10 gcfg = config.services.tarsnap; 11 opt = options.services.tarsnap; 12 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 ''; 29in 30{ 31 imports = [ 32 (lib.mkRemovedOptionModule [ 33 "services" 34 "tarsnap" 35 "cachedir" 36 ] "Use services.tarsnap.archives.<name>.cachedir") 37 ]; 38 39 options = { 40 services.tarsnap = { 41 enable = lib.mkEnableOption "periodic tarsnap backups"; 42 43 package = lib.mkPackageOption pkgs "tarsnap" { }; 44 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`. 52 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"`. 61 62 It's recommended for backups that you generate a key for every archive 63 using {manpage}`tarsnap-keygen(1)`, and then generate a 64 write-only tarsnap key using {manpage}`tarsnap-keymgmt(1)`, 65 and keep your master key(s) for a particular machine off-site. 66 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 }; 71 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. 85 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 {manpage}`tarsnap-keymgmt(1)`. 90 91 Note: every archive must have an individual master key. You 92 must generate multiple keys with 93 {manpage}`tarsnap-keygen(1)`, and then generate write 94 only keys from those. 95 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 }; 100 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. 110 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`. 114 115 Set to `null` to disable caching. 116 ''; 117 }; 118 119 nodump = lib.mkOption { 120 type = lib.types.bool; 121 default = true; 122 description = '' 123 Exclude files with the `nodump` flag. 124 ''; 125 }; 126 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 }; 136 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). 143 144 1GB is the minimum value. A higher value is recommended, 145 as checkpointing is expensive. 146 147 Set to `null` to disable checkpointing. 148 ''; 149 }; 150 151 period = lib.mkOption { 152 type = lib.types.str; 153 default = "01:15"; 154 example = "hourly"; 155 description = '' 156 Create archive at this interval. 157 158 The format is described in 159 {manpage}`systemd.time(7)`. 160 ''; 161 }; 162 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 }; 174 175 directories = lib.mkOption { 176 type = lib.types.listOf lib.types.path; 177 default = [ ]; 178 description = "List of filesystem paths to archive."; 179 }; 180 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 }; 188 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). 195 196 Exclusions have precedence over inclusions. 197 ''; 198 }; 199 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 }; 210 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 }; 220 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 }; 229 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 }; 238 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 }; 247 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 ); 273 274 default = { }; 275 276 example = lib.literalExpression '' 277 { 278 nixos = 279 { directories = [ "/home" "/root/ssl" ]; 280 }; 281 282 gamedata = 283 { directories = [ "/var/lib/minecraft" ]; 284 period = "*:30"; 285 }; 286 } 287 ''; 288 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`. 295 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 }; 305 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); 316 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" ]; 324 325 path = with pkgs; [ 326 iputils 327 gcfg.package 328 util-linux 329 ]; 330 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 ''; 338 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} 354 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 364 365 exec flock ${cachedir}/firstrun ${run} 366 '' 367 else 368 "exec ${run}"; 369 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 // 380 381 (lib.mapAttrs' ( 382 name: cfg: 383 lib.nameValuePair "tarsnap-restore-${name}" { 384 description = "Tarsnap restore '${name}'"; 385 requires = [ "network-online.target" ]; 386 387 path = with pkgs; [ 388 iputils 389 gcfg.package 390 util-linux 391 ]; 392 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; 399 400 in 401 if (cfg.cachedir != null) then 402 '' 403 mkdir -p ${cachedir} 404 chmod 0700 ${cachedir} 405 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 415 416 exec flock ${cachedir}/firstrun ${run} 417 '' 418 else 419 "exec ${run}"; 420 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); 430 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; 441 442 environment.etc = lib.mapAttrs' ( 443 name: cfg: 444 lib.nameValuePair "tarsnap/${name}.conf" { 445 text = configFile name cfg; 446 } 447 ) gcfg.archives; 448 449 environment.systemPackages = [ gcfg.package ]; 450 }; 451}