Files
felhom.eu/documentation/audits/ladder-2026-09-24/tools/liveA2.py
T

97 lines
4.6 KiB
Python

#!/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")