Files
felhom.eu/documentation/audits/release-monday-2026-10-26/tools/r1292_live.py
T

93 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""r1292_live.py — R-1292 live on a box with a TESTED line (Tester 1; 9202 has none, so the rule never acts there):
a THROWAWAY qBittorrent; the household's „alternative speed" switched ON with an alternative limit of 500 KiB/s; then
the controller's 50 % rule (its 15-minute tick) must set the NORMAL limit (`up_limit`) and leave the alternative alone;
then the household switches back to normal speed, and the next tick must NOT read that as „the household changed it".
qBittorrent's own API from inside its container (the admin password rides an env var, never printed). Removed through
the product at the end (data and backups).
usage: SC=<dir with .ctlpw> SWEEP_EV=<evidence dir> TARGET=tester-1 DRIVE_ROOT=<drive> python3 r1292_live.py"""
import json, os, sys, time
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools"))
import sweep as sw # noqa: E402
w = sw.w
APP = "qbittorrent"
def qb(path, data=None):
"""One qBittorrent API call inside its container, through /root/qbtool.sh in the guest (the password via env QP)."""
pw = (w.GENERATED.get(APP) or {}).get("QBT_ADMIN_PASSWORD", "")
arg = " '" + data + "'" if data else ""
return sw.g(f"QP='{pw}' sh /root/qbtool.sh {path}{arg}").strip()
def record():
"""The 50 % rule's record (settings.json upwatch.qb_limit), parsed — never a grep on a JSON file."""
raw = sw.g("docker exec felhom-controller cat /opt/docker/felhom-controller/data/settings.json")
try:
return (json.loads(raw).get("upwatch") or {}).get("qb_limit")
except ValueError:
return "unreadable"
def prefs():
try:
p = json.loads(qb("app/preferences"))
return {"up_limit": p.get("up_limit"), "alt_up_limit": p.get("alt_up_limit"), "mode_alt": qb("transfer/speedLimitsMode") == "1"}
except ValueError:
return {"error": "unreadable"}
def rule_lines(since):
return sw.g(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'qBittorrent (upload )?limit' | cut -c1-200").strip()
def wait_tick(since, say, label, cap=1100):
for _ in range(cap // 20):
time.sleep(20)
out = sw.g(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'upwatch' | grep -vE 'DEBUG' | tail -1")
# the 15-minute tick also logs the hold seeder; any upwatch line after `since` with a qBittorrent verdict counts
ln = rule_lines(since)
if ln:
break
st = record()
say(f" [{label}] rule lines: {rule_lines(since) or '(none at INFO)'} | record: {st or '(none)'}")
return st
def main():
say = print
w.say = say
w.login()
t0 = int(time.time())
try:
ok = w.deploy(APP, "torrent") and w.wait_app("torrent", "/", tries=90, delay=5)
say("1. qBittorrent installed:", ok)
before = prefs()
say(" before:", before)
if "error" in before:
raise SystemExit("INSTRUMENT: qBittorrent's preferences are unreadable — no verdict (fix the probe first)")
say("2. alternative speed ON, alternative limit 500 KiB/s (the household's):",
qb("app/setPreferences", "json=" + json.dumps({"alt_up_limit": 512000})) or "ok",
qb("transfer/toggleSpeedLimitsMode", "x=1") or "ok")
say(" now:", prefs())
rec1 = wait_tick(t0, say, "3. first tick")
p1 = prefs()
say(" after the first tick:", p1)
say("4. the household switches back to normal speed:", qb("transfer/toggleSpeedLimitsMode", "x=1") or "ok", prefs())
t1 = int(time.time())
time.sleep(960) # one full 15-minute tick; a "kept" decision logs nothing, a "household" one logs at INFO
rec2 = record()
say(" after the next tick:", prefs(), "| record:", rec2, "| lines:", rule_lines(t1) or "(none at INFO)")
verdict = ("PASS" if p1.get("up_limit", 0) and abs(p1["up_limit"] - 94.3 * 0.5 * 1e6 / 8) < 2048 and p1.get("alt_up_limit") == 512000
and isinstance(rec2, dict) and not rec2.get("household") else "FAIL")
say("VERDICT:", verdict)
finally:
sw.remove(APP, keep=False, say=say)
say("5. leftovers:", sw.leftovers(APP, "torrent"))
if __name__ == "__main__":
main()