From 5a941062ace183354313d82890d384b85defeb14 Mon Sep 17 00:00:00 2001 From: Matt McHenry Date: Wed, 25 Dec 2024 20:30:28 -0500 Subject: [PATCH 1/2] restic: reformat module and tests --- nixos/modules/services/backup/restic.nix | 723 ++++++++++++----------- nixos/tests/restic.nix | 35 +- 2 files changed, 414 insertions(+), 344 deletions(-) diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index e4ca2740f8e17..2669598fab473 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -1,4 +1,10 @@ -{ config, lib, pkgs, utils, ... }: +{ + config, + lib, + pkgs, + utils, + ... +}: let # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" inherit (utils.systemdUtils.unitOptions) unitOption; @@ -8,269 +14,288 @@ in description = '' Periodic backups to create with Restic. ''; - type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { - options = { - passwordFile = lib.mkOption { - type = lib.types.str; - description = '' - Read the repository password from a file. - ''; - example = "/etc/nixos/restic-password"; - }; + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + passwordFile = lib.mkOption { + type = lib.types.str; + description = '' + Read the repository password from a file. + ''; + example = "/etc/nixos/restic-password"; + }; - environmentFile = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - file containing the credentials to access the repository, in the - format of an EnvironmentFile as described by systemd.exec(5) - ''; - }; + environmentFile = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + file containing the credentials to access the repository, in the + format of an EnvironmentFile as described by systemd.exec(5) + ''; + }; - rcloneOptions = lib.mkOption { - type = with lib.types; nullOr (attrsOf (oneOf [ str bool ])); - default = null; - description = '' - Options to pass to rclone to control its behavior. - See for - available options. When specifying option names, strip the - leading `--`. To set a flag such as - `--drive-use-trash`, which does not take a value, - set the value to the Boolean `true`. - ''; - example = { - bwlimit = "10M"; - drive-use-trash = "true"; - }; - }; + rcloneOptions = lib.mkOption { + type = + with lib.types; + nullOr ( + attrsOf (oneOf [ + str + bool + ]) + ); + default = null; + description = '' + Options to pass to rclone to control its behavior. + See for + available options. When specifying option names, strip the + leading `--`. To set a flag such as + `--drive-use-trash`, which does not take a value, + set the value to the Boolean `true`. + ''; + example = { + bwlimit = "10M"; + drive-use-trash = "true"; + }; + }; - rcloneConfig = lib.mkOption { - type = with lib.types; nullOr (attrsOf (oneOf [ str bool ])); - default = null; - description = '' - Configuration for the rclone remote being used for backup. - See the remote's specific options under rclone's docs at - . When specifying - option names, use the "config" name specified in the docs. - For example, to set `--b2-hard-delete` for a B2 - remote, use `hard_delete = true` in the - attribute set. - Warning: Secrets set in here will be world-readable in the Nix - store! Consider using the `rcloneConfigFile` - option instead to specify secret values separately. Note that - options set here will override those set in the config file. - ''; - example = { - type = "b2"; - account = "xxx"; - key = "xxx"; - hard_delete = true; - }; - }; + rcloneConfig = lib.mkOption { + type = + with lib.types; + nullOr ( + attrsOf (oneOf [ + str + bool + ]) + ); + default = null; + description = '' + Configuration for the rclone remote being used for backup. + See the remote's specific options under rclone's docs at + . When specifying + option names, use the "config" name specified in the docs. + For example, to set `--b2-hard-delete` for a B2 + remote, use `hard_delete = true` in the + attribute set. + Warning: Secrets set in here will be world-readable in the Nix + store! Consider using the `rcloneConfigFile` + option instead to specify secret values separately. Note that + options set here will override those set in the config file. + ''; + example = { + type = "b2"; + account = "xxx"; + key = "xxx"; + hard_delete = true; + }; + }; - rcloneConfigFile = lib.mkOption { - type = with lib.types; nullOr path; - default = null; - description = '' - Path to the file containing rclone configuration. This file - must contain configuration for the remote specified in this backup - set and also must be readable by root. Options set in - `rcloneConfig` will override those set in this - file. - ''; - }; + rcloneConfigFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = '' + Path to the file containing rclone configuration. This file + must contain configuration for the remote specified in this backup + set and also must be readable by root. Options set in + `rcloneConfig` will override those set in this + file. + ''; + }; - inhibitsSleep = lib.mkOption { - default = false; - type = lib.types.bool; - example = true; - description = '' - Prevents the system from sleeping while backing up. - ''; - }; + inhibitsSleep = lib.mkOption { + default = false; + type = lib.types.bool; + example = true; + description = '' + Prevents the system from sleeping while backing up. + ''; + }; - repository = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - repository to backup to. - ''; - example = "sftp:backup@192.168.1.100:/backups/${name}"; - }; + repository = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + repository to backup to. + ''; + example = "sftp:backup@192.168.1.100:/backups/${name}"; + }; - repositoryFile = lib.mkOption { - type = with lib.types; nullOr path; - default = null; - description = '' - Path to the file containing the repository location to backup to. - ''; - }; + repositoryFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = '' + Path to the file containing the repository location to backup to. + ''; + }; - paths = lib.mkOption { - # This is nullable for legacy reasons only. We should consider making it a pure listOf - # after some time has passed since this comment was added. - type = lib.types.nullOr (lib.types.listOf lib.types.str); - default = [ ]; - description = '' - Which paths to backup, in addition to ones specified via - `dynamicFilesFrom`. If null or an empty array and - `dynamicFilesFrom` is also null, no backup command will be run. - This can be used to create a prune-only job. - ''; - example = [ - "/var/lib/postgresql" - "/home/user/backup" - ]; - }; + paths = lib.mkOption { + # This is nullable for legacy reasons only. We should consider making it a pure listOf + # after some time has passed since this comment was added. + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = [ ]; + description = '' + Which paths to backup, in addition to ones specified via + `dynamicFilesFrom`. If null or an empty array and + `dynamicFilesFrom` is also null, no backup command will be run. + This can be used to create a prune-only job. + ''; + example = [ + "/var/lib/postgresql" + "/home/user/backup" + ]; + }; - exclude = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Patterns to exclude when backing up. See - https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for - details on syntax. - ''; - example = [ - "/var/cache" - "/home/*/.cache" - ".git" - ]; - }; + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Patterns to exclude when backing up. See + https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for + details on syntax. + ''; + example = [ + "/var/cache" + "/home/*/.cache" + ".git" + ]; + }; - timerConfig = lib.mkOption { - type = lib.types.nullOr (lib.types.attrsOf unitOption); - default = { - OnCalendar = "daily"; - Persistent = true; - }; - description = '' - When to run the backup. See {manpage}`systemd.timer(5)` for - details. If null no timer is created and the backup will only - run when explicitly started. - ''; - example = { - OnCalendar = "00:05"; - RandomizedDelaySec = "5h"; - Persistent = true; - }; - }; + timerConfig = lib.mkOption { + type = lib.types.nullOr (lib.types.attrsOf unitOption); + default = { + OnCalendar = "daily"; + Persistent = true; + }; + description = '' + When to run the backup. See {manpage}`systemd.timer(5)` for + details. If null no timer is created and the backup will only + run when explicitly started. + ''; + example = { + OnCalendar = "00:05"; + RandomizedDelaySec = "5h"; + Persistent = true; + }; + }; - user = lib.mkOption { - type = lib.types.str; - default = "root"; - description = '' - As which user the backup should run. - ''; - example = "postgresql"; - }; + user = lib.mkOption { + type = lib.types.str; + default = "root"; + description = '' + As which user the backup should run. + ''; + example = "postgresql"; + }; - extraBackupArgs = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Extra arguments passed to restic backup. - ''; - example = [ - "--exclude-file=/etc/nixos/restic-ignore" - ]; - }; + extraBackupArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Extra arguments passed to restic backup. + ''; + example = [ + "--exclude-file=/etc/nixos/restic-ignore" + ]; + }; - extraOptions = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Extra extended options to be passed to the restic --option flag. - ''; - example = [ - "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" - ]; - }; + extraOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Extra extended options to be passed to the restic --option flag. + ''; + example = [ + "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" + ]; + }; - initialize = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Create the repository if it doesn't exist. - ''; - }; + initialize = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Create the repository if it doesn't exist. + ''; + }; - pruneOpts = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - A list of options (--keep-\* et al.) for 'restic forget - --prune', to automatically prune old snapshots. The - 'forget' command is run *after* the 'backup' command, so - keep that in mind when constructing the --keep-\* options. - ''; - example = [ - "--keep-daily 7" - "--keep-weekly 5" - "--keep-monthly 12" - "--keep-yearly 75" - ]; - }; + pruneOpts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + A list of options (--keep-\* et al.) for 'restic forget + --prune', to automatically prune old snapshots. The + 'forget' command is run *after* the 'backup' command, so + keep that in mind when constructing the --keep-\* options. + ''; + example = [ + "--keep-daily 7" + "--keep-weekly 5" + "--keep-monthly 12" + "--keep-yearly 75" + ]; + }; - runCheck = lib.mkOption { - type = lib.types.bool; - default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0); - defaultText = lib.literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0''; - description = "Whether to run the `check` command with the provided `checkOpts` options."; - example = true; - }; + runCheck = lib.mkOption { + type = lib.types.bool; + default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0); + defaultText = lib.literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0''; + description = "Whether to run the `check` command with the provided `checkOpts` options."; + example = true; + }; - checkOpts = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - A list of options for 'restic check'. - ''; - example = [ - "--with-cache" - ]; - }; + checkOpts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + A list of options for 'restic check'. + ''; + example = [ + "--with-cache" + ]; + }; - dynamicFilesFrom = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - A script that produces a list of files to back up. The - results of this command are given to the '--files-from' - option. The result is merged with paths specified via `paths`. - ''; - example = "find /home/matt/git -type d -name .git"; - }; + dynamicFilesFrom = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + A script that produces a list of files to back up. The + results of this command are given to the '--files-from' + option. The result is merged with paths specified via `paths`. + ''; + example = "find /home/matt/git -type d -name .git"; + }; - backupPrepareCommand = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - A script that must run before starting the backup process. - ''; - }; + backupPrepareCommand = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + A script that must run before starting the backup process. + ''; + }; - backupCleanupCommand = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - A script that must run after finishing the backup process. - ''; - }; + backupCleanupCommand = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + A script that must run after finishing the backup process. + ''; + }; - package = lib.mkPackageOption pkgs "restic" { }; + package = lib.mkPackageOption pkgs "restic" { }; - createWrapper = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to generate and add a script to the system path, that has the same environment variables set - as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without - having to manually specify most options. - ''; - }; - }; - })); + createWrapper = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to generate and add a script to the system path, that has the same environment variables set + as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without + having to manually specify most options. + ''; + }; + }; + } + ) + ); default = { }; example = { localbackup = { @@ -300,119 +325,139 @@ in assertion = (v.repository == null) != (v.repositoryFile == null); message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set"; }) config.services.restic.backups; - systemd.services = - lib.mapAttrs' - (name: backup: - let - extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; - inhibitCmd = lib.concatStringsSep " " [ - "${pkgs.systemd}/bin/systemd-inhibit" - "--mode='block'" - "--who='restic'" - "--what='sleep'" - "--why=${lib.escapeShellArg "Scheduled backup ${name}"} " - ]; - resticCmd = "${lib.optionalString backup.inhibitsSleep inhibitCmd}${backup.package}/bin/restic${extraOptions}"; - excludeFlags = lib.optional (backup.exclude != []) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}"; - filesFromTmpFile = "/run/restic-backups-${name}/includes"; - doBackup = (backup.dynamicFilesFrom != null) || (backup.paths != null && backup.paths != []); - pruneCmd = lib.optionals (builtins.length backup.pruneOpts > 0) [ - (resticCmd + " forget --prune " + (lib.concatStringsSep " " backup.pruneOpts)) - ]; - checkCmd = lib.optionals backup.runCheck [ - (resticCmd + " check " + (lib.concatStringsSep " " backup.checkOpts)) - ]; - # Helper functions for rclone remotes - rcloneRemoteName = builtins.elemAt (lib.splitString ":" backup.repository) 1; - rcloneAttrToOpt = v: "RCLONE_" + lib.toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); - rcloneAttrToConf = v: "RCLONE_CONFIG_" + lib.toUpper (rcloneRemoteName + "_" + v); - toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; - in - lib.nameValuePair "restic-backups-${name}" ({ - environment = { + systemd.services = lib.mapAttrs' ( + name: backup: + let + extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; + inhibitCmd = lib.concatStringsSep " " [ + "${pkgs.systemd}/bin/systemd-inhibit" + "--mode='block'" + "--who='restic'" + "--what='sleep'" + "--why=${lib.escapeShellArg "Scheduled backup ${name}"} " + ]; + resticCmd = "${lib.optionalString backup.inhibitsSleep inhibitCmd}${backup.package}/bin/restic${extraOptions}"; + excludeFlags = lib.optional ( + backup.exclude != [ ] + ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}"; + filesFromTmpFile = "/run/restic-backups-${name}/includes"; + doBackup = (backup.dynamicFilesFrom != null) || (backup.paths != null && backup.paths != [ ]); + pruneCmd = lib.optionals (builtins.length backup.pruneOpts > 0) [ + (resticCmd + " forget --prune " + (lib.concatStringsSep " " backup.pruneOpts)) + ]; + checkCmd = lib.optionals backup.runCheck [ + (resticCmd + " check " + (lib.concatStringsSep " " backup.checkOpts)) + ]; + # Helper functions for rclone remotes + rcloneRemoteName = builtins.elemAt (lib.splitString ":" backup.repository) 1; + rcloneAttrToOpt = v: "RCLONE_" + lib.toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); + rcloneAttrToConf = v: "RCLONE_CONFIG_" + lib.toUpper (rcloneRemoteName + "_" + v); + toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; + in + lib.nameValuePair "restic-backups-${name}" ( + { + environment = + { # not %C, because that wouldn't work in the wrapper script RESTIC_CACHE_DIR = "/var/cache/restic-backups-${name}"; RESTIC_PASSWORD_FILE = backup.passwordFile; RESTIC_REPOSITORY = backup.repository; RESTIC_REPOSITORY_FILE = backup.repositoryFile; - } // lib.optionalAttrs (backup.rcloneOptions != null) (lib.mapAttrs' - (name: value: - lib.nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) - ) - backup.rcloneOptions) // lib.optionalAttrs (backup.rcloneConfigFile != null) { + } + // lib.optionalAttrs (backup.rcloneOptions != null) ( + lib.mapAttrs' ( + name: value: lib.nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) + ) backup.rcloneOptions + ) + // lib.optionalAttrs (backup.rcloneConfigFile != null) { RCLONE_CONFIG = backup.rcloneConfigFile; - } // lib.optionalAttrs (backup.rcloneConfig != null) (lib.mapAttrs' - (name: value: - lib.nameValuePair (rcloneAttrToConf name) (toRcloneVal value) - ) - backup.rcloneConfig); - path = [ config.programs.ssh.package ]; - restartIfChanged = false; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - serviceConfig = { + } + // lib.optionalAttrs (backup.rcloneConfig != null) ( + lib.mapAttrs' ( + name: value: lib.nameValuePair (rcloneAttrToConf name) (toRcloneVal value) + ) backup.rcloneConfig + ); + path = [ config.programs.ssh.package ]; + restartIfChanged = false; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = + { Type = "oneshot"; - ExecStart = (lib.optionals doBackup [ "${resticCmd} backup ${lib.concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} --files-from=${filesFromTmpFile}" ]) - ++ pruneCmd ++ checkCmd; + ExecStart = + (lib.optionals doBackup [ + "${resticCmd} backup ${ + lib.concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags) + } --files-from=${filesFromTmpFile}" + ]) + ++ pruneCmd + ++ checkCmd; User = backup.user; RuntimeDirectory = "restic-backups-${name}"; CacheDirectory = "restic-backups-${name}"; CacheDirectoryMode = "0700"; PrivateTmp = true; - } // lib.optionalAttrs (backup.environmentFile != null) { + } + // lib.optionalAttrs (backup.environmentFile != null) { EnvironmentFile = backup.environmentFile; }; - } // lib.optionalAttrs (backup.initialize || doBackup || backup.backupPrepareCommand != null) { - preStart = '' - ${lib.optionalString (backup.backupPrepareCommand != null) '' - ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand} - ''} - ${lib.optionalString (backup.initialize) '' - ${resticCmd} cat config > /dev/null || ${resticCmd} init - ''} - ${lib.optionalString (backup.paths != null && backup.paths != []) '' - cat ${pkgs.writeText "staticPaths" (lib.concatLines backup.paths)} >> ${filesFromTmpFile} - ''} - ${lib.optionalString (backup.dynamicFilesFrom != null) '' - ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} >> ${filesFromTmpFile} - ''} - ''; - } // lib.optionalAttrs (doBackup || backup.backupCleanupCommand != null) { - postStop = '' - ${lib.optionalString (backup.backupCleanupCommand != null) '' - ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand} - ''} - ${lib.optionalString doBackup '' - rm ${filesFromTmpFile} - ''} - ''; - }) - ) - config.services.restic.backups; - systemd.timers = - lib.mapAttrs' - (name: backup: lib.nameValuePair "restic-backups-${name}" { - wantedBy = [ "timers.target" ]; - timerConfig = backup.timerConfig; - }) - (lib.filterAttrs (_: backup: backup.timerConfig != null) config.services.restic.backups); + } + // lib.optionalAttrs (backup.initialize || doBackup || backup.backupPrepareCommand != null) { + preStart = '' + ${lib.optionalString (backup.backupPrepareCommand != null) '' + ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand} + ''} + ${lib.optionalString (backup.initialize) '' + ${resticCmd} cat config > /dev/null || ${resticCmd} init + ''} + ${lib.optionalString (backup.paths != null && backup.paths != [ ]) '' + cat ${pkgs.writeText "staticPaths" (lib.concatLines backup.paths)} >> ${filesFromTmpFile} + ''} + ${lib.optionalString (backup.dynamicFilesFrom != null) '' + ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} >> ${filesFromTmpFile} + ''} + ''; + } + // lib.optionalAttrs (doBackup || backup.backupCleanupCommand != null) { + postStop = '' + ${lib.optionalString (backup.backupCleanupCommand != null) '' + ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand} + ''} + ${lib.optionalString doBackup '' + rm ${filesFromTmpFile} + ''} + ''; + } + ) + ) config.services.restic.backups; + systemd.timers = lib.mapAttrs' ( + name: backup: + lib.nameValuePair "restic-backups-${name}" { + wantedBy = [ "timers.target" ]; + timerConfig = backup.timerConfig; + } + ) (lib.filterAttrs (_: backup: backup.timerConfig != null) config.services.restic.backups); # generate wrapper scripts, as described in the createWrapper option - environment.systemPackages = lib.mapAttrsToList (name: backup: let - extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; - resticCmd = "${backup.package}/bin/restic${extraOptions}"; - in pkgs.writeShellScriptBin "restic-${name}" '' - set -a # automatically export variables - ${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"} - # set same environment variables as the systemd service - ${lib.pipe config.systemd.services."restic-backups-${name}".environment [ - (lib.filterAttrs (n: v: v != null && n != "PATH")) - (lib.mapAttrsToList (n: v: "${n}=${v}")) - (lib.concatStringsSep "\n") - ]} - PATH=${config.systemd.services."restic-backups-${name}".environment.PATH}:$PATH + environment.systemPackages = lib.mapAttrsToList ( + name: backup: + let + extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; + resticCmd = "${backup.package}/bin/restic${extraOptions}"; + in + pkgs.writeShellScriptBin "restic-${name}" '' + set -a # automatically export variables + ${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"} + # set same environment variables as the systemd service + ${lib.pipe config.systemd.services."restic-backups-${name}".environment [ + (lib.filterAttrs (n: v: v != null && n != "PATH")) + (lib.mapAttrsToList (n: v: "${n}=${v}")) + (lib.concatStringsSep "\n") + ]} + PATH=${config.systemd.services."restic-backups-${name}".environment.PATH}:$PATH - exec ${resticCmd} "$@" - '') (lib.filterAttrs (_: v: v.createWrapper) config.services.restic.backups); + exec ${resticCmd} "$@" + '' + ) (lib.filterAttrs (_: v: v.createWrapper) config.services.restic.backups); }; } diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix index 72ce2ed58c125..54d2a88c45458 100644 --- a/nixos/tests/restic.nix +++ b/nixos/tests/restic.nix @@ -45,7 +45,10 @@ import ./make-test-python.nix ( name = "restic"; meta = with pkgs.lib.maintainers; { - maintainers = [ bbigras i077 ]; + maintainers = [ + bbigras + i077 + ]; }; nodes = { @@ -54,7 +57,14 @@ import ./make-test-python.nix ( { services.restic.backups = { remotebackup = { - inherit passwordFile paths exclude pruneOpts backupPrepareCommand backupCleanupCommand; + inherit + passwordFile + paths + exclude + pruneOpts + backupPrepareCommand + backupCleanupCommand + ; repository = remoteRepository; initialize = true; timerConfig = null; # has no effect here, just checking that it doesn't break the service @@ -72,18 +82,33 @@ import ./make-test-python.nix ( ''; }; inhibit-test = { - inherit passwordFile paths exclude pruneOpts; + inherit + passwordFile + paths + exclude + pruneOpts + ; repository = remoteInhibitTestRepository; initialize = true; inhibitsSleep = true; }; remote-noinit-backup = { - inherit passwordFile exclude pruneOpts paths; + inherit + passwordFile + exclude + pruneOpts + paths + ; initialize = false; repository = remoteNoInitRepository; }; rclonebackup = { - inherit passwordFile paths exclude pruneOpts; + inherit + passwordFile + paths + exclude + pruneOpts + ; initialize = true; repository = rcloneRepository; rcloneConfig = { From 780cb397e5837ee6d7163697957f8e987a3b764d Mon Sep 17 00:00:00 2001 From: Matt McHenry Date: Wed, 25 Dec 2024 21:11:28 -0500 Subject: [PATCH 2/2] restic: add unitConfig option fixes #367359 --- nixos/modules/services/backup/restic.nix | 12 ++++++++++++ nixos/tests/restic.nix | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index 2669598fab473..b6418e8838b2e 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -161,6 +161,17 @@ in ]; }; + unitConfig = lib.mkOption { + type = lib.types.nullOr (lib.types.attrsOf unitOption); + default = null; + description = '' + Configuration for the systemd unit. + ''; + example = { + OnFailure = "notify-restic-failure.service"; + }; + }; + timerConfig = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf unitOption); default = { @@ -402,6 +413,7 @@ in EnvironmentFile = backup.environmentFile; }; } + // lib.optionalAttrs (backup.unitConfig != null) backup.unitConfig // lib.optionalAttrs (backup.initialize || doBackup || backup.backupPrepareCommand != null) { preStart = '' ${lib.optionalString (backup.backupPrepareCommand != null) '' diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix index 54d2a88c45458..0df8544f13d26 100644 --- a/nixos/tests/restic.nix +++ b/nixos/tests/restic.nix @@ -137,6 +137,13 @@ import ./make-test-python.nix ( pruneOpts = [ "--keep-last 1" ]; checkOpts = [ "--some-check-option" ]; }; + customUnitConfig = { + inherit passwordFile paths; + repository = remoteRepository; + unitConfig = { + documentation = [ "custom-unit-config-was-written" ]; + }; + }; }; environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local"; @@ -236,6 +243,8 @@ import ./make-test-python.nix ( "systemctl start restic-backups-remoteprune.service", 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"', + # test that custom unit config is present + "systemctl cat restic-backups-customUnitConfig | grep custom-unit-config-was-written" ) # test that the inhibit option is working