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