localbackup: add mounthooks

This commit is contained in:
Jörg Thalheim
2024-03-20 10:55:27 +01:00
parent 64a3e36d6c
commit a66f215ea4

View File

@@ -17,12 +17,18 @@ let
cmd_logger ${pkgs.inetutils}/bin/logger cmd_logger ${pkgs.inetutils}/bin/logger
cmd_du ${pkgs.coreutils}/bin/du cmd_du ${pkgs.coreutils}/bin/du
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
${lib.optionalString (target.preExec != null) '' ${lib.optionalString (target.preBackupHook != null) ''
cmd_preexec ${pkgs.writeShellScript "preexec.sh" target.preExec} cmd_preexec ${pkgs.writeShellScript "preexec.sh" ''
set -efu -o pipefail
${target.preBackupHook}
''}
''} ''}
${lib.optionalString (target.postExec != null) '' ${lib.optionalString (target.postBackupHook != null) ''
cmd_postexec ${pkgs.writeShellScript "postexec.sh" target.postExec} cmd_postexec ${pkgs.writeShellScript "postexec.sh" ''
set -efu -o pipefail
${target.postBackupHook}
''}
''} ''}
retain snapshot ${builtins.toString config.clan.localbackup.snapshots} retain snapshot ${builtins.toString config.clan.localbackup.snapshots}
${lib.concatMapStringsSep "\n" (state: '' ${lib.concatMapStringsSep "\n" (state: ''
@@ -41,7 +47,7 @@ in
{ {
options = { options = {
name = lib.mkOption { name = lib.mkOption {
type = lib.types.str; type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
default = name; default = name;
description = "the name of the backup job"; description = "the name of the backup job";
}; };
@@ -50,16 +56,26 @@ in
description = "the directory to backup"; description = "the directory to backup";
}; };
mountpoint = lib.mkOption { mountpoint = lib.mkOption {
type = lib.types.nullOr (lib.types.strMatching "^[a-zA-Z0-9./_-]+$"); type = lib.types.nullOr lib.types.str;
default = null; default = null;
description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards"; description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
}; };
preExec = lib.mkOption { mountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = if config.mountpoint != null then "mount ${config.mountpoint}" else null;
description = "Shell commands to run before the directory is mounted";
};
unmountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = if config.mountpoint != null then "umount ${config.mountpoint}" else null;
description = "Shell commands to run after the directory is unmounted";
};
preBackupHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines; type = lib.types.nullOr lib.types.lines;
default = null; default = null;
description = "Shell commands to run before the backup"; description = "Shell commands to run before the backup";
}; };
postExec = lib.mkOption { postBackupHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines; type = lib.types.nullOr lib.types.lines;
default = null; default = null;
description = "Shell commands to run after the backup"; description = "Shell commands to run after the backup";
@@ -81,85 +97,101 @@ in
config = config =
let let
setupMount = mountHook = target: ''
mountpoint: if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
lib.optionalString (mountpoint != null) '' /run/current-system/sw/bin/localbackup-mount-${target.name}
mkdir -p ${lib.escapeShellArg mountpoint} fi
if mountpoint -q ${lib.escapeShellArg mountpoint}; then if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
umount ${lib.escapeShellArg mountpoint} trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
fi fi
mount ${lib.escapeShellArg mountpoint} '';
trap "umount ${lib.escapeShellArg mountpoint}" EXIT
'';
in in
lib.mkIf (cfg.targets != { }) { lib.mkIf (cfg.targets != { }) {
environment.systemPackages = [ environment.systemPackages =
(pkgs.writeShellScriptBin "localbackup-create" '' [
set -efu -o pipefail (pkgs.writeShellScriptBin "localbackup-create" ''
export PATH=${ set -efu -o pipefail
lib.makeBinPath [ export PATH=${
pkgs.rsnapshot lib.makeBinPath [
pkgs.coreutils pkgs.rsnapshot
pkgs.util-linux pkgs.coreutils
] pkgs.util-linux
} ]
${lib.concatMapStringsSep "\n" (target: '' }
( ${lib.concatMapStringsSep "\n" (target: ''
echo "Creating backup '${target.name}'"
${setupMount target.mountpoint}
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" sync
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" snapshot
)
'') (builtins.attrValues cfg.targets)}
'')
(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: ''
( (
${setupMount target.mountpoint} ${mountHook target}
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \ echo "Creating backup '${target.name}'"
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.mountpoint or ""}::" + .)}' rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" sync
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target (lib.attrValues config.clanCore.state))}" snapshot
) )
'') (builtins.attrValues cfg.targets) '') (builtins.attrValues cfg.targets)}
}) | jq -s . '')
'') (pkgs.writeShellScriptBin "localbackup-list" ''
(pkgs.writeShellScriptBin "localbackup-restore" '' set -efu -o pipefail
set -efu -o pipefail export PATH=${
export PATH=${ lib.makeBinPath [
lib.makeBinPath [ pkgs.jq
pkgs.rsync pkgs.findutils
pkgs.coreutils pkgs.coreutils
pkgs.util-linux pkgs.util-linux
pkgs.gawk ]
] }
} (${
mountpoint=$(awk -F'::' '{print $1}' <<< $NAME) lib.concatMapStringsSep "\n" (target: ''
backupname=''${NAME#$mountpoint::} (
${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
]
}
name=$(awk -F'::' '{print $1}' <<< $NAME)
backupname=''${NAME#$mountpoint::}
if [[ ! -z $backupname ]]; then if command -v localbackup-mount-$name; then
mkdir -p "$mountpoint" localbackup-mount-$name
if mountpoint -q "$mountpoint"; then fi
umount "$mountpoint" if command -v localbackup-unmount-$name; then
trap "localbackup-unmount-$name" EXIT
fi fi
mount "$mountpoint"
trap "umount $mountpoint" EXIT
fi
IFS=';' read -ra FOLDER <<< "$FOLDERS" if [[ ! -d $backupname ]]; then
for folder in "''${FOLDER[@]}"; do echo "No backup found $backupname"
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder" exit 1
done fi
'')
]; IFS=';' read -ra FOLDER <<< "$FOLDERS"
for folder in "''${FOLDER[@]}"; do
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
done
'')
]
++ (lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
set -efu -o pipefail
${target.mountHook}
''
) (lib.filterAttrs (_name: target: target.mountHook != null) cfg.targets))
++ lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
set -efu -o pipefail
${target.unmountHook}
''
) (lib.filterAttrs (_name: target: target.unmountHook != null) cfg.targets);
clanCore.backups.providers.localbackup = { clanCore.backups.providers.localbackup = {
# TODO list needs to run locally or on the remote machine # TODO list needs to run locally or on the remote machine