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