clan-core/clanModules/localbackup/default.nix

243 lines
8.7 KiB
Nix
Raw Normal View History

2024-03-19 18:23:14 +00:00
{
config,
lib,
pkgs,
...
}:
let
cfg = config.clan.localbackup;
2024-06-05 16:37:31 +00:00
uniqueFolders = lib.unique (
2024-06-17 10:42:28 +00:00
lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
2024-06-05 16:37:31 +00:00
);
2024-05-31 14:36:37 +00:00
rsnapshotConfig = target: ''
2024-03-19 18:23:14 +00:00
config_version 1.2
2024-03-20 09:13:30 +00:00
snapshot_root ${target.directory}
2024-03-19 18:23:14 +00:00
sync_first 1
cmd_cp ${pkgs.coreutils}/bin/cp
cmd_rm ${pkgs.coreutils}/bin/rm
cmd_rsync ${pkgs.rsync}/bin/rsync
cmd_ssh ${pkgs.openssh}/bin/ssh
cmd_logger ${pkgs.inetutils}/bin/logger
cmd_du ${pkgs.coreutils}/bin/du
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
2024-03-20 09:13:30 +00:00
2024-03-20 09:55:27 +00:00
${lib.optionalString (target.postBackupHook != null) ''
cmd_postexec ${pkgs.writeShellScript "postexec.sh" ''
set -efu -o pipefail
${target.postBackupHook}
''}
2024-03-20 09:13:30 +00:00
''}
2024-03-19 18:23:14 +00:00
retain snapshot ${builtins.toString config.clan.localbackup.snapshots}
2024-05-31 14:36:37 +00:00
${lib.concatMapStringsSep "\n" (folder: ''
2024-06-05 16:37:31 +00:00
backup ${folder} ${config.networking.hostName}/
2024-05-31 14:36:37 +00:00
'') uniqueFolders}
2024-03-19 18:23:14 +00:00
'';
in
{
options.clan.localbackup = {
targets = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
2024-03-19 18:23:14 +00:00
{
options = {
name = lib.mkOption {
2024-03-20 09:55:27 +00:00
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
2024-03-19 18:23:14 +00:00
default = name;
description = "the name of the backup job";
};
directory = lib.mkOption {
type = lib.types.str;
description = "the directory to backup";
};
mountpoint = lib.mkOption {
2024-03-20 09:55:27 +00:00
type = lib.types.nullOr lib.types.str;
2024-03-19 18:23:14 +00:00
default = null;
description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
};
preMountHook = lib.mkOption {
2024-03-20 09:55:27 +00:00
type = lib.types.nullOr lib.types.lines;
default = null;
2024-03-20 09:55:27 +00:00
description = "Shell commands to run before the directory is mounted";
};
postMountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run after the directory is mounted";
};
preUnmountHook = lib.mkOption {
2024-03-20 09:55:27 +00:00
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run before the directory is unmounted";
};
postUnmountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
2024-03-20 09:55:27 +00:00
description = "Shell commands to run after the directory is unmounted";
};
preBackupHook = lib.mkOption {
2024-03-20 09:13:30 +00:00
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run before the backup";
};
2024-03-20 09:55:27 +00:00
postBackupHook = lib.mkOption {
2024-03-20 09:13:30 +00:00
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run after the backup";
};
2024-03-19 18:23:14 +00:00
};
}
)
);
default = { };
2024-03-19 18:23:14 +00:00
description = "List of directories where backups are stored";
};
snapshots = lib.mkOption {
type = lib.types.int;
default = 20;
description = "Number of snapshots to keep";
};
};
config =
let
2024-03-20 09:55:27 +00:00
mountHook = target: ''
if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
/run/current-system/sw/bin/localbackup-mount-${target.name}
fi
if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
fi
'';
2024-03-19 18:23:14 +00:00
in
lib.mkIf (cfg.targets != { }) {
2024-03-20 09:55:27 +00:00
environment.systemPackages =
[
(pkgs.writeShellScriptBin "localbackup-create" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.rsnapshot
pkgs.coreutils
pkgs.util-linux
]
}
${lib.concatMapStringsSep "\n" (target: ''
2024-06-05 16:37:31 +00:00
${mountHook target}
echo "Creating backup '${target.name}'"
${lib.optionalString (target.preBackupHook != null) ''
(
${target.preBackupHook}
)
''}
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
2024-06-05 16:37:31 +00:00
preCommandErrors["${state.name}"]=1
fi
''
2024-06-17 10:42:28 +00:00
) (builtins.attrValues config.clan.core.state)}
2024-06-05 16:37:31 +00:00
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
'') (builtins.attrValues cfg.targets)}'')
2024-03-20 09:55:27 +00:00
(pkgs.writeShellScriptBin "localbackup-list" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.jq
pkgs.findutils
pkgs.coreutils
pkgs.util-linux
]
}
(${
lib.concatMapStringsSep "\n" (target: ''
(
${mountHook target}
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
)
'') (builtins.attrValues cfg.targets)
}) | jq -s .
'')
(pkgs.writeShellScriptBin "localbackup-restore" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.rsync
pkgs.coreutils
pkgs.util-linux
pkgs.gawk
]
}
2024-06-05 16:37:31 +00:00
if [[ "''${NAME:-}" == "" ]]; then
echo "No backup name given via NAME environment variable"
exit 1
fi
if [[ "''${FOLDERS:-}" == "" ]]; then
echo "No folders given via FOLDERS environment variable"
exit 1
fi
2024-03-20 09:55:27 +00:00
name=$(awk -F'::' '{print $1}' <<< $NAME)
backupname=''${NAME#$name::}
2024-03-20 09:55:27 +00:00
if command -v localbackup-mount-$name; then
localbackup-mount-$name
fi
if command -v localbackup-unmount-$name; then
trap "localbackup-unmount-$name" EXIT
fi
2024-03-19 18:23:14 +00:00
2024-03-20 09:55:27 +00:00
if [[ ! -d $backupname ]]; then
echo "No backup found $backupname"
exit 1
2024-03-20 09:13:30 +00:00
fi
2024-03-19 18:23:14 +00:00
2024-06-05 16:37:31 +00:00
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
2024-03-20 09:55:27 +00:00
for folder in "''${FOLDER[@]}"; do
2024-06-05 16:37:31 +00:00
mkdir -p "$folder"
2024-03-20 09:55:27 +00:00
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
done
'')
]
++ (lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
set -efu -o pipefail
${lib.optionalString (target.preMountHook != null) target.preMountHook}
${lib.optionalString (target.mountpoint != null) ''
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
2024-03-26 14:53:13 +00:00
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
fi
''}
${lib.optionalString (target.postMountHook != null) target.postMountHook}
2024-03-20 09:55:27 +00:00
''
) cfg.targets)
2024-03-20 09:55:27 +00:00
++ lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
set -efu -o pipefail
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
${lib.optionalString (
target.mountpoint != null
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
2024-03-20 09:55:27 +00:00
''
) cfg.targets;
2024-03-19 18:23:14 +00:00
2024-06-17 10:42:28 +00:00
clan.core.backups.providers.localbackup = {
2024-03-19 18:23:14 +00:00
# TODO list needs to run locally or on the remote machine
list = "localbackup-list";
create = "localbackup-create";
restore = "localbackup-restore";
};
};
}