krebs.backup: DRY up push and pull

This commit is contained in:
tv 2016-02-07 16:21:58 +01:00
parent d01c6f9dbc
commit 071194c394

View File

@ -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?