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:
@@ -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
|
||||
Reference in New Issue
Block a user