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