Merge branch 'main' of git.ocjtech.us:jeff/nixos-restic

This commit is contained in:
Jeffrey C. Ollie 2023-03-30 15:43:21 -05:00
commit 9367bd0267
No known key found for this signature in database
GPG key ID: F936E4DCB7E25F15
4 changed files with 398 additions and 274 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/result*

View file

@ -0,0 +1,24 @@
From 8e6186be04e2819b6e3586e5d1aeb8a824e1979f Mon Sep 17 00:00:00 2001
From: Simon Bruder <simon@sbruder.de>
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

42
flake.lock Normal file
View file

@ -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
}

605
flake.nix
View file

@ -1,5 +1,5 @@
{ {
description = "openlens"; description = "Restic";
inputs = { inputs = {
nixpkgs = { nixpkgs = {
@ -16,98 +16,151 @@
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
}; };
arch = {
"x86_64-linux" = "amd64";
}.${system};
in 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 Nixs 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, ... }: nixosModules = {
let restic = { config, lib, pkgs, ... }:
cfg = config.restic; let
in cfg = config.restic;
{ package = self.packages.${pkgs.system}.restic;
options = { in
restic = lib.options.mkOption { {
type = lib.types.submodule { options = {
options = { restic = lib.options.mkOption {
enable = lib.options.mkEnableOption "Restic"; type = lib.types.submodule {
password = lib.options.mkOption { options = {
type = lib.types.str; enable = lib.options.mkEnableOption "Restic";
}; password = lib.options.mkOption {
storage = lib.options.mkOption { type = lib.types.str;
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;
};
};
}; };
}; storage = lib.options.mkOption {
b2 = lib.options.mkOption { type = lib.types.enum [
type = lib.types.submodule { "azure"
options = { "b2"
bucket = lib.options.mkOption { ];
type = lib.types.str; # default = "b2";
};
account_id = lib.options.mkOption {
type = lib.types.str;
};
account_key = lib.options.mkOption {
type = lib.types.str;
};
};
}; };
}; healthcheck = lib.options.mkOption {
azure = lib.options.mkOption { type = lib.types.submodule {
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 = { options = {
one-file-system = lib.options.mkOption { enable = lib.options.mkEnableOption "use healthcheck";
type = lib.types.bool; api_url = lib.options.mkOption {
default = false; type = lib.types.str;
}; };
excludes = lib.options.mkOption { api_key = lib.options.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.str;
default = [ ];
}; };
paths = lib.options.mkOption { timeout = lib.options.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.int;
default = 86400;
}; };
preCommand = lib.options.mkOption { 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;
};
preCommand = lib.options.mkOption {
type = lib.types.lines; type = lib.types.lines;
default = ""; default = "";
}; };
@ -116,220 +169,224 @@
default = ""; default = "";
}; };
}; };
} }
); );
};
}; };
}; };
}; };
}; };
}; config =
config = let
let bucket = {
bucket = { "b2" = cfg.b2.bucket;
"b2" = cfg.b2.bucket; "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}";
"azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; }.${cfg.storage};
}.${cfg.storage}; directory = {
directory = { "b2" = "${config.networking.hostName}.${config.networking.domain}";
"b2" = "${config.networking.hostName}.${config.networking.domain}"; "azure" = "/";
"azure" = "/"; }.${cfg.storage};
}.${cfg.storage}; in
in lib.mkIf cfg.enable {
lib.mkIf cfg.enable { environment.systemPackages = [
environment.systemPackages = [ package
pkgs.restic ];
];
environment.etc."restic/password" = { environment.etc."restic/password" = {
text = cfg.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};
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"; user = "root";
group = "root"; group = "root";
mode = "0400"; mode = "0400";
}; };
environment.etc."restic/rclone" = { environment.etc."restic/environment" =
text = { let
"b2" = '' hcConfig =
[b2] if cfg.healthcheck.enable
type = b2 then ''
account = ${cfg.b2.account_id} HC_API_KEY=${cfg.healthcheck.api_key}
key = ${cfg.b2.account_key} HC_API_URL=${cfg.healthcheck.api_url}
''; ''
"azure" = '' else
[azure] "";
type = azureblob
account = ${cfg.azure.account_name}
key = ${cfg.azure.account_key}
'';
}.${cfg.storage};
user = "root";
group = "root";
mode = "0400";
};
systemd.services.restic = storageConfig = {
let "b2" = ''
curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; B2_ACCOUNT_ID=${cfg.b2.account_id}
hcPayload = B2_ACCOUNT_KEY=${cfg.b2.account_key}
if cfg.healthcheck.enable '';
then "azure" = ''
builtins.toJSON AZURE_ACCOUNT_NAME=${cfg.azure.account_name}
{ AZURE_ACCOUNT_KEY=${cfg.azure.account_key}
name = "${config.networking.hostName}.${config.networking.domain}"; '';
timeout = cfg.healthcheck.timeout; }.${cfg.storage};
grace = cfg.healthcheck.grace;
unique = [ "name" ]; repositoryConfig = {
channels = "*"; "b2" = "b2:${bucket}:${directory}";
} "azure" = "azure:${bucket}:${directory}";
else }.${cfg.storage};
"";
hcSetup = in
if cfg.healthcheck.enable {
then '' text = ''
HC_PAYLOAD='${hcPayload}' ${hcConfig}
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) RESTIC_CACHE_DIR=/var/cache/restic
'' RESTIC_PASSWORD_FILE=/etc/restic/password
else RESTIC_REPOSITORY=${repositoryConfig}
""; ${storageConfig}
hcStart = '';
if cfg.healthcheck.enable user = "root";
then '' group = "root";
if [ ! -z "$HC_URL" ] 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";
};
systemd.services.restic =
let
curlOptions = "--fail --silent --show-error --max-time 10 --retry 5";
hcPayload =
if cfg.healthcheck.enable
then then
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true builtins.toJSON
fi {
'' name = "${config.networking.hostName}.${config.networking.domain}";
else timeout = cfg.healthcheck.timeout;
""; grace = cfg.healthcheck.grace;
hcStop = unique = [ "name" ];
if cfg.healthcheck.enable channels = "*";
then '' }
if [ ! -z "$HC_URL" ] else
then "";
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true hcSetup =
fi if cfg.healthcheck.enable
'' then ''
else 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)
backupCommands = lib.strings.concatStringsSep "\n" ( ''
map else
( "";
backup: hcStart =
let if cfg.healthcheck.enable
one-file-system = then ''
if backup.one-file-system if [ ! -z "$HC_URL" ]
then then
" --one-file-system" ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true
else fi
""; ''
excludes = lib.strings.concatMapStrings else
( "";
arg: " --exclude=${arg}" hcStop =
) if cfg.healthcheck.enable
backup.excludes; then ''
paths = lib.strings.concatStringsSep " " backup.paths; if [ ! -z "$HC_URL" ]
in 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
''
${backup.preCommand} ${backup.preCommand}
${pkgs.restic}/bin/restic backup${one-file-system}${excludes} ${paths} ${package}/bin/restic backup${one-file-system}${excludes} ${paths}
${backup.postCommand} ${backup.postCommand}
'' ''
) )
cfg.backups cfg.backups
); );
initCheck = { initCheck = {
"b2" = '' "b2" = ''
config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
if [ "$config" != "true" ] if [ "$config" != "true" ]
then then
${pkgs.restic}/bin/restic init ${package}/bin/restic init
fi fi
''; '';
"azure" = '' "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') 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" ] if [ "$container" != "true" ]
then then
${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" ${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}"
fi fi
config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
if [ "$config" != "true" ] if [ "$config" != "true" ]
then then
${pkgs.restic}/bin/restic init ${package}/bin/restic init
fi 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}
''; '';
}.${cfg.storage};
in
{
serviceConfig = {
Type = "oneshot";
CacheDirectory = "restic";
EnvironmentFile = "/etc/restic/environment";
}; };
script = ''
${hcSetup}
${hcStart}
${initCheck} systemd.timers.restic = {
timerConfig = {
${backupCommands} OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "60m";
${pkgs.restic}/bin/restic forget --prune --keep-daily 14 };
${pkgs.restic}/bin/restic check wantedBy = [ "multi-user.target" ];
${pkgs.restic}/bin/restic cache --cleanup
${hcStop}
'';
}; };
systemd.timers.restic = {
timerConfig = {
OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "60m";
};
wantedBy = [ "multi-user.target" ];
}; };
}; };
}; };
}; };
} }