Files
felhom.eu/documentation/audits/usenet-2026-10-12/tools/teardown_usenet.py
T
adminandClaude Opus 5 52615d9139
gates / gates (push) Successful in 6m34s
Website + evidence: the Usenet pair (R-988 closed), and the drill route filed as R-989
The catalog published SABnzbd and NZBGet today (app-catalog 8ee687e). Here:

- two cards in the media category on both language sets, each with the project's own logo and
  each saying IN BOLD that a paid Usenet subscription is the household's own
- the published count 60 -> 62 everywhere it is stated; the category badges are derived from
  the cards now (media read 15 with 17 cards, otthon/home read 14 with 16 — both stale)
- documentation/audits/usenet-2026-10-12/: both bench runs, both box steps and verdicts, the
  nine defects with their measurements, what a missing provider left unmeasured, the teardown
- R-988 closed (moved to CLOSED-ITEMS in this commit); R-989 filed: the drill-catalog route of
  09 §6.5 cannot be followed — that repo is private and no box has catalog credentials, so the
  templates were staged in the box's own catalog cache instead
- walk.app_env: a SECRET field in app.yaml is stored ENC:, so that helper returns ciphertext —
  the docstring says so now, measured the hard way
- STATUS.md and REPORT-usenet-2026-10-12.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-12 22:58:59 +02:00

97 lines
5.2 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Teardown after the Usenet campaign. Three layers, each verified, and the control for the catalog
layer comes from a different channel than the controller's own verdict (R-286).
What this campaign changed on 9202 and therefore has to put back:
1. the two apps are installed -> removed THROUGH THE PRODUCT, volumes and drive folders
2. two templates are STAGED in the cache -> deleted from the cache AND from /opt/docker/stacks
3. the sync interval was raised to 12h -> back to 15m
4. the catalog pointer -> already the live one (the staging needed it); verified
"""
import json
import os
import sys
import time
os.environ.setdefault("SC", "/tmp/usenet-walk")
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools")
import walk as w # noqa: E402
D = "/opt/docker/felhom-controller"
CACHE = D + "/data/catalog-cache/templates"
APPS = ("sabnzbd", "nzbget")
OUT = {}
w.login()
# ---------------------------------------------------------------- 1. the apps, through the product
for app in APPS:
st = w.stack(app)
if not st.get("deployed"):
w.say("[X] %s: not deployed" % app)
OUT[app] = {"was_deployed": False}
continue
code = w.remove(app)
OUT[app] = {"was_deployed": True, "remove_code": code,
"deployed_after": w.stack(app).get("deployed")}
# ---------------------------------------------------------------- 2. the staged templates
before = w.guest("docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip()
for app in APPS:
w.guest("docker exec felhom-controller sh -c 'rm -rf %s/%s'" % (CACHE, app))
w.guest("rm -rf /opt/docker/stacks/%s" % app)
after = w.guest("docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip()
left = w.guest("docker exec felhom-controller sh -c 'ls %s | grep -E \"sab|nzb\" | tr \"\\n\" \" \"'" % CACHE).strip()
stacks_left = w.guest("ls -d /opt/docker/stacks/sabnzbd /opt/docker/stacks/nzbget 2>/dev/null").strip()
OUT["staging"] = {"cache_before": before, "cache_after": after,
"staged_names_left": left or "(none)", "stack_dirs_left": stacks_left or "(none)"}
w.say("[S] cache %s -> %s templates; staged names left: %r; stack dirs left: %r"
% (before, after, left, stacks_left))
# ---------------------------------------------------------------- 3. the sync interval
cur = w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip()
w.guest("docker exec felhom-controller sh -c \"sed -i 's|sync_interval: .*|sync_interval: 15m|' %s/controller.yaml\"" % D)
w.guest("docker restart felhom-controller")
time.sleep(25)
w.login()
now = w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip()
OUT["sync_interval"] = {"before": cur, "after": now}
w.say("[I] sync interval %r -> %r" % (cur, now))
# ---------------------------------------------------------------- 4. the catalog pointer, from a different channel
repo = w.guest("docker exec felhom-controller sh -c 'grep repo_url %s/controller.yaml'" % D).strip()
w.sync_rescan()
time.sleep(10)
import subprocess
CAT = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu"
subprocess.run(["git", "-C", CAT, "fetch", "-q", "origin"], capture_output=True)
checks = {}
for app in ("sabnzbd", "nzbget", "grocy"):
pushed = subprocess.run(["bash", "-c",
"git -C %s show origin/main:templates/%s/.felhom.yml | sha256sum | cut -c1-16" % (CAT, app)],
capture_output=True, text=True).stdout.strip()
onbox = w.guest("docker exec felhom-controller sh -c 'sha256sum %s/%s/.felhom.yml 2>/dev/null'" % (CACHE, app))
onbox = (onbox.split() or [""])[0][:16]
checks[app] = {"published": pushed, "on_the_box": onbox, "same": pushed == onbox}
w.say("[C] %-9s published=%s box=%s %s" % (app, pushed, onbox or "(absent)",
"SAME" if pushed == onbox else "DIFFERENT"))
decoy = subprocess.run(["bash", "-c", "git -C %s show origin/main:README.md | sha256sum | cut -c1-16" % CAT],
capture_output=True, text=True).stdout.strip()
checks["decoy_readme_vs_sabnzbd_template"] = {"same": decoy == checks["sabnzbd"]["on_the_box"]}
OUT["catalog"] = {"repo_url": repo, "templates_in_cache": w.guest(
"docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip(), "sha_checks": checks}
# ---------------------------------------------------------------- what is left running
OUT["containers"] = w.guest("docker ps --format '{{.Names}}' | sort | tr '\\n' ' '").strip()
OUT["volumes_matching"] = w.guest("docker volume ls --format '{{.Name}}' | grep -E 'sab|nzb' | tr '\\n' ' '").strip()
OUT["drive_folders"] = w.guest("ls -d /mnt/felhom-drives/scratch_hdd/userdata/media/usenet "
"/mnt/felhom-drives/scratch_hdd/userdata/media/nzbget 2>/dev/null").strip() or "(gone)"
w.say("[R] containers: " + OUT["containers"])
w.say("[R] volumes matching: " + (OUT["volumes_matching"] or "(none)"))
w.say("[R] drive folders: " + OUT["drive_folders"])
p = os.path.join(os.environ["SC"], "teardown-usenet.json")
json.dump(OUT, open(p, "w"), indent=2, ensure_ascii=False)
print("WROTE " + p)