at 23.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 archiveName="${if cfg.archiveBaseName == null then "" else 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 = with pkgs; [ 88 borgbackup 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 = "${pkgs.borgbackup}/bin/borg"; 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.jobs = mkOption { 235 description = lib.mdDoc '' 236 Deduplicating backups using BorgBackup. 237 Adding a job will cause a borg-job-NAME wrapper to be added 238 to your system path, so that you can perform maintenance easily. 239 See also the chapter about BorgBackup in the NixOS manual. 240 ''; 241 default = { }; 242 example = literalExpression '' 243 { # for a local backup 244 rootBackup = { 245 paths = "/"; 246 exclude = [ "/nix" ]; 247 repo = "/path/to/local/repo"; 248 encryption = { 249 mode = "repokey"; 250 passphrase = "secret"; 251 }; 252 compression = "auto,lzma"; 253 startAt = "weekly"; 254 }; 255 } 256 { # Root backing each day up to a remote backup server. We assume that you have 257 # * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key 258 # best practices are: use -t ed25519, /path/to = /run/keys 259 # * the passphrase is in the file /run/keys/borgbackup_passphrase 260 # * you have initialized the repository manually 261 paths = [ "/etc" "/home" ]; 262 exclude = [ "/nix" "'**/.cache'" ]; 263 doInit = false; 264 repo = "user3@arep.repo.borgbase.com:repo"; 265 encryption = { 266 mode = "repokey-blake2"; 267 passCommand = "cat /path/to/passphrase"; 268 }; 269 environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; }; 270 compression = "auto,lzma"; 271 startAt = "daily"; 272 }; 273 ''; 274 type = types.attrsOf (types.submodule (let globalConfig = config; in 275 { name, config, ... }: { 276 options = { 277 278 paths = mkOption { 279 type = with types; nullOr (coercedTo str lib.singleton (listOf str)); 280 default = null; 281 description = lib.mdDoc '' 282 Path(s) to back up. 283 Mutually exclusive with {option}`dumpCommand`. 284 ''; 285 example = "/home/user"; 286 }; 287 288 dumpCommand = mkOption { 289 type = with types; nullOr path; 290 default = null; 291 description = lib.mdDoc '' 292 Backup the stdout of this program instead of filesystem paths. 293 Mutually exclusive with {option}`paths`. 294 ''; 295 example = "/path/to/createZFSsend.sh"; 296 }; 297 298 repo = mkOption { 299 type = types.str; 300 description = lib.mdDoc "Remote or local repository to back up to."; 301 example = "user@machine:/path/to/repo"; 302 }; 303 304 removableDevice = mkOption { 305 type = types.bool; 306 default = false; 307 description = lib.mdDoc "Whether the repo (which must be local) is a removable device."; 308 }; 309 310 archiveBaseName = mkOption { 311 type = types.nullOr (types.strMatching "[^/{}]+"); 312 default = "${globalConfig.networking.hostName}-${name}"; 313 defaultText = literalExpression ''"''${config.networking.hostName}-<name>"''; 314 description = lib.mdDoc '' 315 How to name the created archives. A timestamp, whose format is 316 determined by {option}`dateFormat`, will be appended. The full 317 name can be modified at runtime (`$archiveName`). 318 Placeholders like `{hostname}` must not be used. 319 Use `null` for no base name. 320 ''; 321 }; 322 323 dateFormat = mkOption { 324 type = types.str; 325 description = lib.mdDoc '' 326 Arguments passed to {command}`date` 327 to create a timestamp suffix for the archive name. 328 ''; 329 default = "+%Y-%m-%dT%H:%M:%S"; 330 example = "-u +%s"; 331 }; 332 333 startAt = mkOption { 334 type = with types; either str (listOf str); 335 default = "daily"; 336 description = lib.mdDoc '' 337 When or how often the backup should run. 338 Must be in the format described in 339 {manpage}`systemd.time(7)`. 340 If you do not want the backup to start 341 automatically, use `[ ]`. 342 It will generate a systemd service borgbackup-job-NAME. 343 You may trigger it manually via systemctl restart borgbackup-job-NAME. 344 ''; 345 }; 346 347 persistentTimer = mkOption { 348 default = false; 349 type = types.bool; 350 example = true; 351 description = lib.mdDoc '' 352 Set the `persistentTimer` option for the 353 {manpage}`systemd.timer(5)` 354 which triggers the backup immediately if the last trigger 355 was missed (e.g. if the system was powered down). 356 ''; 357 }; 358 359 inhibitsSleep = mkOption { 360 default = false; 361 type = types.bool; 362 example = true; 363 description = lib.mdDoc '' 364 Prevents the system from sleeping while backing up. 365 ''; 366 }; 367 368 user = mkOption { 369 type = types.str; 370 description = lib.mdDoc '' 371 The user {command}`borg` is run as. 372 User or group need read permission 373 for the specified {option}`paths`. 374 ''; 375 default = "root"; 376 }; 377 378 group = mkOption { 379 type = types.str; 380 description = lib.mdDoc '' 381 The group borg is run as. User or group needs read permission 382 for the specified {option}`paths`. 383 ''; 384 default = "root"; 385 }; 386 387 encryption.mode = mkOption { 388 type = types.enum [ 389 "repokey" "keyfile" 390 "repokey-blake2" "keyfile-blake2" 391 "authenticated" "authenticated-blake2" 392 "none" 393 ]; 394 description = lib.mdDoc '' 395 Encryption mode to use. Setting a mode 396 other than `"none"` requires 397 you to specify a {option}`passCommand` 398 or a {option}`passphrase`. 399 ''; 400 example = "repokey-blake2"; 401 }; 402 403 encryption.passCommand = mkOption { 404 type = with types; nullOr str; 405 description = lib.mdDoc '' 406 A command which prints the passphrase to stdout. 407 Mutually exclusive with {option}`passphrase`. 408 ''; 409 default = null; 410 example = "cat /path/to/passphrase_file"; 411 }; 412 413 encryption.passphrase = mkOption { 414 type = with types; nullOr str; 415 description = lib.mdDoc '' 416 The passphrase the backups are encrypted with. 417 Mutually exclusive with {option}`passCommand`. 418 If you do not want the passphrase to be stored in the 419 world-readable Nix store, use {option}`passCommand`. 420 ''; 421 default = null; 422 }; 423 424 compression = mkOption { 425 # "auto" is optional, 426 # compression mode must be given, 427 # compression level is optional 428 type = types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?"; 429 description = lib.mdDoc '' 430 Compression method to use. Refer to 431 {command}`borg help compression` 432 for all available options. 433 ''; 434 default = "lz4"; 435 example = "auto,lzma"; 436 }; 437 438 exclude = mkOption { 439 type = with types; listOf str; 440 description = lib.mdDoc '' 441 Exclude paths matching any of the given patterns. See 442 {command}`borg help patterns` for pattern syntax. 443 ''; 444 default = [ ]; 445 example = [ 446 "/home/*/.cache" 447 "/nix" 448 ]; 449 }; 450 451 patterns = mkOption { 452 type = with types; listOf str; 453 description = lib.mdDoc '' 454 Include/exclude paths matching the given patterns. The first 455 matching patterns is used, so if an include pattern (prefix `+`) 456 matches before an exclude pattern (prefix `-`), the file is 457 backed up. See [{command}`borg help patterns`](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns) for pattern syntax. 458 ''; 459 default = [ ]; 460 example = [ 461 "+ /home/susan" 462 "- /home/*" 463 ]; 464 }; 465 466 readWritePaths = mkOption { 467 type = with types; listOf path; 468 description = lib.mdDoc '' 469 By default, borg cannot write anywhere on the system but 470 `$HOME/.config/borg` and `$HOME/.cache/borg`. 471 If, for example, your preHook script needs to dump files 472 somewhere, put those directories here. 473 ''; 474 default = [ ]; 475 example = [ 476 "/var/backup/mysqldump" 477 ]; 478 }; 479 480 privateTmp = mkOption { 481 type = types.bool; 482 description = lib.mdDoc '' 483 Set the `PrivateTmp` option for 484 the systemd-service. Set to false if you need sockets 485 or other files from global /tmp. 486 ''; 487 default = true; 488 }; 489 490 doInit = mkOption { 491 type = types.bool; 492 description = lib.mdDoc '' 493 Run {command}`borg init` if the 494 specified {option}`repo` does not exist. 495 You should set this to `false` 496 if the repository is located on an external drive 497 that might not always be mounted. 498 ''; 499 default = true; 500 }; 501 502 appendFailedSuffix = mkOption { 503 type = types.bool; 504 description = lib.mdDoc '' 505 Append a `.failed` suffix 506 to the archive name, which is only removed if 507 {command}`borg create` has a zero exit status. 508 ''; 509 default = true; 510 }; 511 512 prune.keep = mkOption { 513 # Specifying e.g. `prune.keep.yearly = -1` 514 # means there is no limit of yearly archives to keep 515 # The regex is for use with e.g. --keep-within 1y 516 type = with types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]")); 517 description = lib.mdDoc '' 518 Prune a repository by deleting all archives not matching any of the 519 specified retention options. See {command}`borg help prune` 520 for the available options. 521 ''; 522 default = { }; 523 example = literalExpression '' 524 { 525 within = "1d"; # Keep all archives from the last day 526 daily = 7; 527 weekly = 4; 528 monthly = -1; # Keep at least one archive for each month 529 } 530 ''; 531 }; 532 533 prune.prefix = mkOption { 534 type = types.nullOr (types.str); 535 description = lib.mdDoc '' 536 Only consider archive names starting with this prefix for pruning. 537 By default, only archives created by this job are considered. 538 Use `""` or `null` to consider all archives. 539 ''; 540 default = config.archiveBaseName; 541 defaultText = literalExpression "archiveBaseName"; 542 }; 543 544 environment = mkOption { 545 type = with types; attrsOf str; 546 description = lib.mdDoc '' 547 Environment variables passed to the backup script. 548 You can for example specify which SSH key to use. 549 ''; 550 default = { }; 551 example = { BORG_RSH = "ssh -i /path/to/key"; }; 552 }; 553 554 preHook = mkOption { 555 type = types.lines; 556 description = lib.mdDoc '' 557 Shell commands to run before the backup. 558 This can for example be used to mount file systems. 559 ''; 560 default = ""; 561 example = '' 562 # To add excluded paths at runtime 563 extraCreateArgs="$extraCreateArgs --exclude /some/path" 564 ''; 565 }; 566 567 postInit = mkOption { 568 type = types.lines; 569 description = lib.mdDoc '' 570 Shell commands to run after {command}`borg init`. 571 ''; 572 default = ""; 573 }; 574 575 postCreate = mkOption { 576 type = types.lines; 577 description = lib.mdDoc '' 578 Shell commands to run after {command}`borg create`. The name 579 of the created archive is stored in `$archiveName`. 580 ''; 581 default = ""; 582 }; 583 584 postPrune = mkOption { 585 type = types.lines; 586 description = lib.mdDoc '' 587 Shell commands to run after {command}`borg prune`. 588 ''; 589 default = ""; 590 }; 591 592 postHook = mkOption { 593 type = types.lines; 594 description = lib.mdDoc '' 595 Shell commands to run just before exit. They are executed 596 even if a previous command exits with a non-zero exit code. 597 The latter is available as `$exitStatus`. 598 ''; 599 default = ""; 600 }; 601 602 extraArgs = mkOption { 603 type = types.str; 604 description = lib.mdDoc '' 605 Additional arguments for all {command}`borg` calls the 606 service has. Handle with care. 607 ''; 608 default = ""; 609 example = "--remote-path=/path/to/borg"; 610 }; 611 612 extraInitArgs = mkOption { 613 type = types.str; 614 description = lib.mdDoc '' 615 Additional arguments for {command}`borg init`. 616 Can also be set at runtime using `$extraInitArgs`. 617 ''; 618 default = ""; 619 example = "--append-only"; 620 }; 621 622 extraCreateArgs = mkOption { 623 type = types.str; 624 description = lib.mdDoc '' 625 Additional arguments for {command}`borg create`. 626 Can also be set at runtime using `$extraCreateArgs`. 627 ''; 628 default = ""; 629 example = "--stats --checkpoint-interval 600"; 630 }; 631 632 extraPruneArgs = mkOption { 633 type = types.str; 634 description = lib.mdDoc '' 635 Additional arguments for {command}`borg prune`. 636 Can also be set at runtime using `$extraPruneArgs`. 637 ''; 638 default = ""; 639 example = "--save-space"; 640 }; 641 642 extraCompactArgs = mkOption { 643 type = types.str; 644 description = lib.mdDoc '' 645 Additional arguments for {command}`borg compact`. 646 Can also be set at runtime using `$extraCompactArgs`. 647 ''; 648 default = ""; 649 example = "--cleanup-commits"; 650 }; 651 }; 652 } 653 )); 654 }; 655 656 options.services.borgbackup.repos = mkOption { 657 description = lib.mdDoc '' 658 Serve BorgBackup repositories to given public SSH keys, 659 restricting their access to the repository only. 660 See also the chapter about BorgBackup in the NixOS manual. 661 Also, clients do not need to specify the absolute path when accessing the repository, 662 i.e. `user@machine:.` is enough. (Note colon and dot.) 663 ''; 664 default = { }; 665 type = types.attrsOf (types.submodule ( 666 { ... }: { 667 options = { 668 path = mkOption { 669 type = types.path; 670 description = lib.mdDoc '' 671 Where to store the backups. Note that the directory 672 is created automatically, with correct permissions. 673 ''; 674 default = "/var/lib/borgbackup"; 675 }; 676 677 user = mkOption { 678 type = types.str; 679 description = lib.mdDoc '' 680 The user {command}`borg serve` is run as. 681 User or group needs write permission 682 for the specified {option}`path`. 683 ''; 684 default = "borg"; 685 }; 686 687 group = mkOption { 688 type = types.str; 689 description = lib.mdDoc '' 690 The group {command}`borg serve` is run as. 691 User or group needs write permission 692 for the specified {option}`path`. 693 ''; 694 default = "borg"; 695 }; 696 697 authorizedKeys = mkOption { 698 type = with types; listOf str; 699 description = lib.mdDoc '' 700 Public SSH keys that are given full write access to this repository. 701 You should use a different SSH key for each repository you write to, because 702 the specified keys are restricted to running {command}`borg serve` 703 and can only access this single repository. 704 ''; 705 default = [ ]; 706 }; 707 708 authorizedKeysAppendOnly = mkOption { 709 type = with types; listOf str; 710 description = lib.mdDoc '' 711 Public SSH keys that can only be used to append new data (archives) to the repository. 712 Note that archives can still be marked as deleted and are subsequently removed from disk 713 upon accessing the repo with full write access, e.g. when pruning. 714 ''; 715 default = [ ]; 716 }; 717 718 allowSubRepos = mkOption { 719 type = types.bool; 720 description = lib.mdDoc '' 721 Allow clients to create repositories in subdirectories of the 722 specified {option}`path`. These can be accessed using 723 `user@machine:path/to/subrepo`. Note that a 724 {option}`quota` applies to repositories independently. 725 Therefore, if this is enabled, clients can create multiple 726 repositories and upload an arbitrary amount of data. 727 ''; 728 default = false; 729 }; 730 731 quota = mkOption { 732 # See the definition of parse_file_size() in src/borg/helpers/parseformat.py 733 type = with types; nullOr (strMatching "[[:digit:].]+[KMGTP]?"); 734 description = lib.mdDoc '' 735 Storage quota for the repository. This quota is ensured for all 736 sub-repositories if {option}`allowSubRepos` is enabled 737 but not for the overall storage space used. 738 ''; 739 default = null; 740 example = "100G"; 741 }; 742 743 }; 744 } 745 )); 746 }; 747 748 ###### implementation 749 750 config = mkIf (with config.services.borgbackup; jobs != { } || repos != { }) 751 (with config.services.borgbackup; { 752 assertions = 753 mapAttrsToList mkPassAssertion jobs 754 ++ mapAttrsToList mkKeysAssertion repos 755 ++ mapAttrsToList mkSourceAssertions jobs 756 ++ mapAttrsToList mkRemovableDeviceAssertions jobs; 757 758 system.activationScripts = mapAttrs' mkActivationScript jobs; 759 760 systemd.services = 761 # A job named "foo" is mapped to systemd.services.borgbackup-job-foo 762 mapAttrs' mkBackupService jobs 763 # A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo 764 // mapAttrs' mkRepoService repos; 765 766 # A job named "foo" is mapped to systemd.timers.borgbackup-job-foo 767 # only generate the timer if interval (startAt) is set 768 systemd.timers = mapAttrs' mkBackupTimers (filterAttrs (_: cfg: cfg.startAt != []) jobs); 769 770 users = mkMerge (mapAttrsToList mkUsersConfig repos); 771 772 environment.systemPackages = with pkgs; [ borgbackup ] ++ (mapAttrsToList mkBorgWrapper jobs); 773 }); 774}