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