diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index 2388f1d6ca1..c38fd361d35 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -31,6 +31,59 @@ in ''; }; + rcloneOptions = mkOption { + type = with 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 = mkOption { + type = with 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 = mkOption { + type = with 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. + ''; + }; + repository = mkOption { type = types.str; description = '' @@ -170,11 +223,22 @@ in ( 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 = { diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix index 67bb7f1933d..dad5bdfff27 100644 --- a/nixos/tests/restic.nix +++ b/nixos/tests/restic.nix @@ -4,33 +4,50 @@ import ./make-test-python.nix ( let password = "some_password"; repository = "/tmp/restic-backup"; - passwordFile = pkgs.writeText "password" "correcthorsebatterystaple"; + 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"; meta = with pkgs.stdenv.lib.maintainers; { - maintainers = [ bbigras ]; + maintainers = [ bbigras i077 ]; }; nodes = { server = - { ... }: + { pkgs, ... }: { services.restic.backups = { remotebackup = { - inherit repository; - passwordFile = "${passwordFile}"; - initialize = true; - paths = [ "/opt" ]; - pruneOpts = [ - "--keep-daily 2" - "--keep-weekly 1" - "--keep-monthly 1" - "--keep-yearly 99" - ]; + inherit repository passwordFile initialize paths pruneOpts; + }; + rclonebackup = { + repository = rcloneRepository; + rcloneConfig = { + type = "local"; + one_file_system = true; + }; + + # This gets overridden by rcloneConfig.type + rcloneConfigFile = pkgs.writeText "rclone.conf" '' + [local] + type=ftp + ''; + inherit passwordFile initialize paths pruneOpts; }; }; + + environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local"; }; }; @@ -38,25 +55,35 @@ import ./make-test-python.nix ( server.start() server.wait_for_unit("dbus.socket") server.fail( - "${pkgs.restic}/bin/restic -r ${repository} -p ${passwordFile} snapshots" + "${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"', ) ''; } diff --git a/pkgs/tools/backup/restic/default.nix b/pkgs/tools/backup/restic/default.nix index f366533f9bf..33cac4ad229 100644 --- a/pkgs/tools/backup/restic/default.nix +++ b/pkgs/tools/backup/restic/default.nix @@ -1,4 +1,5 @@ -{ stdenv, lib, buildGoPackage, fetchFromGitHub, installShellFiles, nixosTests}: +{ stdenv, lib, buildGoPackage, fetchFromGitHub, installShellFiles, makeWrapper +, nixosTests, rclone }: buildGoPackage rec { pname = "restic"; @@ -15,11 +16,13 @@ buildGoPackage rec { subPackages = [ "cmd/restic" ]; - nativeBuildInputs = [ installShellFiles ]; + nativeBuildInputs = [ installShellFiles makeWrapper ]; passthru.tests.restic = nixosTests.restic; - postInstall = lib.optionalString (stdenv.hostPlatform == stdenv.buildPlatform) '' + postInstall = '' + wrapProgram $out/bin/restic --prefix PATH : '${rclone}/bin' + '' + lib.optionalString (stdenv.hostPlatform == stdenv.buildPlatform) '' $out/bin/restic generate \ --bash-completion restic.bash \ --zsh-completion restic.zsh \