Files
felhom.eu/documentation/audits/night-2026-09-25/tools/liveCsetup.py
T

119 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Night 2026-09-25 Part C SETUP (adapted from night-2026-09-24 liveC2setup.py). Adds: navidrome's FIRST step marked
needs_person, wishlist's step marked files_may_change. Original doc:
Part C step 2, SETUP on 9202: four apps installed through the product, each at the ladder's first `from`,
then the drill catalog put back at the head — so wishlist is 1 step behind, navidrome 2, romm 3, and vikunja 1
with a FAILING step (its head .felhom.yml probe points at a closed port AFTER the install, so the install's
applied record keeps the good probe and the undo can judge the old version correctly).
The drill also receives the live catalog's step .felhom.yml files (catalog cf7cf84) the reset predates."""
import json, os, re, shutil, sys, time
sys.path.insert(0, ".")
import walk as w
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import ladder
LIVE = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates"
APPS = {"wishlist": "c-wish", "navidrome": "c-navi", "romm": "c-romm", "vikunja": "c-vik"}
T = lambda a: f"{w.DRILL}/templates/{a}"
out = {"controller": w.guest("cat /etc/felhom-controller-image").strip(), "apps": {}}
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, "add", "-A", "templates"])
w.sh(["git", "-C", w.DRILL, "commit", "-qm", "NIGHT-C " + msg])
w.sh(["git", "-C", w.DRILL, "push", "-q", "origin", "main"], timeout=120)
w.say("drill: " + msg)
HEAD = {a: open(f"{T(a)}/docker-compose.yml").read() for a in APPS}
FY = {a: open(f"{T(a)}/.felhom.yml").read() for a in APPS}
def old_compose(a):
E, _, _ = ladder.parse(FY[a])
frm, head = E[0]["from"], ladder.images_in(HEAD[a])
c = HEAD[a]
for svc, ref in head.items():
if frm.get(svc) and frm[svc] != ref:
c = c.replace(f"image: {ref}", f"image: {frm[svc]}", 1)
assert ladder.images_in(c) == frm, (a, ladder.images_in(c), frm)
return c, len(E)
def to_old():
for a in APPS:
c, n = old_compose(a)
open(f"{T(a)}/docker-compose.yml", "w").write(c)
out["apps"][a] = {"ladder_len": n, "installed_at": ladder.images_in(c)}
for a in ("navidrome", "romm"):
for f in os.listdir(f"{LIVE}/{a}/steps"):
if f.endswith(".felhom.yml"):
shutil.copy(f"{LIVE}/{a}/steps/{f}", f"{T(a)}/steps/{f}")
drill(to_old, "wishlist/navidrome/romm/vikunja at the ladder's first from (+ step .felhom.yml files from live cf7cf84)")
w.sync_rescan()
def wait_box_catalog(want):
"""R-607: the sync may answer "no change" while the catalog moved — wait until the BOX's own catalog
copy names the old images (measured the hard way at 22:10: four apps installed at the head)."""
t0 = time.time()
for i in range(40):
got = w.guest("cat /var/lib/docker/volumes/felhom-controller-data/_data/catalog-cache/templates/*/docker-compose.yml /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/*/.felhom.yml "
"/var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/*/docker-compose.yml 2>/dev/null")
if all(ref in got for ref in want):
w.say(f"box catalog caught up in {round(time.time()-t0)} s ({i+1} rounds)")
return
w.sync_rescan()
time.sleep(10)
raise SystemExit("the box catalog never caught up: " + str(want))
wait_box_catalog([r for a in APPS for r in out["apps"][a]["installed_at"].values()])
for a, sub in APPS.items():
t0 = time.time()
ok = w.deploy(a, sub)
st = w.stack(a)
out["apps"][a]["deploy_ok"] = ok
out["apps"][a]["deploy_s"] = round(time.time() - t0)
out["apps"][a]["pinned"] = (st.get("app_config") or {}).get("pinned_images")
w.say(f"{a}: deployed={ok} in {out['apps'][a]['deploy_s']} s pinned={out['apps'][a]['pinned']}")
def to_head():
for a in APPS:
open(f"{T(a)}/docker-compose.yml", "w").write(HEAD[a])
f = re.sub(r"(healthcheck:\n\s+checks:\n\s+- type: api\n\s+port: )3456\b", r"\g<1>8999", FY["vikunja"], count=1)
assert f != FY["vikunja"]
open(f"{T('vikunja')}/.felhom.yml", "w").write(f)
# decision 13's marks, set on the FIRST ladder entry (the step the leg would take tonight)
for app, mark, val in (("navidrome", "needs_person", "drill: a person must check the library scan after this step"),
("wishlist", "files_may_change", True)):
body = open(f"{T(app)}/.felhom.yml").read()
lines = body.split("\n")
i = lines.index("update_ladder:") + 1
e = json.loads(lines[i][4:])
e.setdefault("marks", {})[mark] = val
lines[i] = " - " + json.dumps(e, ensure_ascii=False)
open(f"{T(app)}/.felhom.yml", "w").write("\n".join(lines))
out["apps"][app]["mark"] = {mark: val}
drill(to_head, "heads back; vikunja's head step probes port 8999 (the failing step); navidrome step 1 needs_person; wishlist step files_may_change")
wait_box_catalog([ladder.images_in(HEAD[a])[svc] for a in APPS for svc in ladder.images_in(HEAD[a])] + ["8999"])
w.sync_rescan()
for a in APPS:
st = w.stack(a)
out["apps"][a]["steps_left"] = st.get("ladder_steps_left")
out["apps"][a]["badges"] = w.badges(a)
w.say(f"{a}: steps_left={st.get('ladder_steps_left')} badge={out['apps'][a]['badges']['hu'][:1]}")
json.dump(out, open("../C/C1-setup.json", "w"), indent=2, ensure_ascii=False, default=str)
open("../C/C1-setup.log", "w").write("\n".join(w.LOG) + "\n")