nixos-restic/flake.nix
2023-03-13 09:01:16 -05:00

379 lines
14 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
description = "Restic";
inputs = {
nixpkgs = {
url = "nixpkgs/nixos-unstable";
};
flake-utils = {
url = "github:numtide/flake-utils";
};
};
outputs = { self, nixpkgs, flake-utils, ... }@inputs:
flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
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, ... }:
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;
};
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;
};
};
};
};
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 = [
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};
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" = ''
[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
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}
'';
};
systemd.timers.restic = {
timerConfig = {
OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "60m";
};
wantedBy = [ "multi-user.target" ];
};
};
};
};
};
}