summaryrefslogtreecommitdiffstats
path: root/krebs/3modules/backup.nix
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2016-02-07 16:21:58 +0100
committertv <tv@krebsco.de>2016-02-07 22:20:38 +0100
commit071194c3946b325103311f5c6528fba30580f125 (patch)
tree122095a5e180e0c4eff9ada494b3735f835c8da0 /krebs/3modules/backup.nix
parentd01c6f9dbcd2d1d7ccccff5fc8c41ffb53d04a42 (diff)
krebs.backup: DRY up push and pull
Diffstat (limited to 'krebs/3modules/backup.nix')
-rw-r--r--krebs/3modules/backup.nix333
1 files changed, 125 insertions, 208 deletions
diff --git a/krebs/3modules/backup.nix b/krebs/3modules/backup.nix
index 935370d9..fa5b0cfd 100644
--- a/krebs/3modules/backup.nix
+++ b/krebs/3modules/backup.nix
@@ -58,228 +58,145 @@ let
};
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 =
- flip mapAttrs' (filterAttrs (_:isPullDst) cfg.plans) (name: plan: {
- name = "backup.${name}";
- value = makePullService plan;
- })
- //
- flip mapAttrs' (filterAttrs (_:isPushSrc) cfg.plans) (name: plan: {
- name = "backup.${name}";
- value = makePushService plan;
- })
- ;
- };
-
- isPushSrc = plan:
- plan.method == "push" &&
- plan.src.host.name == config.krebs.build.host.name;
-
- isPullSrc = plan:
- plan.method == "pull" &&
- plan.src.host.name == config.krebs.build.host.name;
-
- isPushDst = plan:
- plan.method == "push" &&
- plan.dst.host.name == config.krebs.build.host.name;
-
- isPullDst = plan:
- plan.method == "pull" &&
- plan.dst.host.name == config.krebs.build.host.name;
-
- # TODO push destination needs this in the dst.user's PATH
- service-path = [
- pkgs.coreutils
- pkgs.gnused
- pkgs.openssh
- pkgs.rsync
- pkgs.utillinux
- ];
-
- # TODO if there is plan.user, then use its privkey
- makePushService = plan: assert isPushSrc plan; {
- path = service-path;
- serviceConfig = {
- ExecStart = push plan;
- Type = "oneshot";
- };
- } // optionalAttrs (plan.startAt != null) {
- inherit (plan) startAt;
- };
+ listToAttrs (map (plan: nameValuePair "backup.${plan.name}" {
+ # TODO if there is plan.user, then use its privkey
+ # TODO push destination users need a similar path
+ path = with pkgs; [
+ coreutils
+ gnused
+ openssh
+ 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)));
- makePullService = plan: assert isPullDst plan; {
- path = service-path;
- serviceConfig = {
- ExecStart = pull plan;
- Type = "oneshot";
- };
- } // optionalAttrs (plan.startAt != null) {
- inherit (plan) startAt;
+ 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));
};
- push = 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}.push" ''
- 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}.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"
+ build-host-is = method: side: plan:
+ plan.method == method &&
+ config.krebs.build.host.name == plan.${side}.host.name;
- 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" ''
+ start = plan: pkgs.writeDash "backup.${plan.name}" ''
+ set -efu
+ ${getAttr plan.method {
+ push = ''
+ identity=${shell.escape plan.src.host.ssh.privkey.path}
+ src_path=${shell.escape plan.src.path}
+ src=$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
+ echo "update snapshot: current; $src -> $dst" >&2
+ dst_shell() {
+ exec ssh -F /dev/null \
+ -i "$identity" \
+ ''${dst_port:+-p $dst_port} \
+ "$dst_user@$dst_host" \
+ -T "$with_dst_path_lock_script"
+ }
+ '';
+ pull = ''
+ 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_path=${shell.escape plan.dst.path}
+ dst=$dst_path
+ echo "update snapshot: current; $dst <- $src" >&2
+ dst_shell() {
+ eval "$with_dst_path_lock_script"
+ }
+ '';
+ }}
+ # 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.
+ # TODO relax this requirement by selectively allowing to inject variables
+ # 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"
+ }
+ rsync >&2 \
+ -aAXF --delete \
+ -e "ssh -F /dev/null -i $identity ''${dst_port:+-p $dst_port}" \
+ --rsync-path ${shell.escape (concatStringsSep " && " [
+ "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
- dst=${shell.escape plan.dst.path}
+ : $start_date
- 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/.partial" "$dst/current"
rm -fR "$dst/.previous"
echo >&2
- exec ${take-snapshots plan}
- '';
- in out;
-
- take-snapshots = plan: pkgs.writeDash "backup.${plan.name}.take-snapshots" ''
- set -efu
- NOW=''${NOW-$(date +%s)}
- dst=${shell.escape plan.dst.path}
-
- snapshot() {(
- : $ns $format $retain
- name=$(date --date="@$NOW" +"$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
- )}
-
- ${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)}
+ snapshot() {(
+ : $ns $format $retain
+ 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
+ )}
+
+ ${concatStringsSep "\n" (mapAttrsToList (ns: { format, retain, ... }:
+ 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?