Merge pull request #175965 from otavio/topic/nixos-restic
Add new restic options for NixOS module
This commit is contained in:
commit
44b5c8b6a7
2 changed files with 217 additions and 150 deletions
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"',
|
||||
)
|
||||
'';
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue