Files
clan-core/clanServices/borgbackup/default.nix

315 lines
11 KiB
Nix

{ ... }:
{
_class = "clan.service";
manifest.name = "borgbackup";
manifest.description = "Efficient, deduplicating backup program with optional compression and secure encryption.";
manifest.categories = [ "System" ];
manifest.readme = builtins.readFile ./README.md;
# TODO: a client can only be in one instance, add constraint
roles.server = {
description = "A borgbackup server that stores the backups of clients.";
interface =
{ lib, ... }:
{
options.directory = lib.mkOption {
type = lib.types.str;
default = "/var/lib/borgbackup";
description = ''
The directory where the borgbackup repositories are stored.
'';
};
};
perInstance =
{
roles,
settings,
...
}:
{
nixosModule =
{
config,
...
}:
{
config.services.openssh.enable = true;
config.services.borgbackup.repos =
let
borgbackupIpMachinePath =
machine:
config.clan.core.settings.directory
+ "/vars/per-machine/${machine}/borgbackup/borgbackup.ssh.pub/value";
hosts = builtins.mapAttrs (machineName: _machineSettings: {
# name = "${instanceName}-${machineName}";
# value = {
path = "${settings.directory}/${machineName}";
authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machineName)) ];
# };
# }) machinesWithKey;
}) (roles.client.machines or { });
in
hosts;
};
};
};
roles.client = {
description = "A borgbackup client that backs up to all borgbackup server roles.";
interface =
{
lib,
...
}:
{
options.destinations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
default = name;
description = "the name of the backup job";
};
repo = lib.mkOption {
type = lib.types.str;
description = "the borgbackup repository to backup to";
};
rsh = lib.mkOption {
type = lib.types.str;
defaultText = "ssh -i \${config.clan.core.vars.generators.borgbackup.files.\"borgbackup.ssh\".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
description = "the rsh to use for the backup";
};
};
}
)
);
default = { };
description = ''
external destinations where the machine should be backuped to
'';
};
options.exclude = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ "*.pyc" ];
default = [ ];
description = ''
Directories/Files to exclude from the backup.
Use * as a wildcard.
'';
};
};
perInstance =
{
extendSettings,
roles,
...
}:
{
nixosModule =
{
config,
lib,
pkgs,
...
}:
let
settings = extendSettings {
# Adding default value with option merging, because it depends on
# generators, which we can reference here.
options.destinations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
rsh = lib.mkOption {
default = "ssh -i ${
config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes -o PasswordAuthentication=no";
};
};
}
);
};
};
in
{
config =
let
preBackupScript = ''
declare -A preCommandErrors
${lib.concatMapStringsSep "\n" (
state:
lib.optionalString (state.preBackupCommand != null) ''
echo "Running pre-backup command for ${state.name}"
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
preCommandErrors["${state.name}"]=1
fi
''
) (lib.attrValues config.clan.core.state)}
if [[ ''${#preCommandErrors[@]} -gt 0 ]]; then
echo "pre-backup commands failed for the following services:"
for state in "''${!preCommandErrors[@]}"; do
echo " $state"
done
exit 1
fi
'';
# The destinations from server.roles.machines.*
# name is the server, machine can only be in one instance
internalDestinations =
let
destinations = builtins.map (serverName: {
name = "${serverName}";
value = {
# inherit name;
name = "${serverName}";
repo = "borg@${serverName}:.";
# rsh = "";
rsh = "ssh -i ${
config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes";
};
}) (builtins.attrNames (roles.server.machines or { }));
in
(builtins.listToAttrs destinations);
# The destinations specified via roles.client.machines.*.settings.destinations.<name>
# name is the <name>
externalDestinations = lib.mapAttrs' (
name: dest: lib.nameValuePair name dest
) settings.destinations;
allDestinations =
lib.warnIf ((builtins.intersectAttrs externalDestinations internalDestinations) != { })
"You are overwriting an internalDestinations through an externalDestination configuration."
(internalDestinations // externalDestinations);
in
{
services.openssh.enable = true;
# Derived from the destinations
systemd.services = lib.mapAttrs' (
destName: _dest:
lib.nameValuePair "borgbackup-job-${destName}" {
# since borgbackup mounts the system read-only, we need to
# run in a ExecStartPre script, so we can generate
# additional files.
serviceConfig.ExecStartPre = [
''+${pkgs.writeShellScript "borgbackup-job-${destName}-pre-backup-commands" preBackupScript}''
];
}
) allDestinations;
services.borgbackup.jobs = lib.mapAttrs (_: dest: {
paths = lib.unique (
lib.flatten (map (state: state.folders) (lib.attrValues config.clan.core.state))
);
exclude = settings.exclude;
repo = dest.repo;
environment.BORG_RSH = dest.rsh;
compression = "auto,zstd";
startAt = "*-*-* 01:00:00";
persistentTimer = true;
encryption = {
mode = "repokey";
passCommand = "cat ${config.clan.core.vars.generators.borgbackup.files."borgbackup.repokey".path}";
};
prune.keep = {
within = "1d"; # Keep all archives from the last day
daily = 7;
weekly = 4;
monthly = 0;
};
}) allDestinations;
clan.core.vars.generators.borgbackup = {
files."borgbackup.ssh.pub".secret = false;
files."borgbackup.ssh" = { };
files."borgbackup.repokey" = { };
migrateFact = "borgbackup";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
pkgs.xkcdpass
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/borgbackup.ssh
xkcdpass -n 4 -d - > "$out"/borgbackup.repokey
'';
};
clan.core.backups.providers.borgbackup = {
list = "borgbackup-list";
create = "borgbackup-create";
restore = "borgbackup-restore";
};
environment.systemPackages = [
(pkgs.writeShellApplication {
name = "borgbackup-create";
runtimeInputs = [ config.systemd.package ];
text = ''
${lib.concatMapStringsSep "\n" (dest: ''
systemctl start borgbackup-job-${dest}
'') (lib.attrNames allDestinations)}
'';
})
(pkgs.writeShellApplication {
name = "borgbackup-list";
runtimeInputs = [ pkgs.jq ];
text = ''
(${
lib.concatMapStringsSep "\n" (
dest:
# we need yes here to skip the changed url verification
''echo y | /run/current-system/sw/bin/borg-job-${dest.name} list --json | jq '[.archives[] | {"name": ("${dest.name}::${dest.repo}::" + .name)}]' ''
) (lib.attrValues allDestinations)
}) | jq -s 'add // []'
'';
})
(pkgs.writeShellApplication {
name = "borgbackup-restore";
runtimeInputs = [ pkgs.gawk ];
text = ''
cd /
IFS=':' read -ra FOLDER <<< "''${FOLDERS-}"
job_name=$(echo "$NAME" | awk -F'::' '{print $1}')
backup_name=''${NAME#"$job_name"::}
if [[ ! -x /run/current-system/sw/bin/borg-job-"$job_name" ]]; then
echo "borg-job-$job_name not found: Backup name is invalid" >&2
exit 1
fi
echo y | /run/current-system/sw/bin/borg-job-"$job_name" extract "$backup_name" "''${FOLDER[@]}"
'';
})
];
};
};
};
};
}