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}