Files
felhom.eu/documentation/audits/day-2026-10-08/r762/9202/tools/box9202.py
T

153 lines
7.8 KiB
Python

"""box9202.py — R-762 9202 half (2026-10-08 afternoon). ONE process, through the product.
phase install: drill commit (wger un-hidden + the gunicorn definition, DRILL only), sync, deploy, seed, login page,
CSS, photo, workers, env; then a ~10-minute watch (anon from memory.stat, oom_kill, restarts) with light load.
The removal is a separate phase (remove) so a failure leaves something to look at.
Evidence -> $EVD (no secrets: the generated admin password is never written)."""
import json, os, re, struct, subprocess, sys, tempfile, time, zlib, secrets
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, SUB = "wger", "fitness"
EVD = os.environ["EVD"]; os.makedirs(EVD, exist_ok=True)
log = open(f"{EVD}/run.txt", "a", buffering=1)
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
NEWC = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/wger/docker-compose.yml"
REC = {"app": APP, "venue": "scratch guest 9202 on demo-hp, drill catalog, the product's deploy endpoint"}
def say(*a):
w.say(*a); log.write(time.strftime("%H:%M:%S ") + " ".join(map(str, a)) + "\n")
def git(*a):
return subprocess.run(["git", "-C", D, *a], check=True, capture_output=True, text=True).stdout
def png(path, n=64):
rgb = secrets.token_bytes(3)
raw = b"".join(b"\x00" + rgb * n for _ in range(n))
def chunk(t, d):
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff)
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", n, n, 8, 2, 0, 0, 0))
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
open(path, "wb").write(data)
return len(data)
def fetch(path):
"""code, bytes, content-type of one GET through the household's route (traefik, gate cookie). Body discarded."""
gc = w.GATE.get(SUB) or w.gate_cookie(SUB)
args = ["curl", "-sSk", "--max-time", "45", "-H", f"Host: {SUB}.{w.DOMAIN}", "-o", "/dev/null",
"-w", "%{http_code} %{size_download} %{content_type}"]
if gc:
args += ["-H", f"Cookie: {gc}"]
r = w.sh(args + [f"{w.BASE}{path}"], timeout=90)
return (r.stdout or "").strip()
def css_links():
rc, code, page = w.app_curl(SUB, "/en/user/login")
links = re.findall(r'(/static/[^"]+\.css)', page or "")[:3]
return code, {l: fetch(l) for l in links}
CG = 'ID=$(docker inspect -f "{{.Id}}" wger); CG=$(find /sys/fs/cgroup -maxdepth 6 -type d -name "*$ID*" | head -1)'
phase = sys.argv[1]
w.login()
if phase == "install":
if w.stack(APP).get("deployed"):
sys.exit(say("STOP: wger already deployed on 9202 — not this run's") or 1)
git("pull", "-q", "--rebase", "origin", "main")
fy = f"{D}/templates/{APP}/.felhom.yml"
s = open(fy).read()
open(fy, "w").write(s.replace("lifecycle: hidden", "lifecycle: available", 1))
open(f"{D}/templates/{APP}/docker-compose.yml", "w").write(open(NEWC).read())
git("add", f"templates/{APP}/.felhom.yml", f"templates/{APP}/docker-compose.yml")
git("commit", "-q", "-m", "DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)")
git("push", "-q", "origin", "main")
REC["drill_commit"] = git("log", "--oneline", "-1").strip()
say("drill:", REC["drill_commit"])
for _ in range(12):
w.sync_rescan(); time.sleep(5)
stk = w.guest(f"grep -c WGER_USE_GUNICORN /opt/docker/stacks/{APP}/docker-compose.yml; grep -n lifecycle /opt/docker/stacks/{APP}/.felhom.yml").split()
if stk and stk[0] == "1" and "available" in " ".join(stk):
break
say("synced template on the box:", " ".join(stk))
if not w.deploy(APP, SUB):
sys.exit(say("RESULT the install did not complete") or 1)
REC["pinned"] = (w.stack(APP).get("app_config") or {}).get("pinned_images")
fx = fixtures.FIXTURES[APP]
tok = fx.seed(w, SUB, say)
if tok is None:
sys.exit(say(f"RESULT seed failed: {getattr(fx, 'tried', '')}") or 1)
REC["seed_read"] = fx.verify(w, SUB, tok, say)
REC["login_page"], REC["css"] = css_links()
say(f"login page -> {REC['login_page']}; CSS {REC['css']}")
jar = tempfile.mktemp(prefix="wger-jar-")
hdr, why = fx._login(w, SUB, tok["pw"], jar)
pic = tempfile.mktemp(prefix="wger-photo-", suffix=".png")
size = png(pic)
rc, code, out = w.app_curl(SUB, "/api/v2/gallery/", *hdr, "-F", f"image=@{pic};type=image/png", "-F",
"date=2026-10-08", "-F", "description=r762", method="POST")
img = None
try:
img = json.loads(out).get("image")
except Exception:
say(f" gallery body {out[:200]}")
path = re.sub(r"^https?://[^/]+", "", img or "")
REC["photo_post"] = code; REC["photo_bytes_posted"] = size; REC["photo_path"] = path
REC["photo_read"] = fetch(path) if path else None
REC["control_unknown_static"] = fetch("/static/r762-nonexistent.css")
say(f"POST /api/v2/gallery/ ({size} B PNG) -> {code}; read back {path} -> {REC['photo_read']}; control -> {REC['control_unknown_static']}")
for f in (jar, pic):
if os.path.exists(f):
os.unlink(f)
g = w.guest(CG + '''
echo "cgroup=$CG"
docker exec wger sh -c 'env | grep -E "^(WGER_USE_GUNICORN|WEB_CONCURRENCY)="'
docker logs wger 2>&1 | grep -E "Using gunicorn|Using django|Booting worker|Listening at|Starting gunicorn|Starting development server"
cat $CG/memory.max''')
say("container:\n" + g)
REC["container"] = g
# the watch: ~10 min, anon sampled every 2 s in the guest; light load from here (login page + CSS + photo)
w.guest(CG + '''
rm -f /root/r762w.*; touch /root/r762w.on
nohup sh -c 'max=0; while [ -f /root/r762w.on ]; do a=$(awk "\\$1==\\"anon\\"{print \\$2}" '"$CG"'/memory.stat 2>/dev/null); [ -n "$a" ] && [ "$a" -gt "$max" ] && max=$a && echo $max > /root/r762w.max; sleep 2; done' >/dev/null 2>&1 &
echo started''')
t0 = time.time(); n = 0; codes = {}
while time.time() - t0 < 600:
for p in ("/en/user/login", list(REC["css"])[0] if REC["css"] else "/en/user/login", path or "/en/user/login"):
c = fetch(p).split(" ")[0]; codes[c] = codes.get(c, 0) + 1; n += 1
rc, c, _ = w.app_curl(SUB, "/en/dashboard"); codes[c] = codes.get(c, 0) + 1; n += 1
REC["watch_s"] = round(time.time() - t0, 1); REC["watch_requests"] = n; REC["watch_codes"] = codes
g = w.guest(CG + '''
rm -f /root/r762w.on; sleep 3
echo "anon_max=$(cat /root/r762w.max)"
echo "memory.max=$(cat $CG/memory.max)"
grep -E "^(oom|oom_kill) " $CG/memory.events
docker inspect -f "restarts={{.RestartCount}} oomkilled={{.State.OOMKilled}} status={{.State.Status}} health={{.State.Health.Status}} started={{.State.StartedAt}}" wger wger-files
docker logs wger 2>&1 | grep -c "Booting worker"
rm -f /root/r762w.*''')
say("watch:\n" + g)
REC["watch"] = g
m = re.search(r"anon_max=(\d+)", g)
if m:
a = int(m.group(1)); REC["anon_max_bytes"] = a
REC["anon_max_mib"] = round(a / 1048576, 1); REC["anon_pct_of_384M"] = round(100 * a / (384 * 1048576), 1)
logs = w.guest("docker logs wger 2>&1 | tail -n 300")
pw = tok.get("pw") or ""
if pw:
logs = logs.replace(pw, "<generated, redacted>")
open(f"{EVD}/wger.log", "w").write(logs)
json.dump(REC, open(f"{EVD}/box-verdict-wger.json", "w"), indent=2, ensure_ascii=False)
say("RECORD", json.dumps({k: REC[k] for k in REC if k not in ("container", "watch")}, ensure_ascii=False))
elif phase == "remove":
say(f"remove -> {w.remove(APP)}")
g = w.guest("docker ps -a --format '{{.Names}}' | grep -i wger; echo vols:; docker volume ls --format '{{.Name}}' | grep -i wger; "
"echo stackdir:; ls -la /opt/docker/stacks/wger; echo end")
say("after remove:\n" + g)
st = w.stack(APP)
say(f"stack: deployed={st.get('deployed')} state={st.get('state')}")