Files
felhom.eu/documentation/audits/catalog-sweep-2026-10-24/tools/unitprobe.py
T
admin 67053335d1
gates / gates (push) Successful in 13m17s
Catalog sweep 2026-10-24: all 65 catalog apps on a fresh box (52 work, 10 with a problem, 0 broken); register 208 -> 217 (R-1269..R-1277 opened; notes on R-739, R-1267); STATUS + D46; map B1 re-checked; evidence
Every catalog app installed, used, backed up, removed keeping data, reinstalled, restored and removed with data on a
fresh box from the public ISO (demo-hp VM 9360, Tester4), endpoint-level. Worst: a reinstall's capture rewrites the
kept unit so its restore breaks the app (R-1269, P2), the restore still reports success (R-1270, P2), and „use my kept
data" loaded another box's future-dated database (R-1271, P2). Drill catalog fixes for the morning list (n8n usage
data; OpenGist 256M, proven by a cgroup probe). VM 9360 and LXC 9361 destroyed; hub host Tester4-aa9466 left for the
operator. REPORT-catalog-sweep-2026-10-24.md.
2026-10-25 00:13:05 +02:00

82 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""unitprobe.py — F10 of the sweep: does a reinstall's own capture rewrite the unit a „keep backups" remove kept?
The household's order, endpoint-level: install, one item, „Mentés most", remove KEEPING data and backups, install the
app again (a fresh install — no kept-data question for a volume app), WAIT for the controller's next capture of the
new install, then „Visszaállítás" of the kept unit. Observed without reading any secret: the unit's manifest (its data
time, its definition checksums) before the reinstall and after the capture, the controller's own lines, the app's state
and its database log after the restore, and the item through the front door.
usage: SC=… SWEEP_EV=… DRIVE_ROOT=… python3 unitprobe.py <app> (evidence: <SWEEP_EV>/_unitprobe/<app>/)
"""
import json, os, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import sweep as sw # noqa: E402
w, fxm = sw.w, sw.fxm
UNIT = "/mnt/sys_drive/felhom-data/backups/primary/{app}"
def manifest(app):
out = sw.g(f"cd {UNIT.format(app=app)} 2>/dev/null && ls -la --time-style=+%FT%T . volume-dumps db-dumps 2>/dev/null; "
f"for f in manifest.json recovery.json unit.json; do [ -f $f ] && echo \"== $f\" && cat $f; done")
return out
def main(app):
d = os.path.join(sw.EV, "_unitprobe", app)
os.makedirs(d, exist_ok=True)
log = open(os.path.join(d, "walk.log"), "a")
def say(*a):
line = time.strftime("%H:%M:%SZ", time.gmtime()) + " " + " ".join(str(x) for x in a)
print(line, flush=True)
log.write(sw.scrub(line) + "\n")
log.flush()
w.say = say
fx = fxm.FIXTURES[app]
sub = getattr(fx, "sub", app)
rec = {"app": app}
w.login()
try:
rec["install"] = w.deploy(app, sub) and w.wait_app(sub, "/", tries=90, delay=5)
tok = fx.seed(w, sub, say)
rec["seed"] = bool(tok)
sw.backup_now(rec, say)
open(os.path.join(d, "unit-1-after-backup.txt"), "w").write(sw.scrub(manifest(app)))
sw.remove(app, keep=True, say=say)
open(os.path.join(d, "unit-2-after-keep-remove.txt"), "w").write(sw.scrub(manifest(app)))
t_re = time.time()
rec["reinstall"] = w.deploy(app, sub, dict(w.GENERATED.get(app) or {}))
# wait for the controller's next capture of the NEW install (the line it logs), at most 10 minutes
captured = ""
for _ in range(60):
time.sleep(10)
captured = sw.g(f"docker logs --since {int(t_re)} felhom-controller 2>&1 | grep 'Recovery unit captured for {app} '")
if captured.strip():
break
rec["capture_after_reinstall"] = captured.strip()[:300]
say(" capture after the reinstall: " + (captured.strip()[:200] or "NONE within 10 min"))
open(os.path.join(d, "unit-3-after-reinstall-capture.txt"), "w").write(sw.scrub(manifest(app)))
sw.restore_unit(app, rec, say)
time.sleep(20)
rec["state_after_restore"] = w.stack(app).get("state")
rec["item_after_restore"] = bool(tok) and bool(fx.verify(w, sub, tok, say))
rec["db_auth_errors_after_restore"] = sw.g(
f"for c in $(docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'); do "
f"docker logs --since 5m $c 2>&1 | grep -ciE 'password authentication failed|access denied for user' | sed \"s/^/$c: /\"; done").strip()
rec["controller_restore_lines"] = sw.scrub(sw.g(
f"docker logs --since {int(t_re)} felhom-controller 2>&1 | grep -E '{app}' | grep -iE 'restor|captured|health|come from the backup' | cut -c1-240"))
say(" state after restore: %s, item: %s, db auth errors: %s" % (rec["state_after_restore"], rec["item_after_restore"],
rec["db_auth_errors_after_restore"]))
finally:
sw.remove(app, keep=False, say=say)
rec["after_remove"] = sw.leftovers(app, sub)
json.dump(rec, open(os.path.join(d, "record.json"), "w"), ensure_ascii=False, indent=1)
if __name__ == "__main__":
main(sys.argv[1])