at master 32 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 9 isLocalPath = 10 x: 11 builtins.substring 0 1 x == "/" # absolute path 12 || builtins.substring 0 1 x == "." # relative path 13 || builtins.match "[.*:.*]" == null; # not machine:path 14 15 mkExcludeFile = 16 cfg: 17 # Write each exclude pattern to a new line 18 pkgs.writeText "excludefile" (lib.concatMapStrings (s: s + "\n") cfg.exclude); 19 20 mkPatternsFile = 21 cfg: 22 # Write each pattern to a new line 23 pkgs.writeText "patternsfile" (lib.concatMapStrings (s: s + "\n") cfg.patterns); 24 25 mkKeepArgs = 26 cfg: 27 # If cfg.prune.keep e.g. has a yearly attribute, 28 # its content is passed on as --keep-yearly 29 lib.concatStringsSep " " (lib.mapAttrsToList (x: y: "--keep-${x}=${toString y}") cfg.prune.keep); 30 31 mkExtraArgs = 32 cfg: 33 # Create BASH arrays of extra args 34 lib.concatLines ( 35 lib.mapAttrsToList 36 (name: values: '' 37 ${name}=(${values}) 38 '') 39 { 40 inherit (cfg) 41 extraArgs 42 extraInitArgs 43 extraCreateArgs 44 extraPruneArgs 45 extraCompactArgs 46 ; 47 } 48 ); 49 50 mkBackupScript = 51 name: cfg: 52 pkgs.writeShellScript "${name}-script" ( 53 '' 54 set -e 55 56 ${mkExtraArgs cfg} 57 58 on_exit() 59 { 60 exitStatus=$? 61 ${cfg.postHook} 62 exit $exitStatus 63 } 64 trap on_exit EXIT 65 66 borgWrapper () { 67 local result 68 borg "$@" && result=$? || result=$? 69 if [[ -z "${toString cfg.failOnWarnings}" ]] && [[ "$result" == 1 || ("$result" -ge 100 && "$result" -le 127) ]]; then 70 echo "ignoring warning return value $result" 71 return 0 72 else 73 return "$result" 74 fi 75 } 76 77 archiveName="${ 78 lib.optionalString (cfg.archiveBaseName != null) (cfg.archiveBaseName + "-") 79 }$(date ${cfg.dateFormat})" 80 archiveSuffix="${lib.optionalString cfg.appendFailedSuffix ".failed"}" 81 ${cfg.preHook} 82 '' 83 + lib.optionalString cfg.doInit '' 84 # Run borg init if the repo doesn't exist yet 85 if ! borgWrapper list "''${extraArgs[@]}" > /dev/null; then 86 borgWrapper init "''${extraArgs[@]}" \ 87 --encryption ${cfg.encryption.mode} \ 88 "''${extraInitArgs[@]}" 89 ${cfg.postInit} 90 fi 91 '' 92 + '' 93 ( 94 set -o pipefail 95 ${lib.optionalString (cfg.dumpCommand != null) ''${lib.escapeShellArg cfg.dumpCommand} | \''} 96 borgWrapper create "''${extraArgs[@]}" \ 97 --compression ${cfg.compression} \ 98 --exclude-from ${mkExcludeFile cfg} \ 99 --patterns-from ${mkPatternsFile cfg} \ 100 "''${extraCreateArgs[@]}" \ 101 "::$archiveName$archiveSuffix" \ 102 ${if cfg.paths == null then "-" else lib.escapeShellArgs cfg.paths} 103 ) 104 '' 105 + lib.optionalString cfg.appendFailedSuffix '' 106 borgWrapper rename "''${extraArgs[@]}" \ 107 "::$archiveName$archiveSuffix" "$archiveName" 108 '' 109 + '' 110 ${cfg.postCreate} 111 '' 112 + lib.optionalString (cfg.prune.keep != { }) '' 113 borgWrapper prune "''${extraArgs[@]}" \ 114 ${mkKeepArgs cfg} \ 115 ${ 116 lib.optionalString ( 117 cfg.prune.prefix != null 118 ) "--glob-archives ${lib.escapeShellArg "${cfg.prune.prefix}*"}" 119 } \ 120 "''${extraPruneArgs[@]}" 121 borgWrapper compact "''${extraArgs[@]}" "''${extraCompactArgs[@]}" 122 ${cfg.postPrune} 123 '' 124 ); 125 126 mkPassEnv = 127 cfg: 128 with cfg.encryption; 129 if passCommand != null then 130 { BORG_PASSCOMMAND = passCommand; } 131 else if passphrase != null then 132 { BORG_PASSPHRASE = passphrase; } 133 else 134 { }; 135 136 mkBackupService = 137 name: cfg: 138 let 139 userHome = config.users.users.${cfg.user}.home; 140 backupJobName = "borgbackup-job-${name}"; 141 backupScript = mkBackupScript backupJobName cfg; 142 in 143 lib.nameValuePair backupJobName { 144 description = "BorgBackup job ${name}"; 145 path = [ 146 config.services.borgbackup.package 147 pkgs.openssh 148 ]; 149 script = 150 "exec " 151 + lib.optionalString cfg.inhibitsSleep '' 152 ${pkgs.systemd}/bin/systemd-inhibit \ 153 --who="borgbackup" \ 154 --what="sleep" \ 155 --why="Scheduled backup" \ 156 '' 157 + backupScript; 158 unitConfig = lib.optionalAttrs (isLocalPath cfg.repo) { 159 RequiresMountsFor = [ cfg.repo ]; 160 }; 161 serviceConfig = { 162 User = cfg.user; 163 Group = cfg.group; 164 # Only run when no other process is using CPU or disk 165 CPUSchedulingPolicy = "idle"; 166 IOSchedulingClass = "idle"; 167 ProtectSystem = "strict"; 168 ReadWritePaths = [ 169 "${userHome}/.config/borg" 170 "${userHome}/.cache/borg" 171 ] 172 ++ cfg.readWritePaths 173 # Borg needs write access to repo if it is not remote 174 ++ lib.optional (isLocalPath cfg.repo) cfg.repo; 175 PrivateTmp = cfg.privateTmp; 176 }; 177 environment = { 178 BORG_REPO = cfg.repo; 179 } 180 // (mkPassEnv cfg) 181 // cfg.environment; 182 }; 183 184 mkBackupTimers = 185 name: cfg: 186 lib.nameValuePair "borgbackup-job-${name}" { 187 description = "BorgBackup job ${name} timer"; 188 wantedBy = [ "timers.target" ]; 189 timerConfig = { 190 Persistent = cfg.persistentTimer; 191 OnCalendar = cfg.startAt; 192 }; 193 # if remote-backup wait for network 194 after = lib.optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target"; 195 wants = lib.optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target"; 196 }; 197 198 # utility function around makeWrapper 199 mkWrapperDrv = 200 { 201 original, 202 name, 203 set ? { }, 204 }: 205 pkgs.runCommand "${name}-wrapper" 206 { 207 nativeBuildInputs = [ pkgs.makeWrapper ]; 208 } 209 ( 210 with lib; 211 '' 212 makeWrapper "${original}" "$out/bin/${name}" \ 213 ${lib.concatStringsSep " \\\n " ( 214 lib.mapAttrsToList (name: value: ''--set ${name} "${value}"'') set 215 )} 216 '' 217 ); 218 219 # Returns a singleton list, due to usage of lib.optional 220 mkBorgWrapper = 221 name: cfg: 222 lib.optional (cfg.wrapper != "" && cfg.wrapper != null) (mkWrapperDrv { 223 original = lib.getExe config.services.borgbackup.package; 224 name = cfg.wrapper; 225 set = { 226 BORG_REPO = cfg.repo; 227 } 228 // (mkPassEnv cfg) 229 // cfg.environment; 230 }); 231 232 # Paths listed in ReadWritePaths must exist before service is started 233 mkTmpfiles = 234 name: cfg: 235 let 236 settings = { inherit (cfg) user group; }; 237 in 238 lib.nameValuePair "borgbackup-job-${name}" ( 239 { 240 # Create parent dirs separately, to ensure correct ownership. 241 "${config.users.users."${cfg.user}".home}/.config".d = settings; 242 "${config.users.users."${cfg.user}".home}/.cache".d = settings; 243 "${config.users.users."${cfg.user}".home}/.config/borg".d = settings; 244 "${config.users.users."${cfg.user}".home}/.cache/borg".d = settings; 245 } 246 // lib.optionalAttrs (isLocalPath cfg.repo && !cfg.removableDevice) { 247 "${cfg.repo}".d = settings; 248 } 249 ); 250 251 mkPassAssertion = name: cfg: { 252 assertion = with cfg.encryption; mode != "none" -> passCommand != null || passphrase != null; 253 message = 254 "passCommand or passphrase has to be specified because" 255 + " borgbackup.jobs.${name}.encryption != \"none\""; 256 }; 257 258 mkRepoService = 259 name: cfg: 260 lib.nameValuePair "borgbackup-repo-${name}" { 261 description = "Create BorgBackup repository ${name} directory"; 262 script = '' 263 mkdir -p ${lib.escapeShellArg cfg.path} 264 chown ${cfg.user}:${cfg.group} ${lib.escapeShellArg cfg.path} 265 ''; 266 serviceConfig = { 267 # The service's only task is to ensure that the specified path exists 268 Type = "oneshot"; 269 }; 270 wantedBy = [ "multi-user.target" ]; 271 }; 272 273 mkAuthorizedKey = 274 cfg: appendOnly: key: 275 let 276 # Because of the following line, clients do not need to specify an absolute repo path 277 cdCommand = "cd ${lib.escapeShellArg cfg.path}"; 278 restrictedArg = "--restrict-to-${if cfg.allowSubRepos then "path" else "repository"} ."; 279 appendOnlyArg = lib.optionalString appendOnly "--append-only"; 280 quotaArg = lib.optionalString (cfg.quota != null) "--storage-quota ${cfg.quota}"; 281 serveCommand = "borg serve ${restrictedArg} ${appendOnlyArg} ${quotaArg}"; 282 in 283 ''command="${cdCommand} && ${serveCommand}",restrict ${key}''; 284 285 mkUsersConfig = name: cfg: { 286 users.${cfg.user} = { 287 openssh.authorizedKeys.keys = ( 288 map (mkAuthorizedKey cfg false) cfg.authorizedKeys 289 ++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly 290 ); 291 useDefaultShell = true; 292 group = cfg.group; 293 isSystemUser = true; 294 }; 295 groups.${cfg.group} = { }; 296 }; 297 298 mkKeysAssertion = name: cfg: { 299 assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeysAppendOnly != [ ]; 300 message = "borgbackup.repos.${name} does not make sense" + " without at least one public key"; 301 }; 302 303 mkSourceAssertions = name: cfg: { 304 assertion = 305 lib.count isNull [ 306 cfg.dumpCommand 307 cfg.paths 308 ] == 1; 309 message = '' 310 Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand 311 must be set. 312 ''; 313 }; 314 315 mkRemovableDeviceAssertions = name: cfg: { 316 assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice; 317 message = '' 318 borgbackup.repos.${name}: repo isn't a local path, thus it can't be a removable device! 319 ''; 320 }; 321 322in 323{ 324 meta.maintainers = with lib.maintainers; [ 325 dotlambda 326 Scrumplex 327 ]; 328 meta.doc = ./borgbackup.md; 329 330 ###### interface 331 332 options.services.borgbackup.package = lib.mkPackageOption pkgs "borgbackup" { }; 333 334 options.services.borgbackup.jobs = lib.mkOption { 335 description = '' 336 Deduplicating backups using BorgBackup. 337 Adding a job will cause a borg-job-NAME wrapper to be added 338 to your system path, so that you can perform maintenance easily. 339 See also the chapter about BorgBackup in the NixOS manual. 340 ''; 341 default = { }; 342 example = lib.literalExpression '' 343 { # for a local backup 344 rootBackup = { 345 paths = "/"; 346 exclude = [ "/nix" ]; 347 repo = "/path/to/local/repo"; 348 encryption = { 349 mode = "repokey"; 350 passphrase = "secret"; 351 }; 352 compression = "auto,lzma"; 353 startAt = "weekly"; 354 }; 355 } 356 { # Root backing each day up to a remote backup server. We assume that you have 357 # * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key 358 # best practices are: use -t ed25519, /path/to = /run/keys 359 # * the passphrase is in the file /run/keys/borgbackup_passphrase 360 # * you have initialized the repository manually 361 paths = [ "/etc" "/home" ]; 362 exclude = [ "/nix" "'**/.cache'" ]; 363 doInit = false; 364 repo = "user3@arep.repo.borgbase.com:repo"; 365 encryption = { 366 mode = "repokey-blake2"; 367 passCommand = "cat /path/to/passphrase"; 368 }; 369 environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; }; 370 compression = "auto,lzma"; 371 startAt = "daily"; 372 }; 373 ''; 374 type = lib.types.attrsOf ( 375 lib.types.submodule ( 376 let 377 globalConfig = config; 378 in 379 { name, config, ... }: 380 { 381 options = { 382 383 paths = lib.mkOption { 384 type = with lib.types; nullOr (coercedTo str lib.singleton (listOf str)); 385 default = null; 386 description = '' 387 Path(s) to back up. 388 Mutually exclusive with {option}`dumpCommand`. 389 ''; 390 example = "/home/user"; 391 }; 392 393 dumpCommand = lib.mkOption { 394 type = with lib.types; nullOr path; 395 default = null; 396 description = '' 397 Backup the stdout of this program instead of filesystem paths. 398 Mutually exclusive with {option}`paths`. 399 ''; 400 example = "/path/to/createZFSsend.sh"; 401 }; 402 403 repo = lib.mkOption { 404 type = lib.types.str; 405 description = "Remote or local repository to back up to."; 406 example = "user@machine:/path/to/repo"; 407 }; 408 409 removableDevice = lib.mkOption { 410 type = lib.types.bool; 411 default = false; 412 description = "Whether the repo (which must be local) is a removable device."; 413 }; 414 415 archiveBaseName = lib.mkOption { 416 type = lib.types.nullOr (lib.types.strMatching "[^/{}]+"); 417 default = "${globalConfig.networking.hostName}-${name}"; 418 defaultText = lib.literalExpression ''"''${config.networking.hostName}-<name>"''; 419 description = '' 420 How to name the created archives. A timestamp, whose format is 421 determined by {option}`dateFormat`, will be appended. The full 422 name can be modified at runtime (`$archiveName`). 423 Placeholders like `{hostname}` must not be used. 424 Use `null` for no base name. 425 ''; 426 }; 427 428 dateFormat = lib.mkOption { 429 type = lib.types.str; 430 description = '' 431 Arguments passed to {command}`date` 432 to create a timestamp suffix for the archive name. 433 ''; 434 default = "+%Y-%m-%dT%H:%M:%S"; 435 example = "-u +%s"; 436 }; 437 438 startAt = lib.mkOption { 439 type = with lib.types; either str (listOf str); 440 default = "daily"; 441 description = '' 442 When or how often the backup should run. 443 Must be in the format described in 444 {manpage}`systemd.time(7)`. 445 If you do not want the backup to start 446 automatically, use `[ ]`. 447 It will generate a systemd service borgbackup-job-NAME. 448 You may trigger it manually via systemctl restart borgbackup-job-NAME. 449 ''; 450 }; 451 452 persistentTimer = lib.mkOption { 453 default = false; 454 type = lib.types.bool; 455 example = true; 456 description = '' 457 Set the `Persistent` option for the 458 {manpage}`systemd.timer(5)` 459 which triggers the backup immediately if the last trigger 460 was missed (e.g. if the system was powered down). 461 ''; 462 }; 463 464 inhibitsSleep = lib.mkOption { 465 default = false; 466 type = lib.types.bool; 467 example = true; 468 description = '' 469 Prevents the system from sleeping while backing up. 470 ''; 471 }; 472 473 user = lib.mkOption { 474 type = lib.types.str; 475 description = '' 476 The user {command}`borg` is run as. 477 User or group need read permission 478 for the specified {option}`paths`. 479 ''; 480 default = "root"; 481 }; 482 483 group = lib.mkOption { 484 type = lib.types.str; 485 description = '' 486 The group borg is run as. User or group needs read permission 487 for the specified {option}`paths`. 488 ''; 489 default = "root"; 490 }; 491 492 wrapper = lib.mkOption { 493 type = with lib.types; nullOr str; 494 description = '' 495 Name of the wrapper that is installed into {env}`PATH`. 496 Set to `null` or `""` to disable it altogether. 497 ''; 498 default = "borg-job-${name}"; 499 defaultText = "borg-job-<name>"; 500 }; 501 502 encryption.mode = lib.mkOption { 503 type = lib.types.enum [ 504 "repokey" 505 "keyfile" 506 "repokey-blake2" 507 "keyfile-blake2" 508 "authenticated" 509 "authenticated-blake2" 510 "none" 511 ]; 512 description = '' 513 Encryption mode to use. Setting a mode 514 other than `"none"` requires 515 you to specify a {option}`passCommand` 516 or a {option}`passphrase`. 517 ''; 518 example = "repokey-blake2"; 519 }; 520 521 encryption.passCommand = lib.mkOption { 522 type = with lib.types; nullOr str; 523 description = '' 524 A command which prints the passphrase to stdout. 525 Mutually exclusive with {option}`passphrase`. 526 ''; 527 default = null; 528 example = "cat /path/to/passphrase_file"; 529 }; 530 531 encryption.passphrase = lib.mkOption { 532 type = with lib.types; nullOr str; 533 description = '' 534 The passphrase the backups are encrypted with. 535 Mutually exclusive with {option}`passCommand`. 536 If you do not want the passphrase to be stored in the 537 world-readable Nix store, use {option}`passCommand`. 538 ''; 539 default = null; 540 }; 541 542 compression = lib.mkOption { 543 # "auto" is optional, 544 # compression mode must be given, 545 # compression level is optional 546 type = lib.types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?"; 547 description = '' 548 Compression method to use. Refer to 549 {command}`borg help compression` 550 for all available options. 551 ''; 552 default = "lz4"; 553 example = "auto,lzma"; 554 }; 555 556 exclude = lib.mkOption { 557 type = with lib.types; listOf str; 558 description = '' 559 Exclude paths matching any of the given patterns. See 560 {command}`borg help patterns` for pattern syntax. 561 ''; 562 default = [ ]; 563 example = [ 564 "/home/*/.cache" 565 "/nix" 566 ]; 567 }; 568 569 patterns = lib.mkOption { 570 type = with lib.types; listOf str; 571 description = '' 572 Include/exclude paths matching the given patterns. The first 573 matching patterns is used, so if an include pattern (prefix `+`) 574 matches before an exclude pattern (prefix `-`), the file is 575 backed up. See [{command}`borg help patterns`](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns) for pattern syntax. 576 ''; 577 default = [ ]; 578 example = [ 579 "+ /home/susan" 580 "- /home/*" 581 ]; 582 }; 583 584 readWritePaths = lib.mkOption { 585 type = with lib.types; listOf path; 586 description = '' 587 By default, borg cannot write anywhere on the system but 588 `$HOME/.config/borg` and `$HOME/.cache/borg`. 589 If, for example, your preHook script needs to dump files 590 somewhere, put those directories here. 591 ''; 592 default = [ ]; 593 example = [ 594 "/var/backup/mysqldump" 595 ]; 596 }; 597 598 privateTmp = lib.mkOption { 599 type = lib.types.bool; 600 description = '' 601 Set the `PrivateTmp` option for 602 the systemd-service. Set to false if you need sockets 603 or other files from global /tmp. 604 ''; 605 default = true; 606 }; 607 608 failOnWarnings = lib.mkOption { 609 type = lib.types.bool; 610 description = '' 611 Fail the whole backup job if any borg command returns a warning 612 (exit code 1), for example because a file changed during backup. 613 ''; 614 default = true; 615 }; 616 617 doInit = lib.mkOption { 618 type = lib.types.bool; 619 description = '' 620 Run {command}`borg init` if the 621 specified {option}`repo` does not exist. 622 You should set this to `false` 623 if the repository is located on an external drive 624 that might not always be mounted. 625 ''; 626 default = true; 627 }; 628 629 appendFailedSuffix = lib.mkOption { 630 type = lib.types.bool; 631 description = '' 632 Append a `.failed` suffix 633 to the archive name, which is only removed if 634 {command}`borg create` has a zero exit status. 635 ''; 636 default = true; 637 }; 638 639 prune.keep = lib.mkOption { 640 # Specifying e.g. `prune.keep.yearly = -1` 641 # means there is no limit of yearly archives to keep 642 # The regex is for use with e.g. --keep-within 1y 643 type = with lib.types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]")); 644 description = '' 645 Prune a repository by deleting all archives not matching any of the 646 specified retention options. See {command}`borg help prune` 647 for the available options. 648 ''; 649 default = { }; 650 example = lib.literalExpression '' 651 { 652 within = "1d"; # Keep all archives from the last day 653 daily = 7; 654 weekly = 4; 655 monthly = -1; # Keep at least one archive for each month 656 } 657 ''; 658 }; 659 660 prune.prefix = lib.mkOption { 661 type = lib.types.nullOr (lib.types.str); 662 description = '' 663 Only consider archive names starting with this prefix for pruning. 664 By default, only archives created by this job are considered. 665 Use `""` or `null` to consider all archives. 666 ''; 667 default = config.archiveBaseName; 668 defaultText = lib.literalExpression "archiveBaseName"; 669 }; 670 671 environment = lib.mkOption { 672 type = with lib.types; attrsOf str; 673 description = '' 674 Environment variables passed to the backup script. 675 You can for example specify which SSH key to use. 676 ''; 677 default = { }; 678 example = { 679 BORG_RSH = "ssh -i /path/to/key"; 680 }; 681 }; 682 683 preHook = lib.mkOption { 684 type = lib.types.lines; 685 description = '' 686 Shell commands to run before the backup. 687 This can for example be used to mount file systems. 688 ''; 689 default = ""; 690 example = '' 691 # To add excluded paths at runtime 692 extraCreateArgs+=("--exclude" "/some/path") 693 ''; 694 }; 695 696 postInit = lib.mkOption { 697 type = lib.types.lines; 698 description = '' 699 Shell commands to run after {command}`borg init`. 700 ''; 701 default = ""; 702 }; 703 704 postCreate = lib.mkOption { 705 type = lib.types.lines; 706 description = '' 707 Shell commands to run after {command}`borg create`. The name 708 of the created archive is stored in `$archiveName`. 709 ''; 710 default = ""; 711 }; 712 713 postPrune = lib.mkOption { 714 type = lib.types.lines; 715 description = '' 716 Shell commands to run after {command}`borg prune`. 717 ''; 718 default = ""; 719 }; 720 721 postHook = lib.mkOption { 722 type = lib.types.lines; 723 description = '' 724 Shell commands to run just before exit. They are executed 725 even if a previous command exits with a non-zero exit code. 726 The latter is available as `$exitStatus`. 727 ''; 728 default = ""; 729 }; 730 731 extraArgs = lib.mkOption { 732 type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str; 733 description = '' 734 Additional arguments for all {command}`borg` calls the 735 service has. Handle with care. 736 ''; 737 default = [ ]; 738 example = [ "--remote-path=/path/to/borg" ]; 739 }; 740 741 extraInitArgs = lib.mkOption { 742 type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str; 743 description = '' 744 Additional arguments for {command}`borg init`. 745 Can also be set at runtime using `$extraInitArgs`. 746 ''; 747 default = [ ]; 748 example = [ "--append-only" ]; 749 }; 750 751 extraCreateArgs = lib.mkOption { 752 type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str; 753 description = '' 754 Additional arguments for {command}`borg create`. 755 Can also be set at runtime using `$extraCreateArgs`. 756 ''; 757 default = [ ]; 758 example = [ 759 "--stats" 760 "--checkpoint-interval 600" 761 ]; 762 }; 763 764 extraPruneArgs = lib.mkOption { 765 type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str; 766 description = '' 767 Additional arguments for {command}`borg prune`. 768 Can also be set at runtime using `$extraPruneArgs`. 769 ''; 770 default = [ ]; 771 example = [ "--save-space" ]; 772 }; 773 774 extraCompactArgs = lib.mkOption { 775 type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str; 776 description = '' 777 Additional arguments for {command}`borg compact`. 778 Can also be set at runtime using `$extraCompactArgs`. 779 ''; 780 default = [ ]; 781 example = [ "--cleanup-commits" ]; 782 }; 783 }; 784 } 785 ) 786 ); 787 }; 788 789 options.services.borgbackup.repos = lib.mkOption { 790 description = '' 791 Serve BorgBackup repositories to given public SSH keys, 792 restricting their access to the repository only. 793 See also the chapter about BorgBackup in the NixOS manual. 794 Also, clients do not need to specify the absolute path when accessing the repository, 795 i.e. `user@machine:.` is enough. (Note colon and dot.) 796 ''; 797 default = { }; 798 type = lib.types.attrsOf ( 799 lib.types.submodule ( 800 { ... }: 801 { 802 options = { 803 path = lib.mkOption { 804 type = lib.types.path; 805 description = '' 806 Where to store the backups. Note that the directory 807 is created automatically, with correct permissions. 808 ''; 809 default = "/var/lib/borgbackup"; 810 }; 811 812 user = lib.mkOption { 813 type = lib.types.str; 814 description = '' 815 The user {command}`borg serve` is run as. 816 User or group needs write permission 817 for the specified {option}`path`. 818 ''; 819 default = "borg"; 820 }; 821 822 group = lib.mkOption { 823 type = lib.types.str; 824 description = '' 825 The group {command}`borg serve` is run as. 826 User or group needs write permission 827 for the specified {option}`path`. 828 ''; 829 default = "borg"; 830 }; 831 832 authorizedKeys = lib.mkOption { 833 type = with lib.types; listOf str; 834 description = '' 835 Public SSH keys that are given full write access to this repository. 836 You should use a different SSH key for each repository you write to, because 837 the specified keys are restricted to running {command}`borg serve` 838 and can only access this single repository. 839 ''; 840 default = [ ]; 841 }; 842 843 authorizedKeysAppendOnly = lib.mkOption { 844 type = with lib.types; listOf str; 845 description = '' 846 Public SSH keys that can only be used to append new data (archives) to the repository. 847 Note that archives can still be marked as deleted and are subsequently removed from disk 848 upon accessing the repo with full write access, e.g. when pruning. 849 ''; 850 default = [ ]; 851 }; 852 853 allowSubRepos = lib.mkOption { 854 type = lib.types.bool; 855 description = '' 856 Allow clients to create repositories in subdirectories of the 857 specified {option}`path`. These can be accessed using 858 `user@machine:path/to/subrepo`. Note that a 859 {option}`quota` applies to repositories independently. 860 Therefore, if this is enabled, clients can create multiple 861 repositories and upload an arbitrary amount of data. 862 ''; 863 default = false; 864 }; 865 866 quota = lib.mkOption { 867 # See the definition of parse_file_size() in src/borg/helpers/parseformat.py 868 type = with lib.types; nullOr (strMatching "[[:digit:].]+[KMGTP]?"); 869 description = '' 870 Storage quota for the repository. This quota is ensured for all 871 sub-repositories if {option}`allowSubRepos` is enabled 872 but not for the overall storage space used. 873 ''; 874 default = null; 875 example = "100G"; 876 }; 877 878 }; 879 } 880 ) 881 ); 882 }; 883 884 ###### implementation 885 886 config = lib.mkIf (with config.services.borgbackup; jobs != { } || repos != { }) ( 887 with config.services.borgbackup; 888 { 889 assertions = 890 lib.mapAttrsToList mkPassAssertion jobs 891 ++ lib.mapAttrsToList mkKeysAssertion repos 892 ++ lib.mapAttrsToList mkSourceAssertions jobs 893 ++ lib.mapAttrsToList mkRemovableDeviceAssertions jobs; 894 895 systemd.tmpfiles.settings = lib.mapAttrs' mkTmpfiles jobs; 896 897 systemd.services = 898 # A job named "foo" is mapped to systemd.services.borgbackup-job-foo 899 lib.mapAttrs' mkBackupService jobs 900 # A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo 901 // lib.mapAttrs' mkRepoService repos; 902 903 # A job named "foo" is mapped to systemd.timers.borgbackup-job-foo 904 # only generate the timer if interval (startAt) is set 905 systemd.timers = lib.mapAttrs' mkBackupTimers (lib.filterAttrs (_: cfg: cfg.startAt != [ ]) jobs); 906 907 users = lib.mkMerge (lib.mapAttrsToList mkUsersConfig repos); 908 909 environment.systemPackages = [ 910 config.services.borgbackup.package 911 ] 912 ++ (lib.flatten (lib.mapAttrsToList mkBorgWrapper jobs)); 913 } 914 ); 915}