summaryrefslogtreecommitdiffstats
path: root/krebs/3modules/backup.nix
diff options
context:
space:
mode:
authormakefu <github@syntax-fehler.de>2015-12-28 21:00:27 +0100
committermakefu <github@syntax-fehler.de>2015-12-28 21:00:27 +0100
commit72d821d96cc7c19134773d6a07317bf7d6c2465c (patch)
tree8d30a69e6feb2a3de26591a87f1acdaba7f5a758 /krebs/3modules/backup.nix
parent246116dabbe849e75612fbdb57b01696913ff27e (diff)
parent5a9ccbef0abe1f3acb16d716a2e1d7faa9bb0af1 (diff)
Merge remote-tracking branch 'cd/master'
Diffstat (limited to 'krebs/3modules/backup.nix')
-rw-r--r--krebs/3modules/backup.nix286
1 files changed, 286 insertions, 0 deletions
diff --git a/krebs/3modules/backup.nix b/krebs/3modules/backup.nix
new file mode 100644
index 000000000..01bb16a2b
--- /dev/null
+++ b/krebs/3modules/backup.nix
@@ -0,0 +1,286 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+ out = {
+ options.krebs.backup = api;
+ config = mkIf cfg.enable imp;
+ };
+
+ cfg = config.krebs.backup;
+
+ api = {
+ enable = mkEnableOption "krebs.backup" // { default = true; };
+ plans = mkOption {
+ default = {};
+ type = types.attrsOf (types.submodule ({
+ # TODO enable = mkEnableOption "TODO" // { default = true; };
+ options = {
+ method = mkOption {
+ type = types.enum ["pull" "push"];
+ };
+ name = mkOption {
+ type = types.str;
+ };
+ src = mkOption {
+ type = types.krebs.file-location;
+ };
+ dst = mkOption {
+ type = types.krebs.file-location;
+ };
+ startAt = mkOption {
+ type = types.str;
+ };
+ snapshots = mkOption {
+ type = types.attrsOf (types.submodule {
+ options = {
+ format = mkOption {
+ type = types.str; # TODO date's +FORMAT
+ };
+ retain = mkOption {
+ type = types.nullOr types.int;
+ default = null; # null = retain all snapshots
+ };
+ };
+ });
+ };
+ };
+ }));
+ };
+ };
+
+ 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}.pull";
+ value = makePullService plan;
+ })
+ //
+ flip mapAttrs' (filterAttrs (_:isPushSrc) cfg.plans) (name: plan: {
+ name = "backup.${name}.push";
+ 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";
+ };
+ startAt = plan.startAt;
+ };
+
+ makePullService = plan: assert isPullDst plan; {
+ path = service-path;
+ serviceConfig = {
+ ExecStart = pull plan;
+ Type = "oneshot";
+ };
+ startAt = plan.startAt;
+ };
+
+ push = plan: let
+ # We use 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 = 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 = 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_target=${shell.escape "root@${getFQDN plan.dst.host}"}
+ dst_path=${shell.escape plan.dst.path}
+ dst=$dst_target:$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" \
+ --rsync-path ${shell.escape
+ "mkdir -m 0700 -p ${shell.escape plan.dst.path} && rsync"} \
+ --link-dest="$dst_path/current" \
+ "$src/" \
+ "$dst/.partial"
+
+ exec ssh -F /dev/null \
+ -i "$identity" \
+ "$dst_target" \
+ -T \
+ env NOW="$NOW" /bin/sh < ${remote-snapshot}
+ EOF
+ '';
+
+ remote-snapshot = 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 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 = 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 = 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=${shell.escape "root@${getFQDN plan.src.host}:${plan.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" \
+ --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: 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)}
+ '';
+
+ # TODO getFQDN: admit hosts in other domains
+ getFQDN = host: "${host.name}.${config.krebs.search-domain}";
+
+ writeDash = name: text: pkgs.writeScript name ''
+ #! ${pkgs.dash}/bin/dash
+ ${text}
+ '';
+
+ writeDashBin = name: text: pkgs.writeTextFile {
+ executable = true;
+ destination = "/bin/${name}";
+ name = name;
+ text = ''
+ #! ${pkgs.dash}/bin/dash
+ ${text}
+ '';
+ };
+
+in out