8db4425d98
gates / gates (push) Successful in 2m38s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
159 lines
9.4 KiB
Python
159 lines
9.4 KiB
Python
"""r638slice0.py <app> — R-638 slice 0, MEASUREMENT ONLY, on scratch 9202 (drill catalog). `09` §3 decision 154.
|
|
|
|
1 the drill's <app> template is put at the OLD definition (the commit before the R-462 move), ladder emptied;
|
|
2 install fresh through the product; seed through the app's own door; read back (C1); the live table list;
|
|
3 a DRILL commit moves it to the NEW definition with the one proven ladder entry; sync; the product's guarded
|
|
Update (its backing-up phase makes the Tier-2 copy at the OLD version); the table list after the migration;
|
|
4 the named unit restore of that pre-update copy through the household's button (POST /backup/restore);
|
|
5 positive observable: the controller's `Imported DB dump` line + the seed read back;
|
|
control from a different channel: the live table list against the table list READ FROM THE COPY'S OWN DUMP FILE
|
|
on the Tier-2 drive — every table the migration added must be GONE;
|
|
6 removed through the product (KEEP=1 keeps it).
|
|
Evidence: ../B/<app>.txt + ../B/<app>-tables.json."""
|
|
import json, os, re, subprocess, sys, time
|
|
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
|
import box_walk as w
|
|
import upgrade_fixtures_box as fixtures
|
|
|
|
APP = sys.argv[1]
|
|
CFG = {
|
|
"docmost": {"sub": "docmost", "old": "6d8cd87^", "new": "6d8cd87", "db": "docmost-postgres",
|
|
"tables": "psql -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\" -Atc \"select tablename from pg_tables where schemaname='public' order by 1\""},
|
|
"romm": {"sub": "romm", "extra": {"HDD_PATH": "/mnt/felhom-drives/scratch_hdd"}, "old": "15f9ebf^", "new": "15f9ebf", "db": "romm-db",
|
|
"tables": "MYSQL_PWD=\"$MYSQL_PASSWORD\" mariadb -u\"$MYSQL_USER\" \"$MYSQL_DATABASE\" -Nse 'show tables'"},
|
|
}[APP]
|
|
LIVE = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu"
|
|
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
EV = os.path.join(HERE, "..", "B")
|
|
log = open(os.path.join(EV, f"{APP}.txt"), "a", buffering=1)
|
|
REC = {"app": APP}
|
|
|
|
|
|
def say(*a):
|
|
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
|
|
|
|
|
|
def git(*a, cwd=D):
|
|
return subprocess.run(["git", "-C", cwd, *a], check=True, capture_output=True, text=True).stdout
|
|
|
|
|
|
def live_tables():
|
|
out = w.guest(f"docker exec -i {CFG['db']} sh -s 2>&1 <<'SQLX'\n{CFG['tables']}\nSQLX\n")
|
|
return sorted(t for t in out.split() if re.fullmatch(r"[A-Za-z0-9_]+", t))
|
|
|
|
|
|
def set_template(rev, ladder_entries, msg):
|
|
git("pull", "-q", "--rebase", "origin", "main")
|
|
comp = git("show", f"{rev}:templates/{APP}/docker-compose.yml", cwd=LIVE)
|
|
open(f"{D}/templates/{APP}/docker-compose.yml", "w").write(comp)
|
|
sys.path.insert(0, f"{LIVE}/scripts")
|
|
import ladder
|
|
fy = open(f"{LIVE}/templates/{APP}/.felhom.yml").read()
|
|
entries, _, _ = ladder.parse(fy)
|
|
head = fy.split("\nupdate_ladder:")[0].rstrip("\n") + "\n"
|
|
if ladder_entries:
|
|
keep = [e for e in entries if e in ladder_entries]
|
|
for e in keep:
|
|
head = ladder.append_entry(head, e)
|
|
open(f"{D}/templates/{APP}/.felhom.yml", "w").write(head)
|
|
subprocess.run(["rm", "-rf", f"{D}/templates/{APP}/steps"], check=True)
|
|
subprocess.run(["git", "-C", D, "add", "-A", f"templates/{APP}"], check=True)
|
|
git("commit", "-q", "--allow-empty", "-m", msg)
|
|
git("push", "-q", "origin", "main")
|
|
say("drill:", git("log", "--oneline", "-1").strip(), "| images:", re.findall(r"image:\s*(\S+)", comp))
|
|
return entries
|
|
|
|
|
|
fx = fixtures.FIXTURES[APP]
|
|
w.login()
|
|
REC["controller"] = w.guest("docker inspect -f '{{.Config.Image}}' felhom-controller").strip()
|
|
say("9202 controller:", REC["controller"])
|
|
if w.stack(APP).get("deployed"):
|
|
say(f"{APP} already installed on 9202 — removing it first (scratch box)")
|
|
w.remove(APP)
|
|
sys.path.insert(0, f"{LIVE}/scripts")
|
|
import ladder
|
|
all_entries = ladder.parse(open(f"{LIVE}/templates/{APP}/.felhom.yml").read())[0]
|
|
set_template(CFG["old"], [], f"DRILL {APP}: the OLD definition ({CFG['old']}) for R-638 slice 0")
|
|
old_imgs = re.findall(r"image:\s*(\S+)", git("show", f"{CFG['old']}:templates/{APP}/docker-compose.yml", cwd=LIVE))
|
|
w.sync_rescan(APP, old_imgs[0]) # wait until the box's catalog carries the OLD definition (run 1 installed the live one)
|
|
if not w.deploy(APP, CFG["sub"], CFG.get("extra")):
|
|
sys.exit(say("RESULT the install did not complete") or 1)
|
|
tok = fx.seed(w, CFG["sub"], say)
|
|
if tok is None or not fx.verify(w, CFG["sub"], tok, say):
|
|
sys.exit(say("RESULT C1 failed — the seed did not read back before") or 1)
|
|
REC["pinned_old"] = (w.stack(APP).get("app_config") or {}).get("pinned_images")
|
|
if old_imgs[0] not in (REC["pinned_old"] or {}).values():
|
|
sys.exit(say(f"RESULT the box installed {REC['pinned_old']}, not the OLD definition — stopping") or 1)
|
|
REC["tables_old_live"] = live_tables()
|
|
say(f"[2] old version installed, pinned={REC['pinned_old']}, live tables {len(REC['tables_old_live'])}")
|
|
|
|
new_comp = git("show", f"{CFG['new']}:templates/{APP}/docker-compose.yml", cwd=LIVE)
|
|
new_imgs = dict(re.findall(r"^ ([a-z0-9-]+):\s*\n(?:.*\n)*?\s+image:\s*(\S+)", new_comp, re.M))
|
|
step = all_entries[:1] # docmost 0.95.0 -> 0.96.0 (pg16), romm 5.0.0 -> 5.3.0 (mariadb 11.4)
|
|
say(f"[3] the ladder entry used: {[ (e['from'], e['to']) for e in step ]}")
|
|
set_template(CFG["new"], step, f"DRILL {APP}: the NEW definition ({CFG['new']}) + its one ladder entry, R-638 slice 0")
|
|
w.sync_rescan(APP, next(iter(step[0]["to"].values())) if step else None)
|
|
since = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip()
|
|
res = w.press_update(APP, poll=1, cap_s=1800)
|
|
REC["update_final_phase"] = res.get("final_phase")
|
|
REC["pinned_new"] = (w.stack(APP).get("app_config") or {}).get("pinned_images")
|
|
time.sleep(10)
|
|
REC["tables_new_live"] = live_tables()
|
|
added = sorted(set(REC["tables_new_live"]) - set(REC["tables_old_live"]))
|
|
REC["tables_added_by_migration"] = added
|
|
say(f"[3] update -> {res.get('final_phase')}, pinned={REC['pinned_new']}, live tables {len(REC['tables_new_live'])}, "
|
|
f"added by the migration: {len(added)} {added}")
|
|
REC["seed_after_update"] = fx.verify(w, CFG["sub"], tok, say)
|
|
if res.get("final_phase") != "done" or not added:
|
|
say("RESULT the update did not migrate (no done, or no new table) — slice 0 cannot measure; stopping before restore")
|
|
json.dump(REC, open(os.path.join(EV, f"{APP}-tables.json"), "w"), indent=2)
|
|
sys.exit(1)
|
|
|
|
snaps = w.snapshots(APP)
|
|
say(f"[4] restorable copies offered: {json.dumps(snaps)[:600]}")
|
|
pick = snaps[0] if snaps else None
|
|
sid = pick and (pick.get("id") or pick.get("snapshot_id") or pick.get("short_id"))
|
|
since_r = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip()
|
|
r = w.restore(APP, sid)
|
|
REC["restore"] = {k: r.get(k) for k in ("snapshot_id", "http", "seconds", "state_after", "hold_after")}
|
|
time.sleep(15)
|
|
lines = w.guest(f"docker logs --since {since_r} felhom-controller 2>&1 | grep -v DEBUG | grep -i -E 'restor|Imported DB dump|replay|volume' | cut -c1-300")
|
|
log.write(lines + "\n")
|
|
REC["imported_db_dump_line"] = [l for l in lines.splitlines() if "Imported DB dump" in l]
|
|
REC["tables_after_restore"] = live_tables()
|
|
REC["seed_after_restore"] = fx.verify(w, CFG["sub"], tok, say)
|
|
# control from a different channel: the table list in the copy's own dump file on the Tier-2 drive
|
|
REC["unit_files"] = w.guest(f"find /mnt/felhom-drives/scratch_hdd/backups -path '*{APP}*' -type f 2>/dev/null | head -40").split()
|
|
# the RESTORED copy's dump — never a `pre-restore-*` safety dump the restore itself wrote (run 3 of romm read one)
|
|
dumps = "\n".join(f for f in REC["unit_files"] if re.search(r"(\.sql(\.gz|\.zst)?$|dump)", f) and "pre-restore" not in f)
|
|
REC["dump_files_seen"] = dumps.split()
|
|
copy_tables = []
|
|
if dumps.split():
|
|
f0 = dumps.split()[0]
|
|
cat = "zcat" if f0.endswith(".gz") else ("zstdcat" if f0.endswith(".zst") else "cat")
|
|
# tables AND views: `SHOW TABLES` / pg_tables list what the live side lists, so the copy side must name the same kinds
|
|
# (romm has three views; run 3 counted only CREATE TABLE and read 24 of 27). pg_tables lists tables only.
|
|
kinds = "TABLE|VIEW" if APP == "romm" else "TABLE"
|
|
out = w.guest(f"{cat} '{f0}' | grep -o -E 'CREATE (ALGORITHM=[A-Z]+ )?(DEFINER=[^ ]+ )?(SQL SECURITY [A-Z]+ )?({kinds}) (IF NOT EXISTS )?[`\"]?([a-z]+\\.)?[`\"]?[A-Za-z0-9_]+' | sed -E 's/.*[ .`\"]//' | sort -u")
|
|
copy_tables = sorted(set(t for t in out.split() if re.fullmatch(r"[A-Za-z0-9_]+", t)))
|
|
REC["tables_in_copy_dump"] = copy_tables
|
|
REC["dump_used_for_control"] = dumps.split()[:1]
|
|
left = sorted(set(REC["tables_after_restore"]) & set(added))
|
|
REC["migration_tables_left_after_restore"] = left
|
|
REC["live_equals_copy"] = REC["tables_after_restore"] == copy_tables
|
|
say(f"[5] Imported DB dump lines: {REC['imported_db_dump_line']}")
|
|
say(f"[5] seed read back after restore: {REC['seed_after_restore']}")
|
|
say(f"[5] live tables after restore {len(REC['tables_after_restore'])}; the copy's own dump lists {len(copy_tables)}; "
|
|
f"equal={REC['live_equals_copy']}; migration tables still there: {left}")
|
|
REC["pinned_after_restore"] = (w.stack(APP).get("app_config") or {}).get("pinned_images")
|
|
REC["state_after_restore"] = w.stack(APP).get("state")
|
|
say(f"[5] pinned after restore {REC['pinned_after_restore']}, state {REC['state_after_restore']}")
|
|
ok = bool(REC["imported_db_dump_line"]) and REC["seed_after_restore"] and not left and REC["live_equals_copy"]
|
|
REC["verdict"] = "SAFE" if ok else "NOT SAFE / NOT SHOWN"
|
|
say(f"RESULT {APP}: {REC['verdict']}")
|
|
json.dump(REC, open(os.path.join(EV, f"{APP}-tables.json"), "w"), indent=2)
|
|
if not os.environ.get("KEEP"):
|
|
say(f"remove -> {w.remove(APP)}")
|