130 lines
6.2 KiB
Python
130 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""undo_proof.py — Part 3: the big step's UNDO, proven on scratch 9202 through the product's own guarded Update.
|
|
|
|
Jellyfin 10.11.11 -> 12.4 converts its database with no way back (the bench's abort put the old image back on the
|
|
converted data: „the app started but the seeded data was gone"). So the box's undo must bring back the DATA, not only
|
|
the old image. The proof:
|
|
1. the drill's jellyfin at FROM (a temporary drill commit), install, seed the first user, read it back (C1);
|
|
2. a temporary drill commit moves the image to 12.4 AND makes its checks fail (compose healthcheck `false`, the
|
|
.felhom.yml probe on a path that does not exist) — so 12.4 starts, converts the database, and the Update fails
|
|
in its check;
|
|
3. press the guarded Update: the final phase must be `undone`; while it verifies, 12.4's own log is saved (the
|
|
conversion is seen, not assumed);
|
|
4. after the undo: the running image is 10.11.11 and the seeded user signs in (a wrong password refused);
|
|
5. the two temporary drill commits are reverted, the app removed.
|
|
usage: SC=<dir with .ctlpw> TARGET=9202 EV=<dir> python3 undo_proof.py
|
|
"""
|
|
import json, os, re, subprocess, sys, threading, time
|
|
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
|
import box_walk as w
|
|
import upgrade_fixtures_box28 as f28
|
|
|
|
APP, SUB = "jellyfin", "jellyfin"
|
|
FROM, TO = "jellyfin/jellyfin:10.11.11", "jellyfin/jellyfin:12.4"
|
|
D = w.DRILL
|
|
EV = os.environ["EV"]
|
|
os.makedirs(EV, exist_ok=True)
|
|
rec = {"app": APP, "from": FROM, "to": TO, "started": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
|
|
|
|
|
def git(*a):
|
|
return subprocess.run(["git", "-C", D] + list(a), capture_output=True, text=True, timeout=120)
|
|
|
|
|
|
def drill_commit(msg, edit):
|
|
git("pull", "-q", "--rebase", "origin", "main")
|
|
comp, fy = f"{D}/templates/{APP}/docker-compose.yml", f"{D}/templates/{APP}/.felhom.yml"
|
|
c, y = open(comp).read(), open(fy).read()
|
|
c, y = edit(c, y)
|
|
open(comp, "w").write(c)
|
|
open(fy, "w").write(y)
|
|
git("add", f"templates/{APP}")
|
|
git("commit", "-q", "-m", msg)
|
|
r = git("push", "-q", "origin", "main")
|
|
h = git("rev-parse", "--short=12", "HEAD").stdout.strip()
|
|
w.say(f" drill commit {h}: {msg} (push rc={r.returncode})")
|
|
w.refresh_box_mirror()
|
|
return h
|
|
|
|
|
|
def finish(verdict, why):
|
|
rec["verdict"], rec["why"] = verdict, why
|
|
rec["log"] = w.LOG
|
|
json.dump(rec, open(os.path.join(EV, "undo-proof-jellyfin.json"), "w"), indent=2)
|
|
w.say(f"RESULT {verdict} — {why}")
|
|
|
|
|
|
w.login()
|
|
head0 = git("rev-parse", "HEAD").stdout.strip()
|
|
rec["drill_head_before"] = head0
|
|
fx = f28.FIXTURES28[APP]
|
|
commits = []
|
|
try:
|
|
def to_from(c, y):
|
|
return c.replace(f"image: {TO}", f"image: {FROM}"), y
|
|
commits.append(drill_commit("UNDO-PROOF setup: jellyfin at FROM (temporary, reverted at the end)", to_from))
|
|
if w.stack(APP).get("deployed"):
|
|
w.remove(APP)
|
|
w.sync_rescan(APP, FROM, tries=40, delay=10)
|
|
if not w.deploy(APP, SUB):
|
|
finish("inconclusive", "the install at FROM did not complete"); raise SystemExit(1)
|
|
w.wait_app(SUB, "/System/Info/Public", want=("200",), tries=60)
|
|
tok = fx.seed(w, SUB, w.say)
|
|
if not tok or not fx.verify(w, SUB, tok, w.say):
|
|
finish("inconclusive", "C1: the seed did not read back before the update"); raise SystemExit(1)
|
|
|
|
def failing_to(c, y):
|
|
c = c.replace(f"image: {FROM}", f"image: {TO}")
|
|
c = c.replace('test: ["CMD", "curl", "-f", "http://127.0.0.1:8096/health"]', 'test: ["CMD", "false"]')
|
|
y = y.replace('path: "/health"', 'path: "/undo-proof-no-such-path"', 1)
|
|
return c, y
|
|
commits.append(drill_commit("UNDO-PROOF: jellyfin 12.4 whose checks fail (temporary, reverted at the end)", failing_to))
|
|
w.sync_rescan(APP, TO, tries=40, delay=10)
|
|
|
|
seen = {"lines": []}
|
|
stop = threading.Event()
|
|
|
|
def watch():
|
|
while not stop.is_set():
|
|
out = w.guest("docker inspect -f '{{.Config.Image}}' jellyfin 2>/dev/null; "
|
|
"docker logs --tail 400 jellyfin 2>&1 | grep -iE 'migrat|Jellyfin version|database' | tail -40")
|
|
if "12.4" in out.split("\n")[0]:
|
|
seen["lines"] = out.strip().split("\n")
|
|
time.sleep(5)
|
|
th = threading.Thread(target=watch, daemon=True)
|
|
th.start()
|
|
rec["update"] = u = w.press_update(APP, poll=1.0, cap_s=1800)
|
|
stop.set()
|
|
th.join(timeout=30)
|
|
rec["to_log_while_verifying"] = seen["lines"][-40:]
|
|
time.sleep(15)
|
|
rec["observables_after"] = obs = w.observables(APP)
|
|
running = w.guest("docker inspect -f '{{.Config.Image}}' jellyfin 2>/dev/null").strip()
|
|
rec["running_image_after"] = running
|
|
if u.get("final_phase") != "undone":
|
|
finish("failed", f"the update ended {u.get('final_phase')!r}, not 'undone'"); raise SystemExit(1)
|
|
if running != FROM:
|
|
finish("failed", f"after the undo the running image is {running!r}, not {FROM}"); raise SystemExit(1)
|
|
if not any(re.search(r"migrat", l, re.I) for l in seen["lines"]):
|
|
w.say(" note: no migration line was captured from 12.4 while it verified")
|
|
rec["seed_after_undo"] = ok = fx.verify(w, SUB, tok, w.say)
|
|
if not ok:
|
|
finish("failed", "the undo put the old image back but the seeded user does not sign in"); raise SystemExit(1)
|
|
finish("proven", "12.4 started on the data and failed its check; the box undid it: 10.11.11 runs again and the "
|
|
"seeded user signs in (a wrong password refused)")
|
|
finally:
|
|
if w.stack(APP).get("deployed"):
|
|
w.remove(APP)
|
|
for h in reversed(commits):
|
|
git("pull", "-q", "--rebase", "origin", "main")
|
|
r = git("revert", "--no-edit", h)
|
|
w.say(f" drill revert {h}: rc={r.returncode}")
|
|
if commits:
|
|
git("push", "-q", "origin", "main")
|
|
w.refresh_box_mirror()
|
|
rec["drill_head_after"] = git("rev-parse", "HEAD").stdout.strip()
|
|
rec["drill_tree_equal_to_before"] = git("diff", "--quiet", head0, "HEAD", "--", f"templates/{APP}").returncode == 0
|
|
w.say(f" drill templates/{APP} equal to before: {rec['drill_tree_equal_to_before']}")
|
|
json.dump({k: v for k, v in rec.items() if k != "log"} | {"log": w.LOG},
|
|
open(os.path.join(EV, "undo-proof-jellyfin.json"), "w"), indent=2)
|