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