Simon Bruder
10b8d432d5
This applies the REUSE specification to the repository, so the licensing information can be tracked for every file individually.
163 lines
4.7 KiB
Nix
163 lines
4.7 KiB
Nix
# SPDX-FileCopyrightText: 2020-2023 Simon Bruder <simon@sbruder.de>
|
||
#
|
||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
{ pkgs, config, lib, ... }:
|
||
let
|
||
cfg = config.sbruder.restic.system;
|
||
|
||
sftpTarget = "u313368-sub4@u313368-sub4.your-storagebox.de";
|
||
sftpPort = 23;
|
||
repository = "sftp://${sftpTarget}:${toString sftpPort}/personal";
|
||
excludes = [
|
||
# Caches
|
||
"/home/*/Downloads/"
|
||
"/home/*/.cache/"
|
||
"/home/*/**/cache/"
|
||
"/home/*/.local/share/Trash" # some gui applications use it
|
||
"/root/.cache"
|
||
"/data/cache/"
|
||
"/var/cache/"
|
||
|
||
# Rust
|
||
"/home/*/.rustup/toolchains/"
|
||
"/home/*/.cargo"
|
||
|
||
# Misc
|
||
"/home/*/mount"
|
||
"/home/*/mounts"
|
||
|
||
# Docker (state should be kept somewhere else)
|
||
"/var/lib/docker/"
|
||
|
||
# Static configuration (generated from this repository)
|
||
"/etc/static/"
|
||
] ++ cfg.extraExcludes;
|
||
excludesFile = pkgs.writeText "excludes.txt" (lib.concatStringsSep "\n" excludes);
|
||
|
||
# script to use restic as user without dealing with authentication
|
||
authScript = pkgs.writeShellScriptBin "restic-auth" ''
|
||
${pkgs.restic}/bin/restic \
|
||
--password-command="pass data/backup/restic-nixos" \
|
||
--repo "${repository}" \
|
||
$@
|
||
'';
|
||
|
||
# HACK: NixOS’ nftables implementation runs nft -c inside the build sandbox,
|
||
# where the target host’s cgroups are not available,
|
||
# and therefore fails.
|
||
# This is there to allow my home router to put backup traffic into the right qdisc,
|
||
# as the ip address and port are also used for other things.
|
||
# This is somewhat of an abuse of the DSCP mark.
|
||
qosRules = pkgs.writeText "restic-qos.nft" ''
|
||
table inet restic
|
||
delete table inet restic
|
||
|
||
table inet restic {
|
||
chain output {
|
||
type filter hook output priority mangle
|
||
ip version 4 socket cgroupv2 level 1 "restic.slice" ip dscp set af12 return
|
||
ip6 version 6 socket cgroupv2 level 1 "restic.slice" ip6 dscp set af12 return
|
||
}
|
||
}
|
||
'';
|
||
in
|
||
{
|
||
options.sbruder.restic.system = {
|
||
enable = lib.mkEnableOption "restic";
|
||
timerConfig = lib.mkOption {
|
||
type = with lib.types; attrsOf str;
|
||
default = {
|
||
OnCalendar = "18:00";
|
||
RandomizedDelaySec = "2h";
|
||
};
|
||
};
|
||
extraPaths = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
example = [ "/data" ];
|
||
};
|
||
extraExcludes = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
};
|
||
uploadLimit = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.int;
|
||
default = null;
|
||
};
|
||
qos = (lib.mkEnableOption "QoS marking (DSCP AF12) of outgoing packets") // { default = !(lib.isNull cfg.uploadLimit); };
|
||
prune = lib.mkEnableOption "pruning";
|
||
};
|
||
|
||
config = lib.mkIf cfg.enable {
|
||
sops.secrets = {
|
||
restic-password = { };
|
||
restic-repository = { };
|
||
} // lib.optionalAttrs cfg.prune {
|
||
restic-ssh-key = {
|
||
sopsFile = ../../machines/${config.networking.hostName}/secrets.yaml;
|
||
};
|
||
};
|
||
|
||
services.restic.backups.system = {
|
||
inherit (cfg) timerConfig;
|
||
repositoryFile = config.sops.secrets.restic-repository.path;
|
||
passwordFile = config.sops.secrets.restic-password.path;
|
||
paths = [
|
||
"/etc"
|
||
"/home"
|
||
"/root"
|
||
"/srv"
|
||
"/var"
|
||
] ++ cfg.extraPaths;
|
||
extraBackupArgs = [
|
||
"--compression auto"
|
||
"--exclude-caches"
|
||
"--exclude-file=${excludesFile}"
|
||
"--tag system"
|
||
"--verbose"
|
||
] ++ lib.optional (cfg.uploadLimit != null) "--limit-upload=${toString cfg.uploadLimit}";
|
||
backupPrepareCommand = ''
|
||
${pkgs.nftables}/bin/nft -f ${qosRules}
|
||
'';
|
||
backupCleanupCommand = ''
|
||
${pkgs.nftables}/bin/nft delete table inet restic
|
||
'';
|
||
};
|
||
|
||
systemd.services."restic-backups-system".serviceConfig = {
|
||
"Nice" = 10;
|
||
"IOSchedulingClass" = "best-effort";
|
||
"IOSchedulingPriority" = 7;
|
||
Slice = "restic.slice";
|
||
};
|
||
|
||
services.restic.backups.system-prune = lib.mkIf cfg.prune {
|
||
inherit repository;
|
||
passwordFile = config.sops.secrets.restic-password.path;
|
||
timerConfig = {
|
||
OnCalendar = "*-1/2-07 03:00:00";
|
||
RandomizedDelaySec = "4h";
|
||
};
|
||
paths = [ ];
|
||
extraOptions = [
|
||
"-o"
|
||
"sftp.command='ssh -i ${config.sops.secrets.restic-ssh-key.path} -p ${toString sftpPort} ${sftpTarget} -s sftp'"
|
||
];
|
||
pruneOpts = [
|
||
"--compression auto"
|
||
"--keep-daily 7"
|
||
"--keep-monthly 12"
|
||
"--keep-weekly 5"
|
||
"--keep-yearly 10"
|
||
"--tag system"
|
||
"--verbose"
|
||
] ++ lib.optional (cfg.uploadLimit != null) "--limit-upload=${toString cfg.uploadLimit}";
|
||
};
|
||
|
||
environment.systemPackages = [
|
||
authScript
|
||
];
|
||
};
|
||
}
|