setup backup with rustic-rs

This commit is contained in:
Dmitriy Kholkin 2023-11-16 03:45:22 +03:00
parent 57a7408ec3
commit 1b740c370e
3 changed files with 91 additions and 169 deletions

View File

@ -2,38 +2,63 @@
let let
secret-conf = { services = [ "rustic-backups-nas.service" ]; }; secret-conf = { services = [ "rustic-backups-nas.service" ]; };
in { in {
secrets.rustic-gdrive-pass = secret-conf; secrets.rustic-nas-pass = secret-conf;
secrets.rclone-gdrive = secret-conf; secrets.rclone-nas-config = secret-conf;
services.rustic.backups.nas = { services.rustic.backups = rec {
initialize = true; nas-backup = {
passwordFile = config.secrets.rustic-gdrive-pass.decrypted; backup = true;
repository = "rclone:gdrive:rustic-backup/nas"; prune = false;
rcloneConfigFile = config.secrets.rclone-gdrive.decrypted; initialize = false;
extraBackupArgs = [ "--ignore-devid" ]; rcloneConfigFile = config.secrets.rclone-nas-config.decrypted;
paths = [
"/media/nas/containers"
"/media/nas/media-stack/configs"
"/srv"
];
exclude = [
"/media/nas/**/cache"
"/media/nas/**/.cache"
"/media/nas/**/log"
"/media/nas/**/logs"
"/media/nas/media-stack/configs/lidarr/config/MediaCover"
"/media/nas/media-stack/configs/qbittorrent/downloads"
"/media/nas/media-stack/configs/recyclarr/repositories"
"/srv/gitea"
];
timerConfig = { timerConfig = {
OnCalendar = "daily"; OnCalendar = "05:00";
Persistent = true; Persistent = true;
}; };
pruneOpts = [ settings = {
"--keep-daily 7" repository = {
"--keep-weekly 5" repository = "rclone:rustic-b2:ataraxia-nas-backup";
"--keep-monthly 2" password-file = config.secrets.rustic-nas-pass.decrypted;
"--keep-yearly 0" };
copy = {
targets = [{
repository = "rclone:gdrive:rustic-backup/nas-backup";
password-file = config.secrets.rustic-nas-pass.decrypted;
}];
};
repository.options = {
timeout = "10min";
};
backup = {
ignore-devid = true;
glob = [
"!/media/nas/**/cache"
"!/media/nas/**/.cache"
"!/media/nas/**/log"
"!/media/nas/**/logs"
"!/media/nas/media-stack/configs/lidarr/config/MediaCover"
"!/media/nas/media-stack/configs/qbittorrent/downloads"
"!/media/nas/media-stack/configs/recyclarr/repositories"
"!/srv/gitea"
]; ];
sources = [{
source = "/srv /media/nas/containers /media/nas/media-stack/configs";
}];
};
forget = {
prune = true;
keep-daily = 7;
keep-weekly = 5;
keep-monthly = 2;
};
};
};
nas-prune = nas-backup // {
backup = false;
prune = true;
timerConfig = {
OnCalendar = "Mon, 07:00";
Persistent = true;
};
};
}; };
} }

View File

@ -6,7 +6,7 @@ in {
./hardware-configuration.nix ./hardware-configuration.nix
./virtualisation.nix ./virtualisation.nix
./disks.nix ./disks.nix
# ./backups.nix ./backups.nix
customProfiles.hardened customProfiles.hardened
customRoles.hypervisor customRoles.hypervisor

View File

@ -5,6 +5,7 @@ with lib;
let let
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
inherit (utils.systemdUtils.unitOptions) unitOption; inherit (utils.systemdUtils.unitOptions) unitOption;
settingsFormat = pkgs.formats.toml {};
in in
{ {
options.services.rustic.backups = mkOption { options.services.rustic.backups = mkOption {
@ -13,12 +14,11 @@ in
''; '';
type = types.attrsOf (types.submodule ({ config, name, ... }: { type = types.attrsOf (types.submodule ({ config, name, ... }: {
options = { options = {
passwordFile = mkOption { settings = mkOption {
type = types.str; type = settingsFormat.type;
default = {};
description = lib.mdDoc '' description = lib.mdDoc ''
Read the repository password from a file.
''; '';
example = "/etc/nixos/rustic-password";
}; };
environmentFile = mkOption { environmentFile = mkOption {
@ -47,30 +47,6 @@ in
}; };
}; };
rcloneConfig = mkOption {
type = with types; nullOr (attrsOf (oneOf [ str bool ]));
default = null;
description = lib.mdDoc ''
Configuration for the rclone remote being used for backup.
See the remote's specific options under rclone's docs at
<https://rclone.org/docs/>. 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 = mkOption { rcloneConfigFile = mkOption {
type = with types; nullOr path; type = with types; nullOr path;
default = null; default = null;
@ -83,54 +59,6 @@ in
''; '';
}; };
repository = mkOption {
type = with types; nullOr str;
default = null;
description = lib.mdDoc ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
repositoryFile = mkOption {
type = with types; nullOr path;
default = null;
description = lib.mdDoc ''
Path to the file containing the repository location to backup to.
'';
};
paths = 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 = types.nullOr (types.listOf types.str);
default = [ ];
description = lib.mdDoc ''
Which paths to backup. If null or an empty array,
no backup command will be run.
This can be used to create a prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = lib.mdDoc ''
Patterns to exclude when backing up. See
https://rustic.readthedocs.io/en/latest/040_backup.html#excluding-files for
details on syntax.
'';
example = [
"/var/cache"
"/home/*/.cache"
".git"
];
};
timerConfig = mkOption { timerConfig = mkOption {
type = types.attrsOf unitOption; type = types.attrsOf unitOption;
default = { default = {
@ -178,6 +106,22 @@ in
]; ];
}; };
backup = mkOption {
type = types.bool;
default = true;
description = lib.mdDoc ''
Start backup.
'';
};
prune = mkOption {
type = types.bool;
default = true;
description = lib.mdDoc ''
Start prune.
'';
};
initialize = mkOption { initialize = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -197,23 +141,6 @@ in
]; ];
}; };
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = lib.mdDoc ''
A list of options (--keep-\* et al.) for 'rustic 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"
];
};
checkOpts = mkOption { checkOpts = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [ ];
@ -263,77 +190,46 @@ in
}; };
})); }));
default = { }; default = { };
example = {
localbackup = {
paths = [ "/home" ];
exclude = [ "/home/*/.cache" ];
repository = "/mnt/backup-hdd";
passwordFile = "/etc/nixos/secrets/rustic-password";
initialize = true;
};
remotebackup = {
paths = [ "/home" ];
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/rustic-password";
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
};
};
}; };
config = { config = {
# assertions = mapAttrsToList (n: v: { # assertions = mapAttrsToList (n: v: {
# assertion = (v.repository == null) != (v.repositoryFile == null); # assertion = (v.backup == true) || (v.prune == true);
# message = "services.rustic.backups.${n}: exactly one of repository or repositoryFile should be set"; # message = "services.rustic.backups.${n}: either one of or both backup and prune options should be enabled.";
# }) config.services.rustic.backups; # }) config.services.rustic.backups;
systemd.services = systemd.services =
mapAttrs' mapAttrs'
(name: backup: (name: backup:
let let
profile = settingsFormat.generate "${name}.toml" backup.settings;
extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
rusticCmd = "${backup.package}/bin/rustic${extraOptions}"; rusticCmd = "${backup.package}/bin/rustic -P ${lib.strings.removeSuffix ".toml" profile}${extraOptions}";
excludeFlags = concatMapStrings (arg: " --glob '!${arg}'") backup.exclude; pruneCmd = optionals (backup.prune) [
doBackup = backup.paths != null && backup.paths != []; (rusticCmd + " forget --prune ")
pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
(rusticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
(rusticCmd + " check " + (concatStringsSep " " backup.checkOpts)) (rusticCmd + " check " + (concatStringsSep " " backup.checkOpts))
]; ];
# Helper functions for rclone remotes # Helper functions for rclone remotes
rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); 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; toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
in in
nameValuePair "rustic-backups-${name}" ({ nameValuePair "rustic-backups-${name}" ({
environment = { environment = {
# not %C, because that wouldn't work in the wrapper script # not %C, because that wouldn't work in the wrapper script
RUSTIC_CACHE_DIR = "/var/cache/rustic-backups-${name}"; RUSTIC_CACHE_DIR = "/var/cache/rustic-backups-${name}";
RUSTIC_PASSWORD_FILE = backup.passwordFile; } // optionalAttrs (backup.rcloneConfigFile != null) {
RUSTIC_REPOSITORY = backup.repository; RCLONE_CONFIG = backup.rcloneConfigFile;
RUSTIC_REPOSITORY_FILE = backup.repositoryFile;
} // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs'
(name: value: (name: value:
nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
) )
backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { backup.rcloneOptions);
RCLONE_CONFIG = backup.rcloneConfigFile;
} // optionalAttrs (backup.rcloneConfig != null) (mapAttrs'
(name: value:
nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
)
backup.rcloneConfig);
path = [ config.programs.ssh.package pkgs.rclone ]; path = [ config.programs.ssh.package pkgs.rclone ];
restartIfChanged = false; restartIfChanged = false;
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
ExecStart = (optionals doBackup [ "${rusticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${excludeFlags} ${escapeShellArgs backup.paths}" ]) ExecStart = (optionals backup.backup [ "${rusticCmd} backup ${concatStringsSep " " backup.extraBackupArgs}" ])
++ pruneCmd; ++ pruneCmd;
User = backup.user; User = backup.user;
RuntimeDirectory = "rustic-backups-${name}"; RuntimeDirectory = "rustic-backups-${name}";
@ -371,8 +267,9 @@ in
# generate wrapper scripts, as described in the createWrapper option # generate wrapper scripts, as described in the createWrapper option
environment.systemPackages = lib.mapAttrsToList (name: backup: let environment.systemPackages = lib.mapAttrsToList (name: backup: let
extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; profile = settingsFormat.generate "${name}.toml" backup.settings;
rusticCmd = "${backup.package}/bin/rustic${extraOptions}"; extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
rusticCmd = "${backup.package}/bin/rustic -P ${lib.strings.removeSuffix ".toml" profile}${extraOptions}";
in pkgs.writeShellScriptBin "rustic-${name}" '' in pkgs.writeShellScriptBin "rustic-${name}" ''
set -a # automatically export variables set -a # automatically export variables
${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"} ${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"}