nixos-restic/flake.nix

408 lines
15 KiB
Nix
Raw Normal View History

2023-01-26 14:29:12 -06:00
{
2023-03-13 09:01:16 -05:00
description = "Restic";
2023-01-26 14:29:12 -06:00
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
{
2023-03-13 09:01:16 -05:00
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 = [
2023-04-04 11:37:34 -05:00
# The TestRestoreWithPermissionFailure test fails in Nix's build sandbox
2023-03-13 09:01:16 -05:00
./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 ];
};
};
};
2023-01-26 14:29:12 -06:00
}
) // {
2023-03-13 09:01:16 -05:00
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";
2023-04-04 11:16:35 -05:00
passwordFile = lib.options.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
2023-03-13 09:01:16 -05:00
password = lib.options.mkOption {
2023-04-04 11:16:35 -05:00
type = lib.types.nullOr lib.types.str;
default = null;
2023-01-26 14:29:12 -06:00
};
2023-03-13 09:01:16 -05:00
storage = lib.options.mkOption {
type = lib.types.enum [
"azure"
"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;
};
2023-01-26 14:29:12 -06:00
};
};
};
2023-03-13 09:01:16 -05:00
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;
};
2023-01-26 14:29:12 -06:00
};
};
};
2023-03-13 09:01:16 -05:00
azure = lib.options.mkOption {
type = lib.types.submodule {
2023-01-26 14:29:12 -06:00
options = {
2023-03-13 09:01:16 -05:00
account_name = lib.options.mkOption {
type = lib.types.str;
2023-01-26 14:29:12 -06:00
};
2023-03-13 09:01:16 -05:00
account_key = lib.options.mkOption {
type = lib.types.str;
2023-01-26 14:29:12 -06:00
};
};
2023-03-13 09:01:16 -05:00
};
};
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 {
2023-03-30 15:50:55 -05:00
type = lib.types.lines;
default = "";
};
postCommand = lib.options.mkOption {
type = lib.types.lines;
default = "";
};
2023-03-30 15:40:02 -05:00
};
2023-03-13 09:01:16 -05:00
}
);
};
2023-01-26 14:29:12 -06:00
};
};
};
};
2023-03-13 09:01:16 -05:00
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 {
2023-04-04 11:16:35 -05:00
assertions = [
{
2023-04-04 11:27:15 -05:00
assertion = cfg.passwordFile != null && cfg.password != null;
2023-04-04 11:16:35 -05:00
message = "Must specifiy either passwordFile or password";
}
];
warnings =
if cfg.password != null then [
''Restic encryption password will be stored world readable in the Nix store.''
] else [ ];
2023-03-13 09:01:16 -05:00
environment.systemPackages = [
package
];
2023-01-26 14:29:12 -06:00
2023-04-04 11:30:32 -05:00
environment.etc."restic/password" = lib.mkIf (cfg.password != null) {
2023-03-13 09:01:16 -05:00
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
"";
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
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
2023-04-04 11:16:35 -05:00
RESTIC_PASSWORD_FILE=${if cfg.passwordFile != null then cfg.passwordFile else "/etc/restic/password"}
2023-03-13 09:01:16 -05:00
RESTIC_REPOSITORY=${repositoryConfig}
${storageConfig}
'';
user = "root";
group = "root";
mode = "0400";
};
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
environment.etc."restic/rclone" = {
text = {
2023-01-26 14:29:12 -06:00
"b2" = ''
2023-03-13 09:01:16 -05:00
[b2]
type = b2
account = ${cfg.b2.account_id}
key = ${cfg.b2.account_key}
2023-01-26 14:29:12 -06:00
'';
"azure" = ''
2023-03-13 09:01:16 -05:00
[azure]
type = azureblob
account = ${cfg.azure.account_name}
key = ${cfg.azure.account_key}
2023-01-26 14:29:12 -06:00
'';
}.${cfg.storage};
user = "root";
group = "root";
mode = "0400";
};
2023-03-13 09:01:16 -05:00
systemd.services.restic =
let
curlOptions = "--fail --silent --show-error --max-time 10 --retry 5";
hcPayload =
if cfg.healthcheck.enable
2023-01-26 14:29:12 -06:00
then
2023-03-13 09:01:16 -05:00
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
''
2023-03-30 15:50:55 -05:00
${backup.preCommand}
2023-03-30 15:40:02 -05:00
2023-03-30 15:50:55 -05:00
${package}/bin/restic backup${one-file-system}${excludes} ${paths}
2023-03-30 15:40:02 -05:00
2023-03-30 15:50:55 -05:00
${backup.postCommand}
''
2023-03-13 09:01:16 -05:00
)
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')
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
if [ "$config" != "true" ]
then
${package}/bin/restic init
fi
'';
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
"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}
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
${initCheck}
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
${backupCommands}
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
${package}/bin/restic forget --prune --keep-daily 14
${package}/bin/restic check
${package}/bin/restic cache --cleanup
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
${hcStop}
'';
};
2023-01-26 14:29:12 -06:00
2023-03-13 09:01:16 -05:00
systemd.timers.restic = {
timerConfig = {
OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "60m";
};
wantedBy = [ "multi-user.target" ];
2023-01-26 14:29:12 -06:00
};
};
2023-03-13 09:01:16 -05:00
};
};
2023-01-26 14:29:12 -06:00
};
}