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