#!/usr/bin/env python3 """newapp_box.py — a NEW app's checklist measurements on scratch guest 9202 (drill catalog), through the product's own endpoints. Evidence under $EV/box//. install — the memory sampler (every named container, from its birth) and a STRANGER's poll of the app (no dashboard session, no gate cookie, through traefik, once a second) start BEFORE the deploy press; then deploy, the seed through the app's own route and its read-back (C1). Seed tokens + this run's deploy secrets go to the 0600 file in SC (boxstep.py reads them). [checklist 3.5, 4.3, 5.1 (box; swap ON here — the bench is the swap-0 venue), 9.2] checks — the app-specific checks (CHECKS[app]) + the generic ones: an unknown page (1.8), the negative control (docker pause → the controller's state, 4.4), the server process (1.5). remove — stop + remove through the product: first REMOVE_HDD=0 then reinstall-free: whatever is left is listed (2.6). Secrets are never printed. """ import json, os, re, secrets, sys, time import walk as w sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") import upgrade_fixtures_box as fixtures, upgrade_fixtures_box28 as _f28 FX = dict(_f28.FIXTURES28); FX.update(fixtures.FIXTURES) phase, app, sub = sys.argv[1:4] EVD = f"{w.EV}/box/{app}"; os.makedirs(EVD, exist_ok=True) TOK = w.SC + "/seed-tokens-box.json" log = open(f"{EVD}/{phase}.txt", "a", buffering=1) HOST = f"{sub}.{w.DOMAIN}" def say(*a): w.say(*a); log.write(" ".join(map(str, a)) + "\n") def containers(): return [l for l in w.guest(f"docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'").split() if l] # ---- app-specific checks ----------------------------------------------------------------------------------------- def checks_radicale(t): user, pw = t["user"], t["pw"] base = ["-sk", "--max-time", "10", "-H", f"Host: {HOST}"] def c(*a): r = w.sh(["curl"] + base + ["-o", "/dev/null", "-w", "%{http_code} %{redirect_url}"] + list(a)) return (r.stdout or "").strip() say("0.7/3.9 a phone calendar client's discovery, through traefik over https (DAVx5 / iOS shape):") say(" PROPFIND /.well-known/caldav (no auth) ->", c("-X", "PROPFIND", f"{w.BASE}/.well-known/caldav")) say(" PROPFIND /.well-known/carddav (no auth) ->", c("-X", "PROPFIND", f"{w.BASE}/.well-known/carddav")) say(" PROPFIND / Depth:0, right login ->", c("-u", f"{user}:{pw}", "-X", "PROPFIND", "-H", "Depth: 0", f"{w.BASE}/")) body = w.sh(["curl"] + base + ["-u", f"{user}:{pw}", "-X", "PROPFIND", "-H", "Depth: 0", "-H", "Content-Type: application/xml", "--data", '', f"{w.BASE}/"]).stdout or "" m = re.search(r"<(?:\w+:)?href>([^<]+)\s*", body) say(" current-user-principal ->", m.group(1) if m else body[:160]) say(" PROPFIND principal Depth:1 (the calendar list), right login ->", c("-u", f"{user}:{pw}", "-X", "PROPFIND", "-H", "Depth: 1", f"{w.BASE}/{user}/")) say(" the same with a WRONG password ->", c("-u", f"{user}:{pw}x", "-X", "PROPFIND", "-H", "Depth: 1", f"{w.BASE}/{user}/")) say(" MKCOL an address book + PUT a contact + GET it back (CardDAV):") ab = "kontakt-" + secrets.token_hex(3) mk = ('' '') say(" MKCOL ->", c("-u", f"{user}:{pw}", "-X", "MKCOL", "-H", "Content-Type: application/xml", "--data", mk, f"{w.BASE}/{user}/{ab}/")) vcf = "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:c1@felhom\r\nFN:Teszt Elek\r\nN:Elek;Teszt;;;\r\nEND:VCARD\r\n" say(" PUT vcf ->", c("-u", f"{user}:{pw}", "-X", "PUT", "-H", "Content-Type: text/vcard", "--data-binary", vcf, f"{w.BASE}/{user}/{ab}/c1.vcf")) g = w.sh(["curl"] + base + ["-u", f"{user}:{pw}", f"{w.BASE}/{user}/{ab}/c1.vcf"]).stdout or "" say(" GET vcf contains FN:Teszt Elek ->", "FN:Teszt Elek" in g) say("3.6 lock-out: 15 wrong passwords for the user's name as a stranger, then the right one:") t0 = time.time(); codes = [] for _ in range(15): codes.append(c("-u", f"{user}:wrong{secrets.token_hex(2)}", "-X", "PROPFIND", "-H", "Depth: 0", f"{w.BASE}/{user}/").split()[0]) dt = time.time() - t0 say(f" 15 wrong -> {sorted(set(codes))} in {dt:.1f} s ({dt/15:.2f} s each — Radicale's own delay)") say(" right password at once ->", c("-u", f"{user}:{pw}", "-X", "PROPFIND", "-H", "Depth: 0", f"{w.BASE}/{user}/")) say(" wrong password for ANOTHER name (admin) ->", c("-u", "admin:admin", "-X", "PROPFIND", "-H", "Depth: 0", f"{w.BASE}/admin/")) say("3.4 sign-up: Radicale has no sign-up route; a stranger's MKCOL on a new principal ->", c("-X", "MKCOL", f"{w.BASE}/stranger{secrets.token_hex(2)}/")) say("2.2/2.7 where the data is, and its size:", " ".join(w.guest( f"for v in $(docker volume ls -q | grep -i {app}); do echo $v=$(du -sh $(docker volume inspect -f '{{{{.Mountpoint}}}}' $v) | cut -f1); done").split())) say(" the login file inside (mode, owner — never its content):", w.guest(f"docker exec {app} ls -l /var/lib/radicale/users").strip()) def checks_karakeep(t): base = ["-sk", "--max-time", "15", "-H", f"Host: {HOST}"] def c(*a): r = w.sh(["curl"] + base + ["-o", "/dev/null", "-w", "%{http_code}"] + list(a)) return (r.stdout or "").strip() def trpc_stranger(path, payload): return c("-H", "Content-Type: application/json", "-X", "POST", "--data", json.dumps({"0": {"json": payload}}), f"{w.BASE}{path}") st = w.stack(app) say("before the press: gate closed? a stranger's GET / ->", c(f"{w.BASE}/"), "| controller:", st.get("setup_gate") or st.get("gate")) pw = "Str-" + secrets.token_hex(8) say("3.3 BEFORE the household's press, a stranger's users.create ->", trpc_stranger("/api/trpc/users.create?batch=1", {"name": "x", "email": f"s{secrets.token_hex(2)}@x.invalid", "password": pw, "confirmPassword": pw})) code, d = w.ctl("POST", f"/apps/{app}/setup-gate/open", {}) say("the household presses „Kész, beállítottam\" ->", code, str(d)[:160]) for i in range(36): time.sleep(5) env = w.guest(f"docker inspect {app} --format '{{{{range .Config.Env}}}}{{{{println .}}}}{{{{end}}}}' | grep -E '^DISABLE_SIGNUPS='").strip() if env.endswith("true"): break say("after_setup: the app's own switch now", env, f"(after {5*(i+1)} s)") w.wait_app(sub, "/api/health", want=("200",), tries=60) say("3.4 a STRANGER after the setup (no session, no gate cookie, through traefik):") for label, path in (("users.create", "/api/trpc/users.create?batch=1"), ("batched whoami,users.create", "/api/trpc/users.whoami,users.create?batch=1"), ("upper-case path", "/API/TRPC/USERS.CREATE?batch=1"), ("double slash", "//api/trpc/users.create?batch=1")): say(f" {label} ->", trpc_stranger(path, {"name": "x", "email": f"s{secrets.token_hex(2)}@x.invalid", "password": pw, "confirmPassword": pw})) say(" GET /signup ->", c(f"{w.BASE}/signup"), "| GET /SignUp ->", c(f"{w.BASE}/SignUp")) n = w.guest(f"docker exec {app} sh -c 'ls /data'").split() say(" the app's own switch, asked straight (inside the container, past traefik):", w.guest( f"""docker exec {app} sh -c "wget -q -O - --header 'Content-Type: application/json' --post-data '{{\\"0\\":{{\\"json\\":{{\\"name\\":\\"x\\",\\"email\\":\\"in@x.invalid\\",\\"password\\":\\"{pw}\\",\\"confirmPassword\\":\\"{pw}\\"}}}}}}' 'http://127.0.0.1:3000/api/trpc/users.create?batch=1' 2>&1 | head -c 200" """).strip()[:220]) say("3.6 lock-out: 12 wrong apiKeys.exchange for the household's email as a stranger, then the right one:") codes = [trpc_stranger("/api/trpc/apiKeys.exchange?batch=1", {"keyName": "x", "email": t["email"], "password": "wrong" + secrets.token_hex(2)}) for _ in range(12)] say(" 12 wrong ->", sorted(set(codes)), "| right at once ->", trpc_stranger("/api/trpc/apiKeys.exchange?batch=1", {"keyName": "y", "email": t["email"], "password": t["pw"]})) key = fixtures.Karakeep()._key(w, sub, t) rc, code, out = w.app_curl(sub, "/api/v1/bookmarks", "-H", f"Authorization: Bearer {key}", "-H", "Content-Type: application/json", data=json.dumps({"type": "link", "url": "https://example.com/"}), method="POST") # felhom.eu resolves to a LAN address here bid = (json.loads(out) if code in ("200", "201") else {}).get("id") say("0.5/5.2 a LINK bookmark (the crawler + Chrome): POST ->", code) for _ in range(24): time.sleep(5) rc, c2, o2 = w.app_curl(sub, f"/api/v1/bookmarks/{bid}", "-H", f"Authorization: Bearer {key}") try: cont = json.loads(o2).get("content") or {} except Exception: cont = {} if cont.get("crawledAt") or cont.get("title"): break say(" crawled:", {k: (str(v)[:60] if v else v) for k, v in cont.items() if k in ("title", "crawledAt", "favicon", "imageUrl")}, "| taggingStatus:", (json.loads(o2) if o2.startswith("{") else {}).get("taggingStatus")) say("0.4 AI is off: OPENAI_API_KEY in the container is", "EMPTY" if w.guest(f"docker inspect {app} --format '{{{{range .Config.Env}}}}{{{{println .}}}}{{{{end}}}}' | grep -c '^OPENAI_API_KEY=$'").strip() == "1" else "SET (!)", "| the app's log on inference:", " ".join(w.guest(f"docker logs {app} 2>&1 | grep -i -E 'inference|openai|tagging' | tail -3").split())[:300]) say("2.2/2.7 data and size:", " ".join(w.guest( f"for v in $(docker volume ls -q | grep -i {app}); do echo $v=$(du -sh $(docker volume inspect -f '{{{{.Mountpoint}}}}' $v) | cut -f1); done").split())) def checks_dawarich(t): import tempfile pw = (w.GENERATED.get(app) or (t or {}).get("__generated") or {}).get("ADMIN_PASSWORD", "") def web_login(email, password): jar = tempfile.mktemp(prefix="dw-jar-") o = f"https://{HOST}" hdr = ["-H", f"Origin: {o}", "-H", f"Referer: {o}/users/sign_in", "-c", jar, "-b", jar] rc, code, page = w.app_curl(sub, "/users/sign_in", *hdr) m = re.search(r'name="authenticity_token" value="([^"]+)"', page or "") rc, code, body = w.app_curl(sub, "/users/sign_in", *hdr, "--data-urlencode", f"authenticity_token={m.group(1) if m else ''}", "--data-urlencode", f"user[email]={email}", "--data-urlencode", f"user[password]={password}", method="POST") os.path.exists(jar) and os.unlink(jar) locked = "locked" in (body or "").lower() return code + (" (locked text)" if locked else "") say("3.2/3.9 the browser form through traefik over https (Origin + CSRF), as the household:") say(" default demo@dawarich.app / safepassword ->", web_login("demo@dawarich.app", "safepassword")) say(" the box's generated password ->", web_login("demo@dawarich.app", pw), "(303 = signed in)") say(" a wrong password ->", web_login("demo@dawarich.app", "wrong-" + secrets.token_hex(3))) base = ["-sk", "--max-time", "15", "-H", f"Host: {HOST}"] def c(*a): return (w.sh(["curl"] + base + ["-o", "/dev/null", "-w", "%{http_code}"] + list(a)).stdout or "").strip() say("3.4 sign-up as a stranger: GET /users/sign_up ->", c(f"{w.BASE}/users/sign_up"), "| POST /users ->", c("-X", "POST", "--data-urlencode", "user[email]=s@x.invalid", "--data-urlencode", "user[password]=Str-12345678abc", "--data-urlencode", "user[password_confirmation]=Str-12345678abc", f"{w.BASE}/users"), "| users in the app:", w.guest("docker exec dawarich bin/rails runner 'puts User.count' 2>/dev/null | tail -1").strip()) say("3.6 lock-out: 11 wrong passwords for the public name as a stranger, then the household's right one:") codes = [web_login("demo@dawarich.app", "wrong-" + secrets.token_hex(3)) for _ in range(11)] say(" 11 wrong ->", codes[-3:], "| right password now ->", web_login("demo@dawarich.app", pw), "| locked_at:", w.guest("docker exec dawarich bin/rails runner 'puts User.order(:id).first.locked_at.inspect' 2>/dev/null | tail -1").strip()) k = fixtures.Dawarich._key(w) rc, code, _ = w.app_curl(sub, f"/api/v1/points?api_key={k}&start_at=2026-09-01T00:00:00Z&end_at=2026-09-02T00:00:00Z") say(" the phone app's API key while the account is locked ->", code) w.guest("docker exec dawarich bin/rails runner 'User.order(:id).first.unlock_access!' 2>/dev/null") say(" (unlocked again with the app's own CLI for the rest of the walk) right password ->", web_login("demo@dawarich.app", pw)) say("0.4 outbound connections held by the app at rest:", " ".join(w.guest( "docker exec dawarich 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("2.2/2.7 data and size:", " ".join(w.guest( f"for v in $(docker volume ls -q | grep -i {app}); do echo $v=$(du -sh $(docker volume inspect -f '{{{{.Mountpoint}}}}' $v) | cut -f1); done").split())) def checks_grimmory(t): base = ["-sk", "--max-time", "15", "-H", f"Host: {HOST}"] def c(*a): return (w.sh(["curl"] + base + ["-o", "/dev/null", "-w", "%{http_code}"] + list(a)).stdout or "").strip() seen = None for i in range(40): # the gate opens by itself when the probe reads done (no household press) seen = c(f"{w.BASE}/") if seen == "200": break time.sleep(10) say(f"3.3 the gate opened BY ITS PROBE: a stranger's GET / -> {seen} after ~{10*i} s (no press)") say(" controller log:", " ".join(w.guest(f"docker logs --since 30m felhom-controller 2>&1 | grep -i 'setup gate' | grep -i {app} | tail -2 | cut -c1-200").split())[:400]) say("3.4 a stranger's second setup ->", c("-H", "Content-Type: application/json", "-X", "POST", "--data", json.dumps({"username": "x", "password": "Str-12345678abc", "email": "x@x.invalid", "name": "X"}), f"{w.BASE}/api/v1/setup"), "| GET /api/v1/books with no token ->", c(f"{w.BASE}/api/v1/books")) code, tok = fixtures.Grimmory()._token(w, sub, t["user"], t["pw"]) auth = ["-H", f"Authorization: Bearer {tok}"] say("0.7 OPDS for e-readers: switched on in the settings (as the household does) ->", w.app_curl(sub, "/api/v1/settings", *auth, "-H", "Content-Type: application/json", data=json.dumps([{"name": "OPDS_SERVER_ENABLED", "value": True}]), method="PUT")[1], "| an OPDS user ->", w.app_curl(sub, "/api/v2/opds-users", *auth, "-H", "Content-Type: application/json", data=json.dumps({"username": "olvaso", "password": "Op-" + t["pw"][-10:]}), method="POST")[1]) say(" the OPDS feed through traefik over https with basic auth: right ->", c("-u", "olvaso:Op-" + t["pw"][-10:], f"{w.BASE}/api/v1/opds"), "| wrong ->", c("-u", "olvaso:wrong", f"{w.BASE}/api/v1/opds"), "| none ->", c(f"{w.BASE}/api/v1/opds")) say("3.6 lock-out: 12 wrong sign-ins for `admin` as a stranger, then the right one:") codes = [c("-H", "Content-Type: application/json", "-X", "POST", "--data", json.dumps({"username": "admin", "password": "w" + secrets.token_hex(3)}), f"{w.BASE}/api/v1/auth/login") for _ in range(12)] say(" 12 wrong ->", sorted(set(codes)), "| right at once ->", fixtures.Grimmory()._token(w, sub, t["user"], t["pw"])[0]) say("2.2/2.8 the book file on the household's drive (userdata, browsable):", " | ".join(w.guest( "find /mnt/felhom-drives -path '*userdata/media/grimmory*' -name '*.epub' 2>/dev/null | head -3").strip().splitlines()) or "NOT FOUND") say("2.7 sizes:", " ".join(w.guest(f"for v in $(docker volume ls -q | grep -i {app}); do echo $v=$(du -sh $(docker volume inspect -f '{{{{.Mountpoint}}}}' $v) | cut -f1); done; du -sh /mnt/felhom-drives/*/userdata/{app} 2>/dev/null | head -3").split())) say("0.4 outbound connections held at rest:", " ".join(w.guest( f"docker exec {app} 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") CHECKS = {"radicale": checks_radicale, "karakeep": checks_karakeep, "dawarich": checks_dawarich, "grimmory": checks_grimmory} SAMPLER = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "sampler.sh")).read() def install(): w.login() say(f"##### {app} on 9202 — controller", w.guest("docker inspect felhom-controller --format {{.Config.Image}}").strip(), "| catalog", w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git log --oneline -1").strip()) say("guest swap:", " ".join(w.guest("free -m | grep -i swap").split()), "(the swap-0 venue for 5.1 is the bench)") names = os.environ.get("NAMES", app) w.guest(f"cat > /root/na-sampler.sh <<'SEOF'\n{SAMPLER}\nSEOF\nrm -f /root/na-mem-{app}.csv.stop; " f"(NAMES='{names}' OUT=/root/na-mem-{app}.csv DUR=1200 nohup bash /root/na-sampler.sh >/dev/null 2>&1 &)") poll_path = os.environ.get("POLL_PATH", "/") w.guest(f"""cat > /root/na-poll.sh <<'PEOF' out=/root/na-poll-{app}.txt; : > $out; t0=$(date +%s) while [ $(( $(date +%s) - t0 )) -lt 300 ]; do r=$(curl -sk --max-time 4 -o /root/na-poll.body -w '%{{http_code}}' -H 'Host: {HOST}' https://127.0.0.1{poll_path}) echo "$(date +%H:%M:%S) $r $(head -c 70 /root/na-poll.body | tr '\\n' ' ')" >> $out; sleep 1 done PEOF (nohup bash /root/na-poll.sh >/dev/null 2>&1 &)""") t0 = time.time() ok = w.deploy(app, sub) say(f"deploy -> {ok} ({round(time.time()-t0)} s to `deployed`)") healthy = None for _ in range(150): hs = [w.guest(f"docker inspect -f '{{{{if .State.Health}}}}{{{{.State.Health.Status}}}}{{{{else}}}}nohc{{{{end}}}}' {c}").strip() for c in containers()] if hs and all(h in ("healthy", "nohc") for h in hs): healthy = time.time() - t0 break time.sleep(2) say(f"4.3 press -> every container healthy: {round(healthy) if healthy else 'NEVER (300 s)'} s; restarts:", w.guest(f"for c in $(docker ps -a --filter label=com.docker.compose.project={app} --format '{{{{.Names}}}}'); do echo -n \"$c=$(docker inspect -f '{{{{.RestartCount}}}}' $c) \"; done").strip(), "| controller state:", w.stack(app).get("state")) toks = json.load(open(TOK)) if os.path.exists(TOK) else {} if os.environ.get("PROBE_PATH"): # the setup gate's probe, read BEFORE the seed (the household) makes the admin w.wait_app(sub, os.environ["PROBE_PATH"], want=("200",), tries=60) say("3.3 probe BEFORE the first admin:", (w.app_curl(sub, os.environ["PROBE_PATH"])[2] or "")[:120]) toks[app] = FX[app].seed(w, sub, say) if os.environ.get("PROBE_PATH"): say("3.3 probe AFTER the first admin:", (w.app_curl(sub, os.environ["PROBE_PATH"])[2] or "")[:120]) if isinstance(toks[app], dict) and w.GENERATED.get(app): toks[app]["__generated"] = w.GENERATED[app] json.dump(toks, open(TOK, "w"), default=str); os.chmod(TOK, 0o600) say(f"C1 seed reads back: {FX[app].verify(w, sub, toks[app], say) if toks[app] else 'NO SEED: ' + str(getattr(FX[app], 'tried', ''))}") time.sleep(5) w.guest("pkill -f na-poll.sh || true") poll = w.guest(f"cat /root/na-poll-{app}.txt").strip().splitlines() codes = {} for l in poll: k = (l.split(" ") + ["?"])[1] codes[k] = codes.get(k, 0) + 1 say(f"3.5 a stranger's {len(poll)} polls of {poll_path} from the press: codes {codes}; first {poll[:1]}; last {poll[-2:]}") with open(f"{EVD}/poll.txt", "w") as fh: fh.write("\n".join(poll) + "\n") def checks(): w.login() toks = json.load(open(TOK)) if app in CHECKS: CHECKS[app](toks[app]) rc, code, body = w.app_curl(sub, "/felhom-no-such-page-xyz/") say(f"1.8 unknown page: http={code} debug-page={'DEBUG = True' in body or 'Traceback' in body}") for c in containers(): say(f"1.5 process in {c}:", w.guest(f"docker exec {c} sh -c 'cat /proc/1/cmdline | tr \"\\000\" \" \"' 2>/dev/null").strip()[:200]) main = app say("4.4 negative control: docker STOP", main, "(sidecars keep running; pause is not used — Docker counts a paused container as running) — state before:", w.stack(app).get("state")) w.guest(f"docker stop {main} >/dev/null") seen = [] for i in range(18): time.sleep(5) s = w.stack(app).get("state") if not seen or seen[-1][1] != s: seen.append((5 * (i + 1), s)) say(f" states while stopped (s, state): {seen}; the front door: {w.app_curl(sub, '/')[1]}") w.guest(f"docker start {main} >/dev/null") back = None for i in range(36): time.sleep(5) if w.stack(app).get("state") == "running": back = 5 * (i + 1) break say(f" running again {back} s after docker start") def remove(): w.login() w.guest(f"touch /root/na-mem-{app}.csv.stop"); time.sleep(3) csv = w.guest(f"cat /root/na-mem-{app}.csv").strip().splitlines() with open(f"{EVD}/mem-box.csv", "w") as fh: fh.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, "swap": 0, "oom": 0, "lim": p[7], "n": 0}) d["anon"] = max(d["anon"], int(p[3])); d["n"] += 1 d["swap"] = max(d["swap"], int(p[5]) if p[5].isdigit() else 0); 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; peak swap {d['swap']} B; oom_kill {d['oom']}") c1, _ = 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": os.environ.get("REMOVE_HDD") == "1", "remove_backups": True}) say(f"2.6 remove (remove_hdd_data={os.environ.get('REMOVE_HDD') == '1'}) -> {code} {str(d)[:300]}") time.sleep(6) say(" left after: stack dir / containers / volumes:", w.guest( f"ls -d /opt/docker/stacks/{app} 2>/dev/null; docker ps -a --format '{{{{.Names}}}}' | grep -E '^{app}'; docker volume ls -q | grep -i {app}").strip() or "nothing") w.guest(f"rm -f /root/na-sampler.sh /root/na-poll.sh /root/na-poll.body /root/na-poll-{app}.txt /root/na-mem-{app}.csv /root/na-mem-{app}.csv.stop") {"install": install, "checks": checks, "remove": remove}[phase]()