diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6944e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/result* diff --git a/0001-Skip-testing-restore-with-permission-failure.patch b/0001-Skip-testing-restore-with-permission-failure.patch new file mode 100644 index 0000000..907f17e --- /dev/null +++ b/0001-Skip-testing-restore-with-permission-failure.patch @@ -0,0 +1,24 @@ +From 8e6186be04e2819b6e3586e5d1aeb8a824e1979f Mon Sep 17 00:00:00 2001 +From: Simon Bruder +Date: Thu, 25 Feb 2021 09:20:51 +0100 +Subject: [PATCH] Skip testing restore with permission failure + +The test fails in sandboxed builds. +--- + cmd/restic/integration_test.go | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go +index 7d198d33..1588ccb1 100644 +--- a/cmd/restic/integration_test.go ++++ b/cmd/restic/integration_test.go +@@ -1170,6 +1170,7 @@ func TestRestoreLatest(t *testing.T) { + } + + func TestRestoreWithPermissionFailure(t *testing.T) { ++ t.Skip("Skipping testing restore with permission failure") + env, cleanup := withTestEnvironment(t) + defer cleanup() + +-- +2.29.2 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..828f5ed --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1678470307, + "narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index f7f6387..9cc18fb 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "openlens"; + description = "Restic"; inputs = { nixpkgs = { @@ -16,318 +16,363 @@ pkgs = import nixpkgs { inherit system; }; - arch = { - "x86_64-linux" = "amd64"; - }.${system}; - - in { - packages = { }; + packages = { + restic = + let + pname = "restic"; + version = "0.15.1"; + sha256 = "sha256-KdPslVJHH+xdUuFfmLZumP2lHzkDrrAvpDaj38SuP8o="; + vendorSha256 = "sha256-oetaCiXWEBUEf382l4sjO0SCPxkoh+bMTgIf/qJTQms="; + in + pkgs.buildGoModule { + inherit pname version; + + src = pkgs.fetchFromGitHub { + owner = "restic"; + repo = "restic"; + rev = "v${version}"; + sha256 = sha256; + }; + + patches = [ + # The TestRestoreWithPermissionFailure test fails in Nix’s build sandbox + ./0001-Skip-testing-restore-with-permission-failure.patch + ]; + + vendorSha256 = vendorSha256; + + subPackages = [ "cmd/restic" ]; + + nativeBuildInputs = [ pkgs.installShellFiles pkgs.makeWrapper ]; + + passthru.tests.restic = pkgs.nixosTests.restic; + + postPatch = '' + rm cmd/restic/integration_fuse_test.go + ''; + + postInstall = '' + wrapProgram $out/bin/restic --prefix PATH : '${pkgs.rclone}/bin' + '' + pkgs.lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) '' + $out/bin/restic generate \ + --bash-completion restic.bash \ + --zsh-completion restic.zsh \ + --man . + installShellCompletion restic.{bash,zsh} + installManPage *.1 + ''; + + meta = with pkgs.lib; { + homepage = "https://restic.net"; + description = "A backup program that is fast, efficient and secure"; + platforms = platforms.linux ++ platforms.darwin; + license = licenses.bsd2; + maintainers = [ maintainers.mbrgm ]; + }; + }; + }; } ) // { - nixosModules.restic = { config, lib, pkgs, ... }: - let - cfg = config.restic; - in - { - options = { - restic = lib.options.mkOption { - type = lib.types.submodule { - options = { - enable = lib.options.mkEnableOption "Restic"; - password = lib.options.mkOption { - type = lib.types.str; - }; - storage = lib.options.mkOption { - type = lib.types.enum [ - "azure" - "b2" - ]; - # default = "b2"; - }; - healthcheck = lib.options.mkOption { - type = lib.types.submodule { - options = { - enable = lib.options.mkEnableOption "use healthcheck"; - api_url = lib.options.mkOption { - type = lib.types.str; - }; - api_key = lib.options.mkOption { - type = lib.types.str; - }; - timeout = lib.options.mkOption { - type = lib.types.int; - default = 86400; - }; - grace = lib.options.mkOption { - type = lib.types.int; - default = 14400; - }; - }; + nixosModules = { + restic = { config, lib, pkgs, ... }: + let + cfg = config.restic; + package = self.packages.${pkgs.system}.restic; + in + { + options = { + restic = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "Restic"; + password = lib.options.mkOption { + type = lib.types.str; }; - }; - b2 = lib.options.mkOption { - type = lib.types.submodule { - options = { - bucket = lib.options.mkOption { - type = lib.types.str; - }; - account_id = lib.options.mkOption { - type = lib.types.str; - }; - account_key = lib.options.mkOption { - type = lib.types.str; - }; - }; + storage = lib.options.mkOption { + type = lib.types.enum [ + "azure" + "b2" + ]; + # default = "b2"; }; - }; - azure = lib.options.mkOption { - type = lib.types.submodule { - options = { - account_name = lib.options.mkOption { - type = lib.types.str; - }; - account_key = lib.options.mkOption { - type = lib.types.str; - }; - }; - }; - }; - backups = lib.options.mkOption { - type = lib.types.listOf ( - lib.types.submodule { + healthcheck = lib.options.mkOption { + type = lib.types.submodule { options = { - one-file-system = lib.options.mkOption { - type = lib.types.bool; - default = false; + enable = lib.options.mkEnableOption "use healthcheck"; + api_url = lib.options.mkOption { + type = lib.types.str; }; - excludes = lib.options.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; + api_key = lib.options.mkOption { + type = lib.types.str; }; - paths = lib.options.mkOption { - type = lib.types.listOf lib.types.str; + timeout = lib.options.mkOption { + type = lib.types.int; + default = 86400; + }; + grace = lib.options.mkOption { + type = lib.types.int; + default = 14400; }; }; - } - ); + }; + }; + b2 = lib.options.mkOption { + type = lib.types.submodule { + options = { + bucket = lib.options.mkOption { + type = lib.types.str; + }; + account_id = lib.options.mkOption { + type = lib.types.str; + }; + account_key = lib.options.mkOption { + type = lib.types.str; + }; + }; + }; + }; + azure = lib.options.mkOption { + type = lib.types.submodule { + options = { + account_name = lib.options.mkOption { + type = lib.types.str; + }; + account_key = lib.options.mkOption { + type = lib.types.str; + }; + }; + }; + }; + backups = lib.options.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + one-file-system = lib.options.mkOption { + type = lib.types.bool; + default = false; + }; + excludes = lib.options.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + paths = lib.options.mkOption { + type = lib.types.listOf lib.types.str; + }; + }; + } + ); + }; }; }; }; }; - }; - config = - let - bucket = { - "b2" = cfg.b2.bucket; - "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; - }.${cfg.storage}; - directory = { - "b2" = "${config.networking.hostName}.${config.networking.domain}"; - "azure" = "/"; - }.${cfg.storage}; - in - lib.mkIf cfg.enable { - environment.systemPackages = [ - pkgs.restic - ]; + config = + let + bucket = { + "b2" = cfg.b2.bucket; + "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; + }.${cfg.storage}; + directory = { + "b2" = "${config.networking.hostName}.${config.networking.domain}"; + "azure" = "/"; + }.${cfg.storage}; + in + lib.mkIf cfg.enable { + environment.systemPackages = [ + package + ]; - environment.etc."restic/password" = { - text = cfg.password; - user = "root"; - group = "root"; - mode = "0400"; - }; - - environment.etc."restic/environment" = - let - hcConfig = - if cfg.healthcheck.enable - then '' - HC_API_KEY=${cfg.healthcheck.api_key} - HC_API_URL=${cfg.healthcheck.api_url} - '' - else - ""; - storageConfig = { - "b2" = '' - B2_ACCOUNT_ID=${cfg.b2.account_id} - B2_ACCOUNT_KEY=${cfg.b2.account_key} - ''; - "azure" = '' - AZURE_ACCOUNT_NAME=${cfg.azure.account_name} - AZURE_ACCOUNT_KEY=${cfg.azure.account_key} - ''; - }.${cfg.storage}; - # if cfg.storage == "b2" - # then - # else - # ""; - repositoryConfig = { - "b2" = "b2:${bucket}:${directory}"; - "azure" = "azure:${bucket}:${directory}"; - }.${cfg.storage}; - - # if cfg.storage == "b2" - # then - # "b2:${cfg.b2.bucket}:${config.networking.hostName}.${config.networking.domain}" - # else - # ""; - in - { - text = '' - ${hcConfig} - RESTIC_CACHE_DIR=/var/cache/restic - RESTIC_PASSWORD_FILE=/etc/restic/password - RESTIC_REPOSITORY=${repositoryConfig} - ${storageConfig} - ''; + environment.etc."restic/password" = { + text = cfg.password; user = "root"; group = "root"; mode = "0400"; }; - environment.etc."restic/rclone" = { - text = { - "b2" = '' - [b2] - type = b2 - account = ${cfg.b2.account_id} - key = ${cfg.b2.account_key} - ''; - "azure" = '' - [azure] - type = azureblob - account = ${cfg.azure.account_name} - key = ${cfg.azure.account_key} - ''; - }.${cfg.storage}; - user = "root"; - group = "root"; - mode = "0400"; - }; + environment.etc."restic/environment" = + let + hcConfig = + if cfg.healthcheck.enable + then '' + HC_API_KEY=${cfg.healthcheck.api_key} + HC_API_URL=${cfg.healthcheck.api_url} + '' + else + ""; - systemd.services.restic = - let - curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; - hcPayload = - if cfg.healthcheck.enable - then - builtins.toJSON - { - name = "${config.networking.hostName}.${config.networking.domain}"; - timeout = cfg.healthcheck.timeout; - grace = cfg.healthcheck.grace; - unique = [ "name" ]; - channels = "*"; - } - else - ""; - hcSetup = - if cfg.healthcheck.enable - then '' - HC_PAYLOAD='${hcPayload}' - HC_URL=$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header "X-Api-Key: $HC_API_KEY" --data "$HC_PAYLOAD" $HC_API_URL | ${pkgs.jq}/bin/jq -r .ping_url) - '' - else - ""; - hcStart = - if cfg.healthcheck.enable - then '' - if [ ! -z "$HC_URL" ] - then - ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true - fi - '' - else - ""; - hcStop = - if cfg.healthcheck.enable - then '' - if [ ! -z "$HC_URL" ] - then - ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true - fi - '' - else - ""; - backupCommands = lib.strings.concatStringsSep "\n" ( - map - ( - backup: - let - one-file-system = - if backup.one-file-system - then - " --one-file-system" - else - ""; - excludes = lib.strings.concatMapStrings - ( - arg: " --exclude=${arg}" - ) - backup.excludes; - paths = lib.strings.concatStringsSep " " backup.paths; - in - "${pkgs.restic}/bin/restic backup${one-file-system}${excludes} ${paths}" - ) - cfg.backups - ); - initCheck = { - "b2" = '' - config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') + storageConfig = { + "b2" = '' + B2_ACCOUNT_ID=${cfg.b2.account_id} + B2_ACCOUNT_KEY=${cfg.b2.account_key} + ''; + "azure" = '' + AZURE_ACCOUNT_NAME=${cfg.azure.account_name} + AZURE_ACCOUNT_KEY=${cfg.azure.account_key} + ''; + }.${cfg.storage}; - if [ "$config" != "true" ] - then - ${pkgs.restic}/bin/restic init - fi + repositoryConfig = { + "b2" = "b2:${bucket}:${directory}"; + "azure" = "azure:${bucket}:${directory}"; + }.${cfg.storage}; + + in + { + text = '' + ${hcConfig} + RESTIC_CACHE_DIR=/var/cache/restic + RESTIC_PASSWORD_FILE=/etc/restic/password + RESTIC_REPOSITORY=${repositoryConfig} + ${storageConfig} ''; + user = "root"; + group = "root"; + mode = "0400"; + }; + environment.etc."restic/rclone" = { + text = { + "b2" = '' + [b2] + type = b2 + account = ${cfg.b2.account_id} + key = ${cfg.b2.account_key} + ''; "azure" = '' - container=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:" | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0') - if [ "$container" != "true" ] - then - ${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" - fi - config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') - if [ "$config" != "true" ] - then - ${pkgs.restic}/bin/restic init - fi + [azure] + type = azureblob + account = ${cfg.azure.account_name} + key = ${cfg.azure.account_key} ''; }.${cfg.storage}; - in - { - serviceConfig = { - Type = "oneshot"; - CacheDirectory = "restic"; - EnvironmentFile = "/etc/restic/environment"; + user = "root"; + group = "root"; + mode = "0400"; + }; + + systemd.services.restic = + let + curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; + hcPayload = + if cfg.healthcheck.enable + then + builtins.toJSON + { + name = "${config.networking.hostName}.${config.networking.domain}"; + timeout = cfg.healthcheck.timeout; + grace = cfg.healthcheck.grace; + unique = [ "name" ]; + channels = "*"; + } + else + ""; + hcSetup = + if cfg.healthcheck.enable + then '' + HC_PAYLOAD='${hcPayload}' + HC_URL=$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header "X-Api-Key: $HC_API_KEY" --data "$HC_PAYLOAD" $HC_API_URL | ${pkgs.jq}/bin/jq -r .ping_url) + '' + else + ""; + hcStart = + if cfg.healthcheck.enable + then '' + if [ ! -z "$HC_URL" ] + then + ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true + fi + '' + else + ""; + hcStop = + if cfg.healthcheck.enable + then '' + if [ ! -z "$HC_URL" ] + then + ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true + fi + '' + else + ""; + backupCommands = lib.strings.concatStringsSep "\n" ( + map + ( + backup: + let + one-file-system = + if backup.one-file-system + then + " --one-file-system" + else + ""; + excludes = lib.strings.concatMapStrings + ( + arg: " --exclude=${arg}" + ) + backup.excludes; + paths = lib.strings.concatStringsSep " " backup.paths; + in + "${package}/bin/restic backup${one-file-system}${excludes} ${paths}" + ) + cfg.backups + ); + initCheck = { + "b2" = '' + config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') + + if [ "$config" != "true" ] + then + ${package}/bin/restic init + fi + ''; + + "azure" = '' + container=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:" | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0') + if [ "$container" != "true" ] + then + ${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" + fi + config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') + if [ "$config" != "true" ] + then + ${package}/bin/restic init + fi + ''; + }.${cfg.storage}; + in + { + serviceConfig = { + Type = "oneshot"; + CacheDirectory = "restic"; + EnvironmentFile = "/etc/restic/environment"; + }; + script = '' + ${hcSetup} + ${hcStart} + + ${initCheck} + + ${backupCommands} + + ${package}/bin/restic forget --prune --keep-daily 14 + ${package}/bin/restic check + ${package}/bin/restic cache --cleanup + + ${hcStop} + ''; }; - script = '' - ${hcSetup} - ${hcStart} - ${initCheck} - - ${backupCommands} - - ${pkgs.restic}/bin/restic forget --prune --keep-daily 14 - ${pkgs.restic}/bin/restic check - ${pkgs.restic}/bin/restic cache --cleanup - - ${hcStop} - ''; + systemd.timers.restic = { + timerConfig = { + OnCalendar = "*-*-* 02:00:00"; + RandomizedDelaySec = "60m"; + }; + wantedBy = [ "multi-user.target" ]; }; - - systemd.timers.restic = { - timerConfig = { - OnCalendar = "*-*-* 02:00:00"; - RandomizedDelaySec = "60m"; - }; - wantedBy = [ "multi-user.target" ]; }; - }; - }; + }; + }; }; }