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