fix(disk-gc): hard guest-level report-only gate for CT 111/.129; correct stale fleet map

CT 111 (tdunna, 192.168.68.129) is Theo's box and is report-only per the captain
(2026-08-17, re-confirmed 2026-09-10). The contract defined AMBER as 'GC scheduled
for next run' and its Execution loop called gc-executor for EVERY threat with no
guest-level exclusion - so a single AMBER reading there would have scheduled apt
clean / journal vacuum / log+tmp deletion against someone else's box. The only
marker was frontmatter report_only_agents, which names an AGENT while the scan unit
is a GUEST.

- Gate the Execution loop on a guest/host-keyed report_only_guests block (guest id,
  hostname and IP all match); an excluded guest is alerted and skipped, so no
  gc-executor call is constructed for it at any level.
- Carry the ruling in the contract body next to the loop, not only in frontmatter.
- scripts/disk-gc-plan.py: executable planner that reads the contract's
  authoritative exclusion block and emits the action plan; tests/ covers it.
- Correct the stale fleet map against pvesh /cluster/resources: CT 105 -> amdpve
  (was minipve), CT 111 -> storepve (was amdpve), add guests 118/119/120, and fix
  the '15 CTs' counts (20 guests: 17 LXC + 3 QEMU VMs).
- scripts/pct-run.sh: same stale map (105/111 wrong node, 120 missing) - this is
  why pct-run 111/105 failed.
- Fold in disk-gc-ct100-probe-gap-20260911: CT 100 verifiably works through
  pct-run now that the map is correct; documented that the scanner must probe it
  like any other guest, never via a local-only path (the scanner runs inside CT 100).
This commit is contained in:
abiba
2026-09-12 18:33:02 +00:00
parent d9e06863d8
commit 7b8cc5f9ac
4 changed files with 358 additions and 34 deletions
+89
View File
@@ -0,0 +1,89 @@
"""Regression tests for the disk-gc report-only gate (CT 111 / tdunna / .129).
WHY THIS FILE EXISTS: `disk-gc-threat-response.prose.md` defined AMBER as "GC scheduled
for next run" and its Execution loop called `gc-executor` for EVERY threat, with no
guest-level exclusion. CT 111 (tdunna, 192.168.68.129) belongs to Theo and is
report-only per the captain (2026-08-17, re-confirmed 2026-09-10) — so a single AMBER
reading on that guest would have scheduled GC commands (apt clean, journal vacuum,
log/tmp deletion, snap removal) against someone else's box. The only marker was
frontmatter `report_only_agents`, which names an AGENT while the scan unit is a GUEST.
These tests execute the real planner (`scripts/disk-gc-plan.py`) and assert observable
behaviour: an excluded guest never produces a `gc-executor` action at any level, while
our own guests still do.
"""
from __future__ import annotations
import json
import pathlib
import subprocess
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
PLAN = ROOT / "scripts" / "disk-gc-plan.py"
def _plan(scan, tmp_path):
scan_file = tmp_path / "scan.json"
scan_file.write_text(json.dumps(scan))
proc = subprocess.run(
[sys.executable, str(PLAN), "--scan", str(scan_file), "--json"],
capture_output=True, text=True,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def _actions_for(plan, target):
return [row for row in plan if str(row["target"]) == str(target)]
def test_excluded_guest_never_gets_gc_at_any_level(tmp_path):
"""CT 111 at AMBER, RED and CRITICAL — always report-only, never gc-executor."""
for pct, level in ((84, "AMBER"), (90, "RED"), (97, "CRITICAL")):
plan = _plan([{"id": 111, "hostname": "tdunna", "ip": "192.168.68.129",
"usage_pct": pct}], tmp_path)
rows = _actions_for(plan, 111)
assert rows, f"CT 111 must still be reported at {level}"
assert rows[0]["level"] == level
assert rows[0]["action"] == "report-only", rows
assert not any(r["action"] == "gc-executor" for r in rows)
def test_exclusion_matches_on_any_identity_key(tmp_path):
"""The gate is keyed on guest/host, so id, hostname or IP all match."""
for entry in ({"id": 111, "usage_pct": 95},
{"hostname": "tdunna", "usage_pct": 95},
{"ip": "192.168.68.129", "usage_pct": 95}):
plan = _plan([entry], tmp_path)
assert all(r["action"] == "report-only" for r in plan), (entry, plan)
def test_our_own_guests_still_get_gc(tmp_path):
"""acerpve .9 and amdpve .15 are ours — they must still be acted on."""
plan = _plan([{"hostname": "acerpve", "ip": "192.168.68.9", "usage_pct": 77},
{"hostname": "amdpve", "ip": "192.168.68.15", "usage_pct": 76}], tmp_path)
assert len(plan) == 2
assert all(r["action"] == "gc-executor" for r in plan), plan
def test_below_threshold_emits_nothing(tmp_path):
"""GREEN guests produce no action at all."""
assert _plan([{"id": 111, "usage_pct": 40}], tmp_path) == []
def test_agent_name_alone_does_not_gate_a_guest(tmp_path):
"""An agent-name marker must not be the gate: an unrelated guest still gets GC."""
plan = _plan([{"id": 999, "hostname": "koby", "usage_pct": 95}], tmp_path)
assert plan and plan[0]["action"] == "gc-executor"
def test_contract_carries_the_guest_keyed_exclusion(tmp_path):
"""The authoritative gate must exist and identify CT 111 by guest/host."""
plan = _plan([{"id": 111, "usage_pct": 95}], tmp_path)
reason = _actions_for(plan, 111)[0]["reason"]
assert reason, "the exclusion must carry a reason for the alert"
contract = (ROOT / "disk-gc-threat-response.prose.md").read_text()
assert "report_only_guests:" in contract
assert "192.168.68.129" in contract
assert "tdunna" in contract