d9147b02de
gates / gates (push) Successful in 3m55s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
153 lines
7.8 KiB
Python
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')}")
|