#!/usr/bin/env python3 """Part A, second half — on controller v0.268.0. vikunja's volumes are UNLABELLED (restored under v0.267.0, liveA1). (e) a failing update (real move 2.5.0 -> 2.6.0 + a probe port the app does not answer): the undo must copy BOTH volumes and seeds A and B must read back. (f) a restore under v0.268.0: the recreated volumes carry compose's labels. (g) seed C, the same failing update again: undone, both copied, A B C read back. (h) remove: volumes_removed names both, no applied-compose.yml / applied-meta/ left (R-651).""" import json, re, sys, time sys.path.insert(0, ".") import walk as w import fixtures as fx S = json.load(open("../partA/state.json")) F, SUB = fx.Vikunja(), S["sub"] out = {"controller": w.guest("cat /etc/felhom-controller-image").strip()} w.login() def drill(edit, msg): import fcntl with open(f"{w.SC}/drill.lock", "w") as lk: fcntl.flock(lk, fcntl.LOCK_EX) w.sh(["git", "-C", w.DRILL, "pull", "-q", "--rebase", "origin", "main"], timeout=120) edit() w.sh(["git", "-C", w.DRILL, "commit", "-qam", "LIVE-A " + msg]) r = w.sh(["git", "-C", w.DRILL, "push", "-q", "origin", "main"], timeout=120) h = w.sh(["git", "-C", w.DRILL, "rev-parse", "--short=12", "HEAD"]).stdout.strip() w.say(f"drill {h}: {msg} push rc={r.returncode}") return h def set_vik(ref, port): p = f"{w.DRILL}/templates/vikunja/docker-compose.yml" s = re.sub(r"image: vikunja/vikunja:\S+", f"image: vikunja/vikunja:{ref}", open(p).read()) open(p, "w").write(s) fy = f"{w.DRILL}/templates/vikunja/.felhom.yml" f = open(fy).read() f2 = re.sub(r"(healthcheck:\n(?:.*\n){0,8}?\s+port: )\d+", lambda m: m.group(1) + str(port), f, count=1) assert f2 != f or str(port) in f, "probe port not found" open(fy, "w").write(f2) def labels(): return w.guest("for v in $(docker volume ls -q | grep '^vikunja_'); do echo \"$v $(docker volume inspect $v --format '{{json .Labels}}')\"; done") def ctl_log(since): return w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'vikunja' | grep -E 'undo copy will hold|carry no compose label|ladder|copied|UNDONE|UNDO|RestoreStack|Restoring Docker volume|was not created|volume .* already exists|created WITHOUT' | tail -30") def readall(tag, seeds): w.wait_app(SUB, "/", tries=60, delay=3) r = {k: bool(F.verify(w, SUB, v, w.say)) for k, v in seeds.items()} w.say(f"READBACK {tag}: {r}") return r PORT_REAL = int(re.search(r"port: (\d+)", open(f"{w.DRILL}/templates/vikunja/.felhom.yml").read().split("healthcheck:")[1]).group(1)) out["probe_port_real"] = PORT_REAL out["labels_before"] = labels() w.say("labels before (unlabelled after the v0.267.0 restore):\n" + out["labels_before"]) # (e) t0 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) out["drill_e"] = drill(lambda: set_vik("2.6.0", 8999), "vikunja 2.6.0 + probe 8999 (the failing edge)") out["badge_e"] = w.sync_rescan("vikunja", "vikunja/vikunja:2.6.0") out["press_e"] = w.press_update("vikunja") out["log_e"] = ctl_log(t0) w.say("controller log (e):\n" + out["log_e"]) out["read_e"] = readall("after the undo (e)", {"A": S["A"], "B": S["B"]}) out["obs_e"] = w.observables("vikunja") # (f) t1 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) out["restore_f"] = w.restore("vikunja") out["labels_after_restore_v0268"] = labels() w.say("labels after the v0.268.0 restore:\n" + out["labels_after_restore_v0268"]) out["log_f"] = ctl_log(t1) out["compose_warn_f"] = w.guest(f"docker logs --since {t1} felhom-controller 2>&1 | grep -iE 'not created by Docker Compose|already exists|Recreate' | tail -5") w.say("compose warnings after the restore (want none): " + (out["compose_warn_f"].strip() or "(none)")) out["read_f"] = readall("after the v0.268.0 restore (f)", {"A": S["A"]}) # (g) C = F.seed(w, SUB, w.say) t2 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) out["press_g"] = w.press_update("vikunja") out["log_g"] = ctl_log(t2) w.say("controller log (g):\n" + out["log_g"]) out["read_g"] = readall("after the second undo (g)", {"A": S["A"], "C": C}) # (h) out["drill_h"] = drill(lambda: set_vik("2.5.0", PORT_REAL), "vikunja back to 2.5.0 + its real probe") out["remove_h"] = w.remove("vikunja") out["leftovers_h"] = w.guest("ls -la /opt/docker/stacks/vikunja/; docker volume ls -q | grep -E '^vikunja' || echo 'no vikunja volumes'") w.say("after remove:\n" + out["leftovers_h"]) json.dump(out, open("../partA/02-v0268-undo-after-restore.json", "w"), indent=2, ensure_ascii=False, default=str) open("../partA/02-v0268-undo-after-restore.log", "w").write("\n".join(w.LOG) + "\n")