e6d1ebd152
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
300 lines
20 KiB
Python
300 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""fg_life.py <phase> — Grimmory's and MeTube's record rows on scratch 9202 (drill catalog, controller 0.287.0), behind the
|
|
family gate, through the product's own endpoints. The family gate is passed as the household (box_walk.family_cookie);
|
|
strangers go through traefik with no cookie. Secrets never printed; seed tokens in a 0600 scratch file. Env: SC, EV.
|
|
|
|
grim-seed — Grimmory: a library + a real EPUB through its own API (the admin the household made at setup), read back.
|
|
metube-fresh — MeTube removed (with its data), the DRILL template moved back to 2026.09.28, a FRESH install with the
|
|
memory sampler and a stranger's poll from the press; the seed (a download) and its read-back; checks.
|
|
metube-step — the product's guarded Update 2026.09.28 -> 2026.09.29 (drill ladder entry), the download read back.
|
|
life — the night chain; both apps removed KEEPING backups (family-gate files go); the restore button for each;
|
|
the seeds read back; the family-gate files and the stranger's refusal come back (the R-773 lesson).
|
|
final — both removed WITH their data and backups; nothing left.
|
|
Evidence: $EV/B/box/<phase>.txt (+ mem-metube.csv, poll-metube.txt, box-verdict-metube.json)."""
|
|
import json, os, re, secrets, subprocess, sys, time
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from fg import * # noqa: F401,F403 (lan, tun, w, DOM, CTL)
|
|
import upgrade_fixtures_box as fixtures
|
|
|
|
phase = sys.argv[1]
|
|
EVD = f"{w.EV}/B/box"; os.makedirs(EVD, exist_ok=True)
|
|
TOK = w.SC + "/seed-tokens-fg.json"
|
|
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
|
|
SAMPLER = open("/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/new-apps-2026-10-01/tools/sampler.sh").read()
|
|
log = open(f"{EVD}/{phase}.txt", "a", buffering=1)
|
|
GM, MT = fixtures.FIXTURES["grimmory"], fixtures.FIXTURES["metube"]
|
|
APPS = {"grimmory": ("library", GM), "metube": ("video", MT)}
|
|
|
|
|
|
def say(*a):
|
|
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
|
|
|
|
|
|
def toks():
|
|
return json.load(open(TOK)) if os.path.exists(TOK) else {}
|
|
|
|
|
|
def save(t):
|
|
json.dump(t, open(TOK, "w")); os.chmod(TOK, 0o600)
|
|
|
|
|
|
def stranger(sub, path="/"):
|
|
c1 = lan(f"{sub}.{DOM}", path)[0]
|
|
c2 = tun(f"{sub}.{DOM}", path, "198.51.100.77")[0]
|
|
return f"LAN {c1} | tunnel {c2}"
|
|
|
|
|
|
def gate_files():
|
|
return w.guest("ls /opt/docker/stacks/traefik/dynamic/ | grep -E 'family-gate|setup-gate' | tr '\\n' ' '").strip() or "none"
|
|
|
|
|
|
def ctlver():
|
|
return w.guest("docker inspect felhom-controller --format {{.Config.Image}}").strip()
|
|
|
|
|
|
def drill_set_metube(ref, entry=None):
|
|
subprocess.run(["git", "-C", D, "pull", "-q", "--rebase", "origin", "main"], check=True)
|
|
comp, fy = f"{D}/templates/metube/docker-compose.yml", f"{D}/templates/metube/.felhom.yml"
|
|
s = re.sub(r"(image:\s*)ghcr\.io/alexta69/metube:\S+", r"\g<1>" + ref, open(comp).read())
|
|
open(comp, "w").write(s)
|
|
if entry:
|
|
f = open(fy).read()
|
|
f = (f.rstrip("\n") + "\n - " + json.dumps(entry) + "\n") if "update_ladder:" in f else (f.rstrip("\n") + "\nupdate_ladder:\n - " + json.dumps(entry) + "\n")
|
|
open(fy, "w").write(f)
|
|
if subprocess.run(["git", "-C", D, "diff", "--quiet"]).returncode == 0:
|
|
say(f"drill: already pins {ref} — nothing to commit")
|
|
return
|
|
subprocess.run(["git", "-C", D, "commit", "-q", "-am", f"DRILL metube: {ref} (box proof, family gate 2026-10-02)"], check=True)
|
|
subprocess.run(["git", "-C", D, "push", "-q", "origin", "main"], check=True, capture_output=True)
|
|
say("drill:", subprocess.run(["git", "-C", D, "log", "--oneline", "-1"], capture_output=True, text=True).stdout.strip())
|
|
|
|
|
|
def remove(app, data):
|
|
w.ctl("POST", f"/api/stacks/{app}/stop")
|
|
for _ in range(24):
|
|
time.sleep(5)
|
|
if w.stack(app).get("state") != "running":
|
|
break
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": data, "remove_backups": data})
|
|
say(f"remove {app} ({'WITH data and backups' if data else 'KEEPING data and backups'}) -> {code} {str(d)[:260]}")
|
|
if data and code == "409":
|
|
# R-442's fail-closed guard: 9202 has no agent (`/api/disks` -> agent not configured), so the drive path never
|
|
# resolves and "remove with data" is REFUSED, the app kept. Measured here, then the household's other choice is
|
|
# taken and this harness tidies ITS OWN test folder by name (scratch guest; the with-data half is a demo-box row).
|
|
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": True})
|
|
say(f" refused (R-442, no agent on 9202) — remove {app} KEEPING drive data -> {code} {str(d)[:200]}")
|
|
w.guest(f"rm -rf /mnt/felhom-drives/scratch_hdd/userdata/{app}")
|
|
say(f" the harness removed its own test folder /mnt/felhom-drives/scratch_hdd/userdata/{app} by hand")
|
|
time.sleep(6)
|
|
left = w.guest(f"ls -d /opt/docker/stacks/{app} 2>/dev/null; docker ps -a --format '{{{{.Names}}}}' | grep -E '^{app}'; "
|
|
f"docker volume ls -q | grep -i {app}; ls -d /mnt/felhom-drives/*/userdata/{app} 2>/dev/null").strip()
|
|
say(f" left after: {' | '.join(left.splitlines()) or 'nothing'}")
|
|
return code
|
|
|
|
|
|
w.login()
|
|
say(f"##### {phase} on 9202 — controller {ctlver()} | catalog "
|
|
+ w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git log --oneline -1").strip()[:60]
|
|
+ f" | {time.strftime('%FT%TZ', time.gmtime())}")
|
|
|
|
if phase == "grim-seed":
|
|
t = toks()
|
|
pw = json.load(open(f"{w.SC}/gm.json"))["admin_pw"]
|
|
code, tok = GM._token(w, "library", "admin", pw)
|
|
auth = ["-H", f"Authorization: Bearer {tok}"]
|
|
rc, code, out = w.app_curl("library", "/api/v1/libraries", *auth, "-H", "Content-Type: application/json",
|
|
data=json.dumps({"name": "Konyvek", "icon": "book", "iconType": "LUCIDE", "paths": [{"path": "/books"}],
|
|
"watch": True, "metadataSource": "EMBEDDED", "organizationMode": "BOOK_PER_FILE"}), method="POST")
|
|
lib = json.loads(out) if code in ("200", "201") else {}
|
|
if not lib: # a library from an earlier run
|
|
rc, code, out = w.app_curl("library", "/api/v1/libraries", *auth)
|
|
lib = (json.loads(out) or [{}])[0]
|
|
lid, pid = lib["id"], lib["paths"][0]["id"]
|
|
title = "Felhom Csalad " + secrets.token_hex(4)
|
|
import tempfile
|
|
fn = tempfile.mktemp(suffix=".epub"); open(fn, "wb").write(GM._epub(title))
|
|
rc, code, out = w.app_curl("library", "/api/v1/files/upload", *auth, "-F", f"file=@{fn};type=application/epub+zip",
|
|
"-F", f"libraryId={lid}", "-F", f"pathId={pid}", method="POST")
|
|
os.unlink(fn)
|
|
say(f"grimmory: library {lid}, the household uploads an EPUB through its own API (behind the family gate) -> {code}")
|
|
t["grimmory"] = {"user": "admin", "pw": pw, "title": title}
|
|
save(t)
|
|
say("grimmory: the seed reads back:", GM.verify(w, "library", t["grimmory"], say))
|
|
say("2.2/2.8 the book on the household's drive:", " | ".join(w.guest(
|
|
"find /mnt/felhom-drives -path '*userdata*grimmory*' -name '*.epub' 2>/dev/null | head -3").strip().splitlines()) or "NOT FOUND")
|
|
say("2.7 sizes:", " ".join(w.guest("for v in $(docker volume ls -q | grep -i grimmory); do echo $v=$(du -sh $(docker volume inspect -f '{{.Mountpoint}}' $v) | cut -f1); done; "
|
|
"du -sh /mnt/felhom-drives/*/userdata/grimmory 2>/dev/null").split()))
|
|
|
|
elif phase == "metube-fresh":
|
|
if w.stack("metube").get("deployed"):
|
|
remove("metube", True)
|
|
drill_set_metube("ghcr.io/alexta69/metube:2026.09.28")
|
|
w.sync_rescan()
|
|
for _ in range(24):
|
|
if "2026.09.28" in w.guest("grep image: /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/metube/docker-compose.yml"):
|
|
break
|
|
time.sleep(5); w.sync_rescan()
|
|
say("the box's catalog now pins:", w.guest("grep image: /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache/templates/metube/docker-compose.yml").strip())
|
|
say("guest swap:", " ".join(w.guest("free -m | grep -i swap").split()), "(the swap-0 venue for 5.1 is the bench)")
|
|
w.guest(f"cat > /root/fg-sampler.sh <<'SEOF'\n{SAMPLER}\nSEOF\nrm -f /root/fg-mem-metube.csv.stop; "
|
|
"(NAMES='metube' OUT=/root/fg-mem-metube.csv DUR=1500 nohup bash /root/fg-sampler.sh >/dev/null 2>&1 &)")
|
|
w.guest(f"""cat > /root/fg-poll.sh <<'PEOF'
|
|
out=/root/fg-poll-metube.txt; : > $out; t0=$(date +%s)
|
|
while [ $(( $(date +%s) - t0 )) -lt 240 ]; do
|
|
a=$(curl -sk --max-time 4 -o /dev/null -w '%{{http_code}}' -H 'Host: video.{DOM}' https://127.0.0.1/)
|
|
b=$(curl -sk --max-time 4 -o /dev/null -w '%{{http_code}}' -H 'Host: video.{DOM}' -H 'Content-Type: application/json' -X POST --data '{{"url":"https://example.com/x.mp4"}}' https://127.0.0.1/add)
|
|
echo "$(date +%H:%M:%S) $a $b" >> $out; sleep 1
|
|
done
|
|
PEOF
|
|
(nohup sh /root/fg-poll.sh >/dev/null 2>&1 &)""")
|
|
t0 = time.time(); ok = w.deploy("metube", "video"); say(f"deploy metube -> {ok} ({round(time.time()-t0)} s to `deployed`)")
|
|
say(" gate files right after the deploy:", gate_files())
|
|
healthy = None
|
|
for _ in range(150):
|
|
h = w.guest("docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}nohc{{end}}' metube 2>/dev/null").strip()
|
|
if h == "healthy":
|
|
healthy = time.time() - t0; break
|
|
time.sleep(2)
|
|
say(f"4.3 healthy {round(healthy) if healthy else 'NEVER'} s after the press; restarts:",
|
|
w.guest("docker inspect -f '{{.RestartCount}}' metube").strip(), "| state:", w.stack("metube").get("state"),
|
|
"| image:", w.guest("docker inspect -f '{{.Config.Image}}' metube").strip())
|
|
t = toks(); t["metube"] = MT.seed(w, "video", say); save(t)
|
|
say("C1 the seed reads back:", MT.verify(w, "video", t["metube"], say) if t["metube"] else "NO SEED " + str(getattr(MT, "tried", "")))
|
|
time.sleep(3); w.guest("pkill -f fg-poll.sh || true")
|
|
poll = w.guest("cat /root/fg-poll-metube.txt").strip().splitlines()
|
|
codes = {}
|
|
for l in poll:
|
|
k = " ".join(l.split(" ")[1:3]); codes[k] = codes.get(k, 0) + 1
|
|
say(f"3.3/3.5 a stranger's {len(poll)} polls (GET / and POST /add, 1/s) from the press, no cookie: codes {codes}; "
|
|
f"any app answer (200): {any(' 200' in l for l in poll)}")
|
|
open(f"{EVD}/poll-metube.txt", "w").write("\n".join(poll) + "\n")
|
|
say("2.2/2.8 where the download landed (the household's drive, browsable):", " | ".join(w.guest(
|
|
"find /mnt/felhom-drives -path '*userdata*' -path '*metube*' -name '*.mp4' 2>/dev/null | head -3").strip().splitlines()) or "NOT FOUND",
|
|
"| owner:", w.guest("find /mnt/felhom-drives -path '*userdata*' -path '*metube*' -name '*.mp4' -exec stat -c '%u:%g %a' {} \\; 2>/dev/null | head -1").strip())
|
|
say("2.7 sizes:", " ".join(w.guest("for v in $(docker volume ls -q | grep -i metube); do echo $v=$(du -sh $(docker volume inspect -f '{{.Mountpoint}}' $v) | cut -f1); done; "
|
|
"du -sh /mnt/felhom-drives/*/userdata/metube 2>/dev/null").split()))
|
|
say("1.5 process:", w.guest("docker exec metube sh -c 'cat /proc/1/cmdline | tr \"\\000\" \" \"'").strip()[:200],
|
|
"| children:", " ".join(w.guest("docker exec metube ps -eo user,comm 2>/dev/null | sort -u | head -8").split())[:200])
|
|
rc, code, body = w.app_curl("video", "/felhom-no-such-page-xyz/")
|
|
say(f"1.8 unknown page (a member): http={code} traceback={'Traceback' in body}")
|
|
say("0.4 outbound connections held at rest:", " ".join(w.guest(
|
|
"docker exec metube sh -c \"cat /proc/net/tcp /proc/net/tcp6 2>/dev/null\" | awk 'NR>1 && $4==\"01\"{print $3}' | sort -u | head -8").split()) or "none")
|
|
say("0.4/0.5 hosts named in the first-start log:", sorted(set(re.findall(r"https?://([a-z0-9.-]+\.[a-z]{2,})", w.guest("docker logs metube 2>&1 | head -300"))))[:12])
|
|
say("4.4 negative control: docker STOP metube — state before:", w.stack("metube").get("state"))
|
|
w.guest("docker stop metube >/dev/null"); seen = []
|
|
for i in range(12):
|
|
time.sleep(5); s = w.stack("metube").get("state")
|
|
if not seen or seen[-1][1] != s: seen.append((5 * (i + 1), s))
|
|
say(f" states while stopped (s, state): {seen}; a member at / -> {w.app_curl('video', '/')[1]}")
|
|
w.guest("docker start metube >/dev/null")
|
|
for i in range(36):
|
|
time.sleep(5)
|
|
if w.stack("metube").get("state") == "running":
|
|
say(f" running again {5*(i+1)} s after docker start"); break
|
|
|
|
elif phase == "metube-step":
|
|
t = toks()
|
|
before = (w.stack("metube").get("app_config") or {}).get("pinned_images")
|
|
say(f"before: pinned={before}")
|
|
entry = {"from": {"metube": "ghcr.io/alexta69/metube:2026.09.28"}, "to": {"metube": "ghcr.io/alexta69/metube:2026.09.29"},
|
|
"verdict": "proven", "tested_at": "DRILL", "harness_version": 4, "evidence": "DRILL (box proof in progress)",
|
|
"marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}}
|
|
drill_set_metube("ghcr.io/alexta69/metube:2026.09.29", entry)
|
|
w.sync_rescan("metube", "ghcr.io/alexta69/metube:2026.09.29")
|
|
since = w.guest("date -u +%Y-%m-%dT%H:%M:%SZ").strip()
|
|
res = w.press_update("metube", poll=1, cap_s=1800)
|
|
for p in res.get("phases", []):
|
|
log.write(f" phase +{p['t']}s {p['phase']} | err={p['error']}\n")
|
|
time.sleep(10)
|
|
read = MT.verify(w, "video", t["metube"], say)
|
|
log.write(w.guest(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'update metube' | grep -v DEBUG | cut -c1-300") + "\n")
|
|
st = w.stack("metube"); after = (st.get("app_config") or {}).get("pinned_images")
|
|
say(" gate files after the step:", gate_files(), "| a stranger at / ->", stranger("video"))
|
|
verdict = {"app": "metube", "venue": f"box 9202 (drill catalog, controller {ctlver().rsplit(':', 1)[-1]}), the product's guarded Update",
|
|
"from": before, "to": after,
|
|
"verdict": "proven" if (res.get("final_phase") == "done" and read and (after or {}).get("metube") == "ghcr.io/alexta69/metube:2026.09.29") else "failed",
|
|
"seed_read_before": True, "seed_read_after": read, "healthy_after": st.get("state") == "running",
|
|
"duration_s": res.get("duration_s"), "final_phase": res.get("final_phase"), "measured_at": since,
|
|
"evidence": "felhom.eu/documentation/audits/family-gate-2026-10-02/B/box/metube-step.txt"}
|
|
json.dump(verdict, open(f"{EVD}/box-verdict-metube.json", "w"), indent=2)
|
|
say(f"RESULT final_phase={res.get('final_phase')} after={after} seed_after={read} verdict={verdict['verdict']} ({res.get('duration_s')} s)")
|
|
|
|
elif phase == "grim-step":
|
|
# 9202's Grimmory was installed at v3.4.1 (the drill template kept yesterday's FROM); step it to the published v3.5.0
|
|
# through the product's guarded Update, behind the family gate, and read the seed back.
|
|
t = toks()
|
|
before = (w.stack("grimmory").get("app_config") or {}).get("pinned_images")
|
|
say(f"before: pinned={before}")
|
|
subprocess.run(["git", "-C", D, "pull", "-q", "--rebase", "origin", "main"], check=True)
|
|
comp, fy = f"{D}/templates/grimmory/docker-compose.yml", f"{D}/templates/grimmory/.felhom.yml"
|
|
src = open(comp).read() # read BEFORE opening for write — the one-line form truncated the file first (attempt 1)
|
|
assert "grimmory:v3.4.1" in src, "drill compose is not at v3.4.1"
|
|
open(comp, "w").write(src.replace("grimmory:v3.4.1", "grimmory:v3.5.0"))
|
|
entry = {"from": {"grimmory": "ghcr.io/grimmory-tools/grimmory:v3.4.1", "grimmory-db": "mariadb:11.4"},
|
|
"to": {"grimmory": "ghcr.io/grimmory-tools/grimmory:v3.5.0", "grimmory-db": "mariadb:11.4"},
|
|
"verdict": "proven", "tested_at": "DRILL", "harness_version": 4, "evidence": "DRILL (box proof in progress)",
|
|
"marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}}
|
|
f = open(fy).read()
|
|
f = (f.rstrip("\n") + "\n - " + json.dumps(entry) + "\n") if "update_ladder:" in f else (f.rstrip("\n") + "\nupdate_ladder:\n - " + json.dumps(entry) + "\n")
|
|
open(fy, "w").write(f)
|
|
subprocess.run(["git", "-C", D, "commit", "-q", "-am", "DRILL grimmory: v3.5.0 (box step behind the family gate, 2026-10-02)"], check=True)
|
|
subprocess.run(["git", "-C", D, "push", "-q", "origin", "main"], check=True, capture_output=True)
|
|
say("drill:", subprocess.run(["git", "-C", D, "log", "--oneline", "-1"], capture_output=True, text=True).stdout.strip())
|
|
w.sync_rescan("grimmory", "ghcr.io/grimmory-tools/grimmory:v3.5.0")
|
|
res = w.press_update("grimmory", poll=1, cap_s=1800)
|
|
for p in res.get("phases", []):
|
|
log.write(f" phase +{p['t']}s {p['phase']} | err={p['error']}\n")
|
|
time.sleep(10)
|
|
read = GM.verify(w, "library", t["grimmory"], say)
|
|
after = (w.stack("grimmory").get("app_config") or {}).get("pinned_images")
|
|
say(" gate files after the step:", gate_files(), "| a stranger at / ->", stranger("library"),
|
|
"| OPDS exception, a stranger ->", stranger("library", "/api/v1/opds"))
|
|
say(f"RESULT final_phase={res.get('final_phase')} after={after} seed_after={read} ({res.get('duration_s')} s)")
|
|
|
|
elif phase == "life":
|
|
t = toks()
|
|
say("before: gate files:", gate_files())
|
|
code, d = w.ctl("POST", "/api/debug/backup/night-chain"); say("2.5 the night chain (debug action) ->", code)
|
|
for _ in range(120):
|
|
if "finished in" in w.guest("docker logs --since 10m felhom-controller 2>&1 | grep 'night-chain\\] finished' | tail -1"):
|
|
break
|
|
time.sleep(5)
|
|
say(" ", w.guest("docker logs --since 10m felhom-controller 2>&1 | grep 'night-chain\\] finished' | tail -1").strip()[:200])
|
|
for app in APPS:
|
|
say(f" {app} restore points:", [(s.get("id") or s.get("snapshot_id"), s.get("time") or s.get("created")) for s in w.snapshots(app)][:2])
|
|
for app in APPS:
|
|
remove(app, False)
|
|
time.sleep(8)
|
|
say("after both removes: gate files:", gate_files(), "| a stranger at library/ ->", stranger("library"), "| video/ ->", stranger("video"))
|
|
for app, (sub, fx) in APPS.items():
|
|
r = w.restore(app)
|
|
say(f"2.5 restore {app}:", {k: r.get(k) for k in ("ok", "snapshot_id", "seconds", "state_after", "hold_after")})
|
|
time.sleep(10)
|
|
say("after both restores: gate files:", gate_files())
|
|
say(" family_gate record:", " | ".join(" ".join(w.guest(f"grep -A4 '^family_gate:' /opt/docker/stacks/{a}/app.yaml").split()) for a in APPS))
|
|
say(" a STRANGER (no cookie): library/ ->", stranger("library"), "| library/api/v1/books ->", stranger("library", "/api/v1/books"),
|
|
"| video/ ->", stranger("video"), "| video/history ->", stranger("video", "/history"))
|
|
for app, (sub, fx) in APPS.items():
|
|
say(f"2.5 {app}: the seed reads back after remove + restore (a member, then the app's own login):", fx.verify(w, sub, t[app], say))
|
|
|
|
elif phase == "final":
|
|
for app in APPS:
|
|
remove(app, True)
|
|
time.sleep(8)
|
|
say("gate files:", gate_files())
|
|
w.guest("touch /root/fg-mem-metube.csv.stop"); time.sleep(3)
|
|
csv = w.guest("cat /root/fg-mem-metube.csv 2>/dev/null").strip().splitlines()
|
|
if csv:
|
|
open(f"{EVD}/mem-metube.csv", "w").write("epoch,name,id,anon,current,swap,oom_kill,limit,restarts\n" + "\n".join(csv) + "\n")
|
|
per = {}
|
|
for l in csv:
|
|
p = l.split(",")
|
|
if len(p) >= 9 and p[3].isdigit():
|
|
d = per.setdefault((p[1], p[2]), {"anon": 0, "oom": 0, "lim": p[7], "n": 0})
|
|
d["anon"] = max(d["anon"], int(p[3])); d["n"] += 1
|
|
d["oom"] = max(d["oom"], int(p[6]) if p[6].isdigit() else 0)
|
|
for (n, i), d in per.items():
|
|
say(f"5.1 (box) {n} [{i}] {d['n']} samples: peak anon {d['anon']/1048576:.1f} MiB = "
|
|
f"{100.0*d['anon']/int(d['lim']) if d['lim'].isdigit() else -1:.1f} % of the limit; oom_kill {d['oom']}")
|
|
w.guest("rm -f /root/fg-sampler.sh /root/fg-poll.sh /root/fg-poll-metube.txt /root/fg-mem-metube.csv /root/fg-mem-metube.csv.stop")
|