krebs.backup: DRY up push and pull
This commit is contained in:
parent
d01c6f9dbc
commit
071194c394
@ -58,228 +58,145 @@ let
|
|||||||
};
|
};
|
||||||
|
|
||||||
imp = {
|
imp = {
|
||||||
users.groups.backup.gid = genid "backup";
|
|
||||||
users.users = {}
|
|
||||||
// {
|
|
||||||
root.openssh.authorizedKeys.keys =
|
|
||||||
map (plan: plan.dst.host.ssh.pubkey)
|
|
||||||
(filter isPullSrc (attrValues cfg.plans))
|
|
||||||
++
|
|
||||||
map (plan: plan.src.host.ssh.pubkey)
|
|
||||||
(filter isPushDst (attrValues cfg.plans))
|
|
||||||
;
|
|
||||||
}
|
|
||||||
;
|
|
||||||
systemd.services =
|
systemd.services =
|
||||||
flip mapAttrs' (filterAttrs (_:isPullDst) cfg.plans) (name: plan: {
|
listToAttrs (map (plan: nameValuePair "backup.${plan.name}" {
|
||||||
name = "backup.${name}";
|
# TODO if there is plan.user, then use its privkey
|
||||||
value = makePullService plan;
|
# TODO push destination users need a similar path
|
||||||
})
|
path = with pkgs; [
|
||||||
//
|
coreutils
|
||||||
flip mapAttrs' (filterAttrs (_:isPushSrc) cfg.plans) (name: plan: {
|
gnused
|
||||||
name = "backup.${name}";
|
openssh
|
||||||
value = makePushService plan;
|
rsync
|
||||||
})
|
utillinux
|
||||||
;
|
];
|
||||||
|
serviceConfig = rec {
|
||||||
|
ExecStart = start plan;
|
||||||
|
SyslogIdentifier = ExecStart.name;
|
||||||
|
Type = "oneshot";
|
||||||
|
};
|
||||||
|
} // optionalAttrs (plan.startAt != null) {
|
||||||
|
inherit (plan) startAt;
|
||||||
|
}) (filter (plan: build-host-is "pull" "dst" plan ||
|
||||||
|
build-host-is "push" "src" plan)
|
||||||
|
(attrValues cfg.plans)));
|
||||||
|
|
||||||
|
users.groups.backup.gid = genid "backup";
|
||||||
|
users.users.root.openssh.authorizedKeys.keys =
|
||||||
|
map (plan: getAttr plan.method {
|
||||||
|
push = plan.src.host.ssh.pubkey;
|
||||||
|
pull = plan.dst.host.ssh.pubkey;
|
||||||
|
}) (filter (plan: build-host-is "pull" "src" plan ||
|
||||||
|
build-host-is "push" "dst" plan)
|
||||||
|
(attrValues cfg.plans));
|
||||||
};
|
};
|
||||||
|
|
||||||
isPushSrc = plan:
|
build-host-is = method: side: plan:
|
||||||
plan.method == "push" &&
|
plan.method == method &&
|
||||||
plan.src.host.name == config.krebs.build.host.name;
|
config.krebs.build.host.name == plan.${side}.host.name;
|
||||||
|
|
||||||
isPullSrc = plan:
|
start = plan: pkgs.writeDash "backup.${plan.name}" ''
|
||||||
plan.method == "pull" &&
|
set -efu
|
||||||
plan.src.host.name == config.krebs.build.host.name;
|
${getAttr plan.method {
|
||||||
|
push = ''
|
||||||
isPushDst = plan:
|
identity=${shell.escape plan.src.host.ssh.privkey.path}
|
||||||
plan.method == "push" &&
|
src_path=${shell.escape plan.src.path}
|
||||||
plan.dst.host.name == config.krebs.build.host.name;
|
src=$src_path
|
||||||
|
dst_user=root
|
||||||
isPullDst = plan:
|
dst_host=$(${fastest-address plan.dst.host})
|
||||||
plan.method == "pull" &&
|
dst_port=$(${network-ssh-port plan.dst.host "$dst_host"})
|
||||||
plan.dst.host.name == config.krebs.build.host.name;
|
dst_path=${shell.escape plan.dst.path}
|
||||||
|
dst=$dst_user@$dst_host:$dst_path
|
||||||
# TODO push destination needs this in the dst.user's PATH
|
echo "update snapshot: current; $src -> $dst" >&2
|
||||||
service-path = [
|
dst_shell() {
|
||||||
pkgs.coreutils
|
exec ssh -F /dev/null \
|
||||||
pkgs.gnused
|
-i "$identity" \
|
||||||
pkgs.openssh
|
''${dst_port:+-p $dst_port} \
|
||||||
pkgs.rsync
|
"$dst_user@$dst_host" \
|
||||||
pkgs.utillinux
|
-T "$with_dst_path_lock_script"
|
||||||
];
|
}
|
||||||
|
'';
|
||||||
# TODO if there is plan.user, then use its privkey
|
pull = ''
|
||||||
makePushService = plan: assert isPushSrc plan; {
|
identity=${shell.escape plan.dst.host.ssh.privkey.path}
|
||||||
path = service-path;
|
src_user=root
|
||||||
serviceConfig = {
|
src_host=$(${fastest-address plan.src.host})
|
||||||
ExecStart = push plan;
|
src_port=$(${network-ssh-port plan.src.host "$src_host"})
|
||||||
Type = "oneshot";
|
src_path=${shell.escape plan.src.path}
|
||||||
};
|
src=$src_user@$src_host:$src_path
|
||||||
} // optionalAttrs (plan.startAt != null) {
|
dst_path=${shell.escape plan.dst.path}
|
||||||
inherit (plan) startAt;
|
dst=$dst_path
|
||||||
};
|
echo "update snapshot: current; $dst <- $src" >&2
|
||||||
|
dst_shell() {
|
||||||
makePullService = plan: assert isPullDst plan; {
|
eval "$with_dst_path_lock_script"
|
||||||
path = service-path;
|
}
|
||||||
serviceConfig = {
|
'';
|
||||||
ExecStart = pull plan;
|
}}
|
||||||
Type = "oneshot";
|
# Note that this only works because we trust date +%s to produce output
|
||||||
};
|
# that doesn't need quoting when used to generate a command string.
|
||||||
} // optionalAttrs (plan.startAt != null) {
|
# TODO relax this requirement by selectively allowing to inject variables
|
||||||
inherit (plan) startAt;
|
# e.g.: ''${shell.quote "exec env NOW=''${shell.unquote "$NOW"} ..."}
|
||||||
};
|
with_dst_path_lock_script="exec env start_date=$(date +%s) "${shell.escape
|
||||||
|
"flock -n ${shell.escape plan.dst.path} /bin/sh"
|
||||||
push = plan: let
|
}
|
||||||
# We use pkgs.writeDashBin and return the absolute path so systemd will
|
rsync >&2 \
|
||||||
# produce nice names in the log, i.e. without the Nix store hash.
|
-aAXF --delete \
|
||||||
out = "${main}/bin/${main.name}";
|
-e "ssh -F /dev/null -i $identity ''${dst_port:+-p $dst_port}" \
|
||||||
|
--rsync-path ${shell.escape (concatStringsSep " && " [
|
||||||
main = pkgs.writeDashBin "backup.${plan.name}.push" ''
|
"mkdir -m 0700 -p ${shell.escape plan.dst.path}/current"
|
||||||
|
"exec flock -n ${shell.escape plan.dst.path} rsync"
|
||||||
|
])} \
|
||||||
|
--link-dest="$dst_path/current" \
|
||||||
|
"$src/" \
|
||||||
|
"$dst/.partial"
|
||||||
|
dst_shell < ${toFile "backup.${plan.name}.take-snapshots" ''
|
||||||
set -efu
|
set -efu
|
||||||
|
: $start_date
|
||||||
|
|
||||||
dst=${shell.escape plan.dst.path}
|
dst=${shell.escape plan.dst.path}
|
||||||
|
|
||||||
mkdir -m 0700 -p "$dst"
|
|
||||||
exec flock -n "$dst" ${critical-section}
|
|
||||||
'';
|
|
||||||
|
|
||||||
critical-section = pkgs.writeDash "backup.${plan.name}.push.critical-section" ''
|
|
||||||
# TODO check if there is a previous
|
|
||||||
set -efu
|
|
||||||
identity=${shell.escape plan.src.host.ssh.privkey.path}
|
|
||||||
src=${shell.escape plan.src.path}
|
|
||||||
dst_user=root
|
|
||||||
dst_host=$(${fastest-address plan.dst.host})
|
|
||||||
dst_port=$(${network-ssh-port plan.dst.host "$dst_host"})
|
|
||||||
dst_path=${shell.escape plan.dst.path}
|
|
||||||
dst=$dst_user@$dst_host:$dst_path
|
|
||||||
|
|
||||||
# Export NOW so runtime of rsync doesn't influence snapshot naming.
|
|
||||||
export NOW
|
|
||||||
NOW=$(date +%s)
|
|
||||||
|
|
||||||
echo >&2 "update snapshot: current; $src -> $dst"
|
|
||||||
rsync >&2 \
|
|
||||||
-aAXF --delete \
|
|
||||||
-e "ssh -F /dev/null -i $identity ''${dst_port:+-p $dst_port}" \
|
|
||||||
--rsync-path ${shell.escape
|
|
||||||
"mkdir -m 0700 -p ${shell.escape plan.dst.path}/current && rsync"} \
|
|
||||||
--link-dest="$dst_path/current" \
|
|
||||||
"$src/" \
|
|
||||||
"$dst/.partial"
|
|
||||||
|
|
||||||
exec ssh -F /dev/null \
|
|
||||||
-i "$identity" \
|
|
||||||
''${dst_port:+-p $dst_port} \
|
|
||||||
"$dst_user@$dst_host" \
|
|
||||||
-T \
|
|
||||||
env NOW="$NOW" /bin/sh < ${remote-snapshot}
|
|
||||||
'';
|
|
||||||
|
|
||||||
remote-snapshot = pkgs.writeDash "backup.${plan.name}.push.remote-snapshot" ''
|
|
||||||
set -efu
|
|
||||||
dst=${shell.escape plan.dst.path}
|
|
||||||
|
|
||||||
if test -e "$dst/current"; then
|
|
||||||
mv "$dst/current" "$dst/.previous"
|
|
||||||
fi
|
|
||||||
mv "$dst/.partial" "$dst/current"
|
|
||||||
rm -fR "$dst/.previous"
|
|
||||||
echo >&2
|
|
||||||
|
|
||||||
(${(take-snapshots plan).text})
|
|
||||||
'';
|
|
||||||
|
|
||||||
in out;
|
|
||||||
|
|
||||||
# TODO admit plan.dst.user and its ssh identity
|
|
||||||
pull = plan: let
|
|
||||||
# We use pkgs.writeDashBin and return the absolute path so systemd will
|
|
||||||
# produce nice names in the log, i.e. without the Nix store hash.
|
|
||||||
out = "${main}/bin/${main.name}";
|
|
||||||
|
|
||||||
main = pkgs.writeDashBin "backup.${plan.name}.pull" ''
|
|
||||||
set -efu
|
|
||||||
dst=${shell.escape plan.dst.path}
|
|
||||||
|
|
||||||
mkdir -m 0700 -p "$dst"
|
|
||||||
exec flock -n "$dst" ${critical-section}
|
|
||||||
'';
|
|
||||||
|
|
||||||
critical-section = pkgs.writeDash "backup.${plan.name}.pull.critical-section" ''
|
|
||||||
# TODO check if there is a previous
|
|
||||||
set -efu
|
|
||||||
identity=${shell.escape plan.dst.host.ssh.privkey.path}
|
|
||||||
src_user=root
|
|
||||||
src_host=$(${fastest-address plan.src.host})
|
|
||||||
src_port=$(${network-ssh-port plan.src.host "$src_host"})
|
|
||||||
src_path=${shell.escape plan.src.path}
|
|
||||||
src=$src_user@$src_host:$src_path
|
|
||||||
dst=${shell.escape plan.dst.path}
|
|
||||||
|
|
||||||
# Export NOW so runtime of rsync doesn't influence snapshot naming.
|
|
||||||
export NOW
|
|
||||||
NOW=$(date +%s)
|
|
||||||
|
|
||||||
echo >&2 "update snapshot: current; $dst <- $src"
|
|
||||||
mkdir -m 0700 -p ${shell.escape plan.dst.path}
|
|
||||||
rsync >&2 \
|
|
||||||
-aAXF --delete \
|
|
||||||
-e "ssh -F /dev/null -i $identity ''${src_port:+-p $src_port}" \
|
|
||||||
--link-dest="$dst/current" \
|
|
||||||
"$src/" \
|
|
||||||
"$dst/.partial"
|
|
||||||
mv "$dst/current" "$dst/.previous"
|
mv "$dst/current" "$dst/.previous"
|
||||||
mv "$dst/.partial" "$dst/current"
|
mv "$dst/.partial" "$dst/current"
|
||||||
rm -fR "$dst/.previous"
|
rm -fR "$dst/.previous"
|
||||||
echo >&2
|
echo >&2
|
||||||
|
|
||||||
exec ${take-snapshots plan}
|
snapshot() {(
|
||||||
'';
|
: $ns $format $retain
|
||||||
in out;
|
name=$(date --date="@$start_date" +"$format")
|
||||||
|
if ! test -e "$dst/$ns/$name"; then
|
||||||
|
echo >&2 "create snapshot: $ns/$name"
|
||||||
|
mkdir -m 0700 -p "$dst/$ns"
|
||||||
|
rsync >&2 \
|
||||||
|
-aAXF --delete \
|
||||||
|
--link-dest="$dst/current" \
|
||||||
|
"$dst/current/" \
|
||||||
|
"$dst/$ns/.partial.$name"
|
||||||
|
mv "$dst/$ns/.partial.$name" "$dst/$ns/$name"
|
||||||
|
echo >&2
|
||||||
|
fi
|
||||||
|
case $retain in
|
||||||
|
([0-9]*)
|
||||||
|
delete_from=$(($retain + 1))
|
||||||
|
ls -r "$dst/$ns" \
|
||||||
|
| sed -n "$delete_from,\$p" \
|
||||||
|
| while read old_name; do
|
||||||
|
echo >&2 "delete snapshot: $ns/$old_name"
|
||||||
|
rm -fR "$dst/$ns/$old_name"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
(ALL)
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
)}
|
||||||
|
|
||||||
take-snapshots = plan: pkgs.writeDash "backup.${plan.name}.take-snapshots" ''
|
${concatStringsSep "\n" (mapAttrsToList (ns: { format, retain, ... }:
|
||||||
set -efu
|
toString (map shell.escape [
|
||||||
NOW=''${NOW-$(date +%s)}
|
"ns=${ns}"
|
||||||
dst=${shell.escape plan.dst.path}
|
"format=${format}"
|
||||||
|
"retain=${if retain == null then "ALL" else toString retain}"
|
||||||
snapshot() {(
|
"snapshot"
|
||||||
: $ns $format $retain
|
]))
|
||||||
name=$(date --date="@$NOW" +"$format")
|
plan.snapshots)}
|
||||||
if ! test -e "$dst/$ns/$name"; then
|
''}
|
||||||
echo >&2 "create snapshot: $ns/$name"
|
|
||||||
mkdir -m 0700 -p "$dst/$ns"
|
|
||||||
rsync >&2 \
|
|
||||||
-aAXF --delete \
|
|
||||||
--link-dest="$dst/current" \
|
|
||||||
"$dst/current/" \
|
|
||||||
"$dst/$ns/.partial.$name"
|
|
||||||
mv "$dst/$ns/.partial.$name" "$dst/$ns/$name"
|
|
||||||
echo >&2
|
|
||||||
fi
|
|
||||||
case $retain in
|
|
||||||
([0-9]*)
|
|
||||||
delete_from=$(($retain + 1))
|
|
||||||
ls -r "$dst/$ns" \
|
|
||||||
| sed -n "$delete_from,\$p" \
|
|
||||||
| while read old_name; do
|
|
||||||
echo >&2 "delete snapshot: $ns/$old_name"
|
|
||||||
rm -fR "$dst/$ns/$old_name"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
(ALL)
|
|
||||||
:
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
)}
|
|
||||||
|
|
||||||
${concatStringsSep "\n" (mapAttrsToList (ns: { format, retain ? null, ... }:
|
|
||||||
toString (map shell.escape [
|
|
||||||
"ns=${ns}"
|
|
||||||
"format=${format}"
|
|
||||||
"retain=${if retain == null then "ALL" else toString retain}"
|
|
||||||
"snapshot"
|
|
||||||
]))
|
|
||||||
plan.snapshots)}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# XXX Is one ping enough to determine fastest address?
|
# XXX Is one ping enough to determine fastest address?
|
||||||
|
Loading…
Reference in New Issue
Block a user