diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index 8ff8e31864be..333fdd494e3b 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -96,13 +96,22 @@ in }; repository = mkOption { - type = types.str; + type = with types; nullOr str; + default = null; description = '' repository to backup to. ''; example = "sftp:backup@192.168.1.100:/backups/${name}"; }; + repositoryFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to the file containing the repository location to backup to. + ''; + }; + paths = mkOption { type = types.nullOr (types.listOf types.str); default = null; @@ -142,7 +151,7 @@ in extraBackupArgs = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; description = '' Extra arguments passed to restic backup. ''; @@ -153,7 +162,7 @@ in extraOptions = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; description = '' Extra extended options to be passed to the restic --option flag. ''; @@ -172,7 +181,7 @@ in pruneOpts = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; description = '' A list of options (--keep-* et al.) for 'restic forget --prune', to automatically prune old snapshots. The @@ -197,9 +206,25 @@ in ''; example = "find /home/matt/git -type d -name .git"; }; + + backupPrepareCommand = mkOption { + type = with types; nullOr str; + default = null; + description = '' + A script that must run before starting the backup process. + ''; + }; + + backupCleanupCommand = mkOption { + type = with types; nullOr str; + default = null; + description = '' + A script that must run after finishing the backup process. + ''; + }; }; })); - default = {}; + default = { }; example = { localbackup = { paths = [ "/home" ]; @@ -225,66 +250,85 @@ in config = { warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups); systemd.services = - mapAttrs' (name: backup: - let - extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; - resticCmd = "${pkgs.restic}/bin/restic${extraOptions}"; - filesFromTmpFile = "/run/restic-backups-${name}/includes"; - backupPaths = if (backup.dynamicFilesFrom == null) - then if (backup.paths != null) then concatStringsSep " " backup.paths else "" - else "--files-from ${filesFromTmpFile}"; - pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ - ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) ) - ( resticCmd + " check" ) - ]; - # Helper functions for rclone remotes - rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1; - rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); - rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); - toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; - in nameValuePair "restic-backups-${name}" ({ - environment = { - RESTIC_PASSWORD_FILE = backup.passwordFile; - RESTIC_REPOSITORY = backup.repository; - } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value: - nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) - ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { - RCLONE_CONFIG = backup.rcloneConfigFile; - } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value: - nameValuePair (rcloneAttrToConf name) (toRcloneVal value) - ) backup.rcloneConfig); - path = [ pkgs.openssh ]; - restartIfChanged = false; - serviceConfig = { - Type = "oneshot"; - ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ]) - ++ pruneCmd; - User = backup.user; - RuntimeDirectory = "restic-backups-${name}"; - CacheDirectory = "restic-backups-${name}"; - CacheDirectoryMode = "0700"; - } // optionalAttrs (backup.environmentFile != null) { - EnvironmentFile = backup.environmentFile; - }; - } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) { - preStart = '' - ${optionalString (backup.initialize) '' - ${resticCmd} snapshots || ${resticCmd} init - ''} - ${optionalString (backup.dynamicFilesFrom != null) '' - ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile} - ''} - ''; - } // optionalAttrs (backup.dynamicFilesFrom != null) { - postStart = '' - rm ${filesFromTmpFile} - ''; - }) - ) config.services.restic.backups; + mapAttrs' + (name: backup: + let + extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; + resticCmd = "${pkgs.restic}/bin/restic${extraOptions}"; + filesFromTmpFile = "/run/restic-backups-${name}/includes"; + backupPaths = + if (backup.dynamicFilesFrom == null) + then if (backup.paths != null) then concatStringsSep " " backup.paths else "" + else "--files-from ${filesFromTmpFile}"; + pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ + (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts)) + (resticCmd + " check") + ]; + # Helper functions for rclone remotes + rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1; + rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); + rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); + toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; + in + nameValuePair "restic-backups-${name}" ({ + environment = { + RESTIC_PASSWORD_FILE = backup.passwordFile; + RESTIC_REPOSITORY = backup.repository; + RESTIC_REPOSITORY_FILE = backup.repositoryFile; + } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' + (name: value: + nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) + ) + backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { + RCLONE_CONFIG = backup.rcloneConfigFile; + } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' + (name: value: + nameValuePair (rcloneAttrToConf name) (toRcloneVal value) + ) + backup.rcloneConfig); + path = [ pkgs.openssh ]; + restartIfChanged = false; + serviceConfig = { + Type = "oneshot"; + ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ]) + ++ pruneCmd; + User = backup.user; + RuntimeDirectory = "restic-backups-${name}"; + CacheDirectory = "restic-backups-${name}"; + CacheDirectoryMode = "0700"; + } // optionalAttrs (backup.environmentFile != null) { + EnvironmentFile = backup.environmentFile; + }; + } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) { + preStart = '' + ${optionalString (backup.backupPrepareCommand != null) '' + ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand} + ''} + ${optionalString (backup.initialize) '' + ${resticCmd} snapshots || ${resticCmd} init + ''} + ${optionalString (backup.dynamicFilesFrom != null) '' + ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile} + ''} + ''; + } // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) { + postStart = '' + ${optionalString (backup.backupCleanupCommand != null) '' + ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand} + ''} + ${optionalString (backup.dynamicFilesFrom != null) '' + rm ${filesFromTmpFile} + ''} + ''; + }) + ) + config.services.restic.backups; systemd.timers = - mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" { - wantedBy = [ "timers.target" ]; - timerConfig = backup.timerConfig; - }) config.services.restic.backups; + mapAttrs' + (name: backup: nameValuePair "restic-backups-${name}" { + wantedBy = [ "timers.target" ]; + timerConfig = backup.timerConfig; + }) + config.services.restic.backups; }; } diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix index 16979eab8217..7523d5e5ed5d 100644 --- a/nixos/tests/restic.nix +++ b/nixos/tests/restic.nix @@ -1,96 +1,119 @@ import ./make-test-python.nix ( { pkgs, ... }: - let - password = "some_password"; - repository = "/tmp/restic-backup"; - rcloneRepository = "rclone:local:/tmp/restic-rclone-backup"; + let + password = "some_password"; + repository = "/tmp/restic-backup"; + repositoryFile = "${pkgs.writeText "repositoryFile" "/tmp/restic-backup-from-file"}"; + rcloneRepository = "rclone:local:/tmp/restic-rclone-backup"; - passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}"; - initialize = true; - paths = [ "/opt" ]; - pruneOpts = [ - "--keep-daily 2" - "--keep-weekly 1" - "--keep-monthly 1" - "--keep-yearly 99" - ]; - in - { - name = "restic"; + backupPrepareCommand = '' + touch /opt/backupPrepareCommand + test ! -e /opt/backupCleanupCommand + ''; - meta = with pkgs.lib.maintainers; { - maintainers = [ bbigras i077 ]; - }; + backupCleanupCommand = '' + rm /opt/backupPrepareCommand + touch /opt/backupCleanupCommand + ''; - nodes = { - server = - { pkgs, ... }: - { - services.restic.backups = { - remotebackup = { - inherit repository passwordFile initialize paths pruneOpts; - }; - rclonebackup = { - repository = rcloneRepository; - rcloneConfig = { - type = "local"; - one_file_system = true; - }; + passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}"; + initialize = true; + paths = [ "/opt" ]; + pruneOpts = [ + "--keep-daily 2" + "--keep-weekly 1" + "--keep-monthly 1" + "--keep-yearly 99" + ]; + in + { + name = "restic"; - # This gets overridden by rcloneConfig.type - rcloneConfigFile = pkgs.writeText "rclone.conf" '' - [local] - type=ftp - ''; - inherit passwordFile initialize paths pruneOpts; - }; - remoteprune = { - inherit repository passwordFile; - pruneOpts = [ "--keep-last 1" ]; - }; - }; + meta = with pkgs.lib.maintainers; { + maintainers = [ bbigras i077 ]; + }; - environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local"; + nodes = { + server = + { pkgs, ... }: + { + services.restic.backups = { + remotebackup = { + inherit repository passwordFile initialize paths pruneOpts backupPrepareCommand backupCleanupCommand; + }; + remotebackup-from-file = { + inherit repositoryFile passwordFile initialize paths pruneOpts; + }; + rclonebackup = { + repository = rcloneRepository; + rcloneConfig = { + type = "local"; + one_file_system = true; }; - }; - testScript = '' - server.start() - server.wait_for_unit("dbus.socket") - server.fail( - "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots", - "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots", - ) - server.succeed( - "mkdir -p /opt", - "touch /opt/some_file", - "mkdir -p /tmp/restic-rclone-backup", - "timedatectl set-time '2016-12-13 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', - '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', - "timedatectl set-time '2017-12-13 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - "timedatectl set-time '2018-12-13 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - "timedatectl set-time '2018-12-14 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - "timedatectl set-time '2018-12-15 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - "timedatectl set-time '2018-12-16 13:45'", - "systemctl start restic-backups-remotebackup.service", - "systemctl start restic-backups-rclonebackup.service", - '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"', - '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"', - "systemctl start restic-backups-remoteprune.service", - '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', - ) - ''; - } + # This gets overridden by rcloneConfig.type + rcloneConfigFile = pkgs.writeText "rclone.conf" '' + [local] + type=ftp + ''; + inherit passwordFile initialize paths pruneOpts; + }; + remoteprune = { + inherit repository passwordFile; + pruneOpts = [ "--keep-last 1" ]; + }; + }; + + environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local"; + }; + }; + + testScript = '' + server.start() + server.wait_for_unit("dbus.socket") + server.fail( + "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots", + '${pkgs.restic}/bin/restic --repository-file ${repositoryFile} -p ${passwordFile} snapshots"', + "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots", + ) + server.succeed( + "mkdir -p /opt", + "touch /opt/some_file", + "mkdir -p /tmp/restic-rclone-backup", + "timedatectl set-time '2016-12-13 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-remotebackup-from-file.service", + "systemctl start restic-backups-rclonebackup.service", + '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', + '${pkgs.restic}/bin/restic --repository-file ${repositoryFile} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', + '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', + "timedatectl set-time '2017-12-13 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-rclonebackup.service", + "timedatectl set-time '2018-12-13 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-rclonebackup.service", + "timedatectl set-time '2018-12-14 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-rclonebackup.service", + "timedatectl set-time '2018-12-15 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-rclonebackup.service", + "timedatectl set-time '2018-12-16 13:45'", + "systemctl start restic-backups-remotebackup.service", + "rm /opt/backupCleanupCommand", + "systemctl start restic-backups-rclonebackup.service", + '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"', + '${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots -c | grep -e "^4 snapshot"', + "systemctl start restic-backups-remoteprune.service", + '${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots -c | grep -e "^1 snapshot"', + ) + ''; + } )