Files
pulse/scripts/tests/test_vm_disk_diagnostics.py
pulse-triage[bot] 41bc99d68f Bound the complete passive VM preflight read
Apply the existing ten-second deadline to both qm and its response reader. Stop only the local collection group even when a producer exits with an inherited pipe or a reader ignores TERM. Preserve byte limits, independent failures, private output and the refusal to probe or change a guest; test actual owned-reader termination and the mirrored help.

Contract-Neutral: Passive operator diagnostics and existing help only; Proxmox polling, guest-agent admission, APIs, storage, permissions and service behaviour are unchanged.
Change-source: pulse-maintainer
2026-10-12 00:10:22 +01:00

758 lines
42 KiB
Python

#!/usr/bin/env python3
"""Exercise the actual passive helper with synthetic Proxmox commands.
No hypervisor, guest, backup or host service is touched. These controls prove
command selection, failure handling and private output, not native thaw.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import select
import shutil
import signal
import subprocess
import tempfile
import textwrap
import time
import unittest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts/test-vm-disk.sh"
DOC = ROOT / "docs/VM_DISK_MONITORING.md"
BASH = shutil.which("bash")
REAL_TIMEOUT = shutil.which("timeout")
REAL_PYTHON = os.sys.executable
PRIVATE = "synthetic-private-infrastructure-marker"
READS = [["status", "100"], ["config", "100", "--current"]]
STATUS_LIMIT = 256
CONFIG_LIMIT = 64 * 1024
def guide_section(guide, heading):
"""Select one named H3, never another section's executable examples."""
sections, current = [], None
fenced = False
for line in guide.splitlines(keepends=True):
boundary = None if fenced else re.match(r"^(#{1,3}) (.*?)\s*$", line)
if boundary:
if current is not None:
sections.append("".join(current))
current = [] if boundary.group(1) == "###" and boundary.group(2) == heading else None
elif current is not None:
current.append(line)
if line.lstrip().startswith("```"):
fenced = not fenced
if current is not None:
sections.append("".join(current))
if len(sections) != 1:
raise AssertionError(f"Expected one guide section: {heading}")
return sections[0]
def bash_examples(section):
return [textwrap.dedent(block).strip()
for block in re.findall(r"```bash\n(.*?)```", section, re.DOTALL)]
# Record every attempted operation, including prohibited ones. The timeout
# adapter checks the helper's real deadline arguments. Deadline controls use
# real coreutils timeout and pidfds for only their synthetic local readers.
TOOL = '''#!/usr/bin/env python3
import json, os, pathlib, signal, subprocess, sys, time
name = pathlib.Path(sys.argv[0]).name
args = sys.argv[1:]
root = pathlib.Path(os.environ['VM_FIXTURE'])
with (root / 'calls.jsonl').open('a') as stream:
stream.write(json.dumps({'name': name, 'args': args}) + '\\n')
settings = json.loads((root / 'settings.json').read_text())
if name == 'timeout':
# Keep the former producer-only form available for exact-base evidence;
# normal assertions admit only the complete collection deadline.
if args[:4] == ['--signal=TERM', '--kill-after=2s', '10s', 'qm']:
command = args[3:]
else:
assert args[:5] == ['--signal=KILL', '10s', 'bash', '-s', '--']
assert args[5] in ('256', '65536') and args[6] == 'qm'
command = args[2:]
if settings.get('real_timeout'):
result = subprocess.run([os.environ['REAL_TIMEOUT'], *args])
else:
result = subprocess.run(command)
# Popen reports a signal as a negative number; Bash exposes 128 + signal.
# Preserve the real command status rather than wrapping -SIGKILL to 247.
sys.exit(result.returncode if result.returncode >= 0 else 128 - result.returncode)
elif name == 'bash':
assert args[:2] == ['-s', '--'] and args[2] in ('256', '65536')
os.execv(os.environ['REAL_BASH'], [os.environ['REAL_BASH'], *args])
elif name == 'qm':
if args == ['status', '100']:
operation = 'status'
default = 'status: running\\n'
elif args == ['config', '100', '--current']:
operation = 'config'
default = 'agent: 1\\nname: synthetic-private-infrastructure-marker\\n'
else:
# No fake succeeds on an unrecognised or guest-agent operation.
sys.exit(90)
if settings.get('sleep_operation') == operation:
signal.signal(signal.SIGTERM, signal.SIG_IGN)
(root / 'blocked-reader.json').write_text(json.dumps({'pid': os.getpid()}))
time.sleep(60)
if settings.get('pipe_holder_operation') == operation:
# The producer exits successfully but its TERM-resistant child retains
# stdout. No bytes beyond the small normal reply are needed to hang an
# unbounded response reader.
subprocess.Popen([os.environ['REAL_PYTHON'], '-I', '-c',
'import json, os, pathlib, signal, sys, time; '
'signal.signal(signal.SIGTERM, signal.SIG_IGN); '
'pathlib.Path(sys.argv[1]).write_text(json.dumps({"pid": os.getpid()})); '
'time.sleep(60)', str(root / 'blocked-reader.json')],
stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Write bytes so corrupt transport controls are not silently repaired by
# the fixture's text encoding before they reach the actual helper.
output = settings.get(operation + '_output', default).encode('utf-8')
if settings.get(operation + '_invalid_utf8'):
output += b'\\xff'
try:
sys.stdout.buffer.write(output)
sys.stdout.buffer.flush()
except BrokenPipeError:
# A bounded reader is entitled to close the pipe before the producer
# finishes. Do not turn this fixture's expected stop into raw output.
os._exit(1)
sys.stderr.write('synthetic-private-infrastructure-marker\\n')
sys.exit(settings.get(operation + '_exit', 0))
elif name == 'python3':
# The non-isolated form exists only for the startup-code negative control.
assert (len(args) == 4 and args[:2] == ['-I', '-c'] or
len(args) == 3 and args[0] == '-c') and args[-1] in ('256', '65536')
if settings.get('blocked_response_reader') and args[-1] == settings.get('blocked_limit', '256'):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
(root / 'blocked-reader.json').write_text(json.dumps({'pid': os.getpid()}))
if settings.get('close_reader_pipes'):
os.close(0); os.close(1); os.close(2)
time.sleep(60)
if settings.get('reader_exit') is not None:
sys.exit(settings['reader_exit'])
os.execv(os.environ['REAL_PYTHON'], [os.environ['REAL_PYTHON'], *args])
elif name == 'sudo':
assert args == ['bash', './scripts/test-vm-disk.sh', '100']
os.execv(os.environ['REAL_BASH'], [os.environ['REAL_BASH'], *args[1:]])
else:
sys.exit(91)
'''
def run_owned_preflight(invocation, env, *, blocked=False):
"""Observe the exact fixture process; emergency cleanup is not a pass.
A pidfd remains bound to that synthetic reader if its pipes close or its
parent exits. No hypervisor process, service or guest is signalled.
"""
with subprocess.Popen(invocation, env=env, cwd=ROOT, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
start_new_session=True) as process:
reader_fd = None
try:
if blocked:
record = Path(env["VM_FIXTURE"]) / "blocked-reader.json"
deadline = time.monotonic() + 3
while not record.exists() and time.monotonic() < deadline:
time.sleep(0.01)
if not record.exists():
raise AssertionError("owned blocked reader was not observed")
# The writer can be between creation and flush of its tiny
# record. Retain a bounded wait, not a favourable test replay.
while True:
try:
pid = json.loads(record.read_text())["pid"]
break
except json.JSONDecodeError:
if time.monotonic() >= deadline:
raise AssertionError("owned reader identity was incomplete")
time.sleep(0.01)
reader_fd = os.pidfd_open(pid)
poller = select.poll()
poller.register(reader_fd, select.POLLIN)
try:
stdout, stderr = process.communicate(timeout=16)
except subprocess.TimeoutExpired as error:
raise AssertionError("preflight exceeded its complete-read deadline") from error
if reader_fd is not None:
if not poller.poll(1000):
raise AssertionError("owned reader outlived the completed preflight")
return subprocess.CompletedProcess(invocation, process.returncode, stdout, stderr)
finally:
if reader_fd is not None:
if not poller.poll(0):
signal.pidfd_send_signal(reader_fd, signal.SIGKILL)
if not poller.poll(1000):
raise AssertionError("test-owned reader cleanup was not observed")
os.close(reader_fd)
if process.poll() is None:
os.killpg(process.pid, signal.SIGKILL)
process.communicate(timeout=3)
class VMDiskDiagnosticsTest(unittest.TestCase):
def exercise(self, *, settings=None, args=None, recipe=None, source=None,
missing=None):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / "settings.json").write_text(json.dumps(settings or {}))
for name in ("qm", "timeout", "python3", "bash", "sudo", "pveum", "pveversion", "curl", "systemctl"):
if name == missing:
continue
tool = root / name
# The collector adapter is itself named python3. Pin the
# fixture interpreter so /usr/bin/env cannot recurse through
# that adapter instead of ever executing the fake qm.
tool.write_text(TOOL.replace("/usr/bin/env python3", f"{REAL_PYTHON} -I"))
tool.chmod(0o700)
env = dict(os.environ, VM_FIXTURE=str(root), REAL_BASH=BASH,
REAL_TIMEOUT=REAL_TIMEOUT, REAL_PYTHON=REAL_PYTHON,
PATH=f"{root}:{os.environ['PATH']}")
if missing:
# The adapters use an absolute interpreter, so an empty
# fallback PATH can genuinely model unavailable dependencies.
env["PATH"] = str(root)
startup_marker = root / "startup-executed"
if (settings or {}).get("poison_pythonpath"):
startup = root / "python-startup"
startup.mkdir()
(startup / "sitecustomize.py").write_text(
"from pathlib import Path\n"
f"Path({str(startup_marker)!r}).write_text('synthetic startup')\n"
"raise SystemExit(29)\n"
)
env["PYTHONPATH"] = str(startup)
script = SCRIPT
if source is not None:
script = root / "candidate.sh"
script.write_text(source)
invocation = [BASH, str(script), *(args if args is not None else ["100"])]
if recipe:
invocation = [BASH, "-c", recipe]
blocked = any((settings or {}).get(key) for key in (
"sleep_operation", "pipe_holder_operation", "blocked_response_reader"
))
result = run_owned_preflight(invocation, env, blocked=blocked)
calls_file = root / "calls.jsonl"
calls = [json.loads(line) for line in calls_file.read_text().splitlines()] if calls_file.exists() else []
self.assertFalse(startup_marker.exists(), "untrusted Python startup code executed")
self.assertNotIn(PRIVATE, result.stdout + result.stderr)
return result, calls
def assert_passive(self, calls, *, operations=READS):
self.assertEqual([c["args"] for c in calls if c["name"] == "qm"], operations)
self.assertTrue(all(c["name"] in ("timeout", "qm", "python3", "bash", "sudo") for c in calls))
self.assertEqual([c["args"][-1] for c in calls if c["name"] == "python3"],
[str(STATUS_LIMIT if operation[0] == "status" else CONFIG_LIMIT)
for operation in operations])
for call in calls:
if call["name"] == "timeout":
self.assertEqual(call["args"][:5], ["--signal=KILL", "10s", "bash", "-s", "--"])
self.assertIn(call["args"][5], (str(STATUS_LIMIT), str(CONFIG_LIMIT)))
self.assertEqual(call["args"][6], "qm")
if call["name"] == "bash":
self.assertEqual(call["args"][:2], ["-s", "--"])
if call["name"] == "python3":
self.assertEqual(call["args"][:2], ["-I", "-c"])
self.assertIn(call["args"][3], (str(STATUS_LIMIT), str(CONFIG_LIMIT)))
def test_oversized_reads_fail_before_interpreting_partial_configuration(self):
cases = [
{"status_output": "status: running\n" + PRIVATE + "x" * (1024 * 1024)},
{"config_output": "agent: 1\nname: " + PRIVATE + "\ndescription: "
+ "x" * (1024 * 1024) + "\nlock: backup\n"},
]
for settings in cases:
with self.subTest(operation=next(iter(settings))):
result, calls = self.exercise(settings=settings)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("exceeded the", result.stderr)
self.assertIn("byte limit; preflight incomplete", result.stderr)
self.assertNotIn("Guest agent configured:", result.stdout)
self.assertNotIn("VM lock:", result.stdout)
self.assertNotIn("preflight completed", result.stdout)
self.assertLess(len(result.stdout + result.stderr), 600)
self.assert_passive(calls, operations=READS[:1] if "status_output" in settings else READS)
def test_config_limit_is_measured_in_bytes_and_includes_trailing_newlines(self):
prefix = "agent: 1\nname: " + PRIVATE + "\ndescription: "
padding = CONFIG_LIMIT - len(prefix.encode()) - 1
exact = prefix + "x" * padding + "\n"
self.assertEqual(len(exact.encode()), CONFIG_LIMIT)
result, calls = self.exercise(settings={"config_output": exact})
self.assertEqual(result.returncode, 0, result.stderr)
self.assert_passive(calls)
# Shell command substitution removes trailing newlines. Neither that
# nor a multi-byte name may make an oversized response look complete.
for oversized in (exact + "\n", prefix + "é" * padding + "\n"):
with self.subTest(bytes=len(oversized.encode())):
result, calls = self.exercise(settings={"config_output": oversized})
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("byte limit; preflight incomplete", result.stderr)
self.assertNotIn("VM lock:", result.stdout)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls)
def test_status_limit_includes_trailing_newlines_before_interpretation(self):
exact = "status: running" + "\n" * (STATUS_LIMIT - len("status: running"))
result, calls = self.exercise(settings={"status_output": exact})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("VM status: running", result.stdout)
self.assert_passive(calls)
result, calls = self.exercise(settings={"status_output": exact + "\n"})
self.assertEqual(result.returncode, 1)
self.assertIn("256-byte limit; preflight incomplete", result.stderr)
self.assertNotIn("VM status:", result.stdout)
self.assert_passive(calls, operations=READS[:1])
def test_corrupt_reads_cannot_be_silently_normalised_into_a_success(self):
for settings in (
{"status_output": "status: run\u0000ning\n"},
{"config_output": "agent: \u00001\nname: " + PRIVATE + "\n"},
{"config_output": "agent: 1\nname: " + PRIVATE + "\n", "config_invalid_utf8": True},
):
with self.subTest(settings=settings):
result, calls = self.exercise(settings=settings)
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("non-text bytes; preflight incomplete", result.stderr)
self.assertNotIn("Guest agent configured:", result.stdout)
self.assertNotIn("VM lock:", result.stdout)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls, operations=READS[:1] if "status_output" in settings else READS)
def test_valid_unicode_configuration_remains_passive(self):
result, calls = self.exercise(settings={"config_output": "agent: 1\nname: café\nlock: backup\n"})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Guest agent configured: enabled", result.stdout)
self.assertIn("VM lock: backup", result.stdout)
self.assertNotIn("café", result.stdout + result.stderr)
self.assert_passive(calls)
def test_producer_failures_are_independent_of_reader_result_codes(self):
# Producer exit codes are independent of bounded-reader result codes.
for code in (80, 81):
with self.subTest(code=code):
result, calls = self.exercise(settings={"config_exit": code})
self.assertEqual(result.returncode, 1)
self.assertIn(f"configuration read failed (exit {code})", result.stderr)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls)
def test_reader_failure_is_incomplete_not_an_empty_or_successful_response(self):
result, calls = self.exercise(settings={"reader_exit": 23})
self.assertEqual(result.returncode, 1)
self.assertIn("status read failed (exit 23)", result.stderr)
self.assertNotIn("VM status:", result.stdout)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls, operations=READS[:1])
def test_reader_does_not_execute_python_environment_startup_code(self):
result, calls = self.exercise(settings={"poison_pythonpath": True})
self.assertEqual(result.returncode, 0, result.stderr)
self.assert_passive(calls)
unsafe = SCRIPT.read_text().replace("python3 -I -c", "python3 -c")
self.assertNotEqual(unsafe, SCRIPT.read_text())
with self.assertRaisesRegex(AssertionError, "untrusted Python startup"):
self.exercise(settings={"poison_pythonpath": True}, source=unsafe)
def test_running_vm_preflight_is_passive_even_without_a_lock(self):
result, calls = self.exercise()
self.assertEqual(result.returncode, 0, result.stderr)
self.assert_passive(calls)
self.assertIn("VM status: running", result.stdout)
self.assertIn("Guest agent configured: enabled", result.stdout)
self.assertIn("VM lock: not reported", result.stdout)
self.assertIn("does not establish", result.stdout)
self.assertIn("guest thaw were not tested", result.stdout)
def test_stopped_guest_is_not_started_or_probed(self):
result, calls = self.exercise(settings={"status_output": "status: stopped\n"})
self.assertEqual(result.returncode, 0)
self.assertIn("VM status: stopped", result.stdout)
self.assert_passive(calls)
def test_backup_other_empty_and_future_locks_never_send_a_probe(self):
for lock in ("backup", "snapshot", "migrate", "", PRIVATE):
with self.subTest(lock=lock):
result, calls = self.exercise(settings={"config_output": f"agent: 1\nlock: {lock}\nname: {PRIVATE}\n"})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("VM lock: backup" if lock == "backup" else "VM lock: present or unknown", result.stdout)
self.assert_passive(calls)
def test_agent_boolean_is_not_inferred_from_other_numeric_options(self):
cases = {
None: "disabled", "0": "disabled", "1": "enabled",
"0,freeze-fs-on-backup=1": "disabled", "enabled=0,fstrim_cloned_disks=1": "disabled",
"enabled=1,freeze-fs-on-backup=0": "enabled", "freeze-fs-on-backup=1,enabled=0": "disabled",
"fstrim_cloned_disks=1": "unknown", "enabled=2": "unknown", "": "unknown",
"0,enabled=1": "unknown", "1,enabled=1": "unknown", "disabled": "unknown",
}
for value, expected in cases.items():
with self.subTest(value=value):
config = f"name: {PRIVATE}\n" + (f"agent: {value}\n" if value is not None else "")
result, calls = self.exercise(settings={"config_output": config})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"Guest agent configured: {expected} (not a responsiveness test)", result.stdout)
self.assert_passive(calls)
def test_failed_status_never_reads_configuration_or_reports_completion(self):
for code in (1, 13, 124, 137, 143):
with self.subTest(code=code):
result, calls = self.exercise(settings={"status_exit": code, "status_output": PRIVATE})
self.assertEqual(result.returncode, 1)
self.assertNotIn("preflight completed", result.stdout)
self.assertNotIn("VM status:", result.stdout)
self.assertIn("timed out" if code in (124, 137) else f"exit {code}", result.stderr)
self.assert_passive(calls, operations=READS[:1])
def test_config_failure_cannot_be_presented_as_an_absent_lock(self):
for code in (1, 13, 124, 137):
with self.subTest(code=code):
result, calls = self.exercise(settings={"config_exit": code, "config_output": f"agent: 1\nlock: backup\n{PRIVATE}"})
self.assertEqual(result.returncode, 1)
self.assertNotIn("preflight completed", result.stdout)
self.assertNotIn("VM lock:", result.stdout)
self.assertIn("configuration read", result.stderr)
self.assert_passive(calls)
def test_empty_unknown_and_repeated_responses_fail_without_dumping_config(self):
cases = [
{"status_output": ""}, {"status_output": "status: " + PRIVATE},
{"config_output": ""}, {"config_output": PRIVATE},
{"config_output": f"agent: 1\nagent: 0\nname: {PRIVATE}\n"},
{"config_output": f"lock: backup\nlock: migrate\nname: {PRIVATE}\n"},
]
for settings in cases:
with self.subTest(settings=settings):
result, calls = self.exercise(settings=settings)
self.assertEqual(result.returncode, 1)
self.assertIn("preflight incomplete", result.stderr)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls, operations=READS[:1] if "status_output" in settings else READS)
def test_malformed_lines_cannot_hide_agent_or_lock_configuration(self):
lines = (
" lock: backup", "\tlock: backup", "lock=backup", "lock : backup",
"Lock: backup", "LOCK: backup", "lock", "agent 1", "Agent: 1",
"warning " + PRIVATE,
)
for line in lines:
for suffix in ("", "\n"):
with self.subTest(line=line, suffix=suffix):
config = f"agent: 1\nname: {PRIVATE}\n{line}{suffix}"
result, calls = self.exercise(settings={"config_output": config})
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("unrecognised line; preflight incomplete", result.stderr)
self.assertNotIn("Guest agent configured:", result.stdout)
self.assertNotIn("VM lock:", result.stdout)
self.assertNotIn("preflight completed", result.stdout)
self.assert_passive(calls)
def test_blank_comments_and_unknown_well_formed_fields_remain_passive(self):
for agent in (None, "0", "1"):
with self.subTest(agent=agent):
config = (f"\n# {PRIVATE}\n \t\nfuture_read_option: {PRIVATE}\n"
f"\t# lock: backup {PRIVATE}\nname: {PRIVATE}\n")
if agent is not None:
config += f"agent: {agent}\n"
result, calls = self.exercise(settings={"config_output": config})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Guest agent configured: enabled" if agent == "1"
else "Guest agent configured: disabled", result.stdout)
self.assertIn("VM lock: not reported", result.stdout)
self.assert_passive(calls)
result, calls = self.exercise(settings={"config_output": f"\n# {PRIVATE}\n \t\n"})
self.assertEqual(result.returncode, 1)
self.assertIn("preflight incomplete", result.stderr)
self.assertNotIn("VM lock:", result.stdout)
self.assert_passive(calls)
def test_help_and_invalid_arguments_never_contact_proxmox(self):
for args in ([], ["0"], ["-1"], ["100;touch BAD"], ["100", "--probe"],
["9999999999"], ["--probe"], ["--help"]):
with self.subTest(args=args):
result, calls = self.exercise(args=args)
self.assertEqual(result.returncode, 0 if args == ["--help"] else 2)
self.assertEqual(calls, [])
def test_missing_dependencies_do_not_run_a_partial_preflight(self):
for missing in ("qm", "timeout", "python3", "bash"):
with self.subTest(missing=missing):
result, calls = self.exercise(missing=missing)
self.assertEqual(result.returncode, 1)
self.assertIn(f"unavailable: {missing}", result.stderr)
self.assertEqual(calls, [])
def test_real_deadline_stops_a_blocked_synthetic_local_read(self):
self.assertIsNotNone(REAL_TIMEOUT)
start = time.monotonic()
result, calls = self.exercise(settings={"real_timeout": True, "sleep_operation": "status"})
elapsed = time.monotonic() - start
self.assertEqual(result.returncode, 1)
self.assertIn("status read timed out", result.stderr)
self.assertLess(elapsed, 14)
self.assert_passive(calls, operations=READS[:1])
def test_complete_read_deadline_covers_inherited_stdout_and_the_reader(self):
self.assertIsNotNone(REAL_TIMEOUT)
for operation in ("status", "config"):
limit = str(STATUS_LIMIT if operation == "status" else CONFIG_LIMIT)
for mode in ("inherited-pipe", "reader", "closed-pipe-reader"):
with self.subTest(operation=operation, mode=mode):
settings = {"real_timeout": True}
if mode == "inherited-pipe":
settings["pipe_holder_operation"] = operation
else:
settings.update(blocked_response_reader=True, blocked_limit=limit,
close_reader_pipes=mode == "closed-pipe-reader")
start = time.monotonic()
result, calls = self.exercise(settings=settings)
elapsed = time.monotonic() - start
self.assertEqual(result.returncode, 1, result.stdout)
label = "status" if operation == "status" else "configuration"
self.assertIn(f"{label} read timed out", result.stderr)
self.assertLess(elapsed, 14)
self.assertNotIn("preflight completed", result.stdout)
self.assertNotIn("Guest agent configured:", result.stdout)
self.assertNotIn("VM lock:", result.stdout)
if operation == "status":
self.assertNotIn("VM status:", result.stdout)
self.assertLess(len(result.stdout + result.stderr), 600)
self.assert_passive(calls, operations=READS[:1] if operation == "status" else READS)
def test_exact_documented_command_uses_the_passive_helper(self):
recipes = bash_examples(guide_section(DOC.read_text(), "Passive host preflight"))
self.assertEqual(len(recipes), 1)
result, calls = self.exercise(recipe=recipes[0])
self.assertEqual(result.returncode, 0, result.stderr)
self.assert_passive(calls)
self.assertEqual([c["args"] for c in calls if c["name"] == "sudo"],
[["bash", "./scripts/test-vm-disk.sh", "100"]])
def test_safety_oracle_rejects_reintroduced_guest_probes_and_mutations(self):
source = SCRIPT.read_text()
for unsafe in ('qm agent "$vmid" ping', 'qm agent "$vmid" get-fsinfo',
'qm unlock "$vmid"', 'qm reset "$vmid"',
'pveum acl modify /', 'systemctl restart pulse'):
with self.subTest(operation=unsafe):
mutated = source.replace("vmid=$1", "vmid=$1\n" + unsafe)
result, calls = self.exercise(source=mutated, settings={"config_output": "agent: 1\nlock: backup\n"})
self.assertNotEqual(result.returncode, 0)
with self.assertRaises(AssertionError):
self.assert_passive(calls)
class VMDiskHelpTest(unittest.TestCase):
def test_documented_read_limits_and_failure_meaning_match_the_helper(self):
section = guide_section(DOC.read_text(), "Passive host preflight")
for phrase in ("256 bytes", "64 KiB", "Python 3", "GNU `timeout`", "ten-second hard deadline",
"both `qm` and its response", "only that local collection group",
"No VM, guest agent or", "not an absent lock", "do not remove the deadline",
"truncated prefix",
"printing raw", "saving it to disk", "Inspect the configuration privately"):
self.assertIn(phrase, section)
self.assertIn(f"read_bounded status status_output {STATUS_LIMIT}", SCRIPT.read_text())
self.assertIn(f"read_bounded configuration config {CONFIG_LIMIT}", SCRIPT.read_text())
def test_section_selection_preserves_shell_comments_and_rejects_missing_or_duplicate_headings(self):
content = "```bash\n# From the reviewed checkout\n### Shell comment, not a heading\ntrue\n```\n"
guide = "### Passive host preflight\n" + content + "## Next\nOther text\n"
self.assertEqual(guide_section(guide, "Passive host preflight"), content)
for invalid in ("## No passive section\n", guide + "### Passive host preflight\n"):
with self.subTest(guide=invalid), self.assertRaises(AssertionError):
guide_section(invalid, "Passive host preflight")
def assert_guide_commands(self, guide):
# The planned server pause is intentionally active, unlike the passive
# hypervisor diagnostic. Admit only the six reviewed server/timer blocks
# here; do not permit arbitrary systemctl or guest commands elsewhere.
precaution = guide_section(guide, "Pause Pulse for a planned freeze-enabled backup")
self.assertEqual(bash_examples(precaution), [
"systemctl show pulse.service --property=LoadState,ActiveState,MainPID",
"systemctl show pulse-update.timer --property=LoadState,ActiveState",
"sudo systemctl stop pulse-update.timer\n"
"systemctl show pulse-update.timer pulse-update.service \\\n"
" --property=Id,LoadState,ActiveState,MainPID",
"sudo systemctl stop pulse.service\n"
"systemctl show pulse.service \\\n"
" --property=LoadState,ActiveState,MainPID,Result,ExecMainCode,ExecMainStatus",
"sudo systemctl start pulse.service\nsystemctl is-active pulse.service",
"sudo systemctl start pulse-update.timer\nsystemctl is-active pulse-update.timer",
])
for block in bash_examples(guide.replace(precaution, "", 1)):
self.assertNotRegex(block, r"\b(curl|wget|qm agent|pveum|systemctl)\b")
def test_command_boundaries_reject_active_diagnostics_and_unreviewed_pause_operations(self):
guide = DOC.read_text()
mutations = [
guide.replace("sudo bash ./scripts/test-vm-disk.sh 100",
"systemctl restart pulse.service"),
guide.replace("sudo systemctl stop pulse.service", "sudo systemctl restart pulse.service"),
guide.replace("sudo systemctl start pulse-update.timer", "qm agent 100 ping"),
guide.replace(",Result,ExecMainCode,ExecMainStatus", ""),
guide + "\n```bash\nsystemctl stop pulse.service\n```\n",
guide.replace("sudo bash ./scripts/test-vm-disk.sh 100", "curl https://example.invalid"),
]
for index, mutated in enumerate(mutations):
with self.subTest(mutation=index), self.assertRaises(AssertionError):
self.assert_guide_commands(mutated)
def test_guides_are_mirrored_and_preserve_the_safety_and_proof_boundaries(self):
for name in ("VM_DISK_MONITORING", "TROUBLESHOOTING"):
source = (ROOT / f"docs/{name}.md").read_bytes()
self.assertEqual(source, (ROOT / f"frontend-modern/public/docs/{name}.md").read_bytes())
guide = DOC.read_text()
for phrase in ("monitoring and alerts", "not established as a reproduced cause",
"does not prove thaw", "a backup can", "older copies", "non-zero exit",
"host-root diagnostic does not test", "not a claim", "bounded timeouts"):
self.assertIn(phrase, guide)
self.assert_guide_commands(guide)
self.assertNotIn("GUEST_AGENT_FSINFO_TIMEOUT=", guide)
def test_shutdown_readback_is_not_guest_request_completion_or_a_forced_stop(self):
precaution = " ".join(guide_section(
DOC.read_text(), "Pause Pulse for a planned freeze-enabled backup"
).split())
for phrase in (
"`Result=success`", "`ExecMainCode=1`", "`ExecMainStatus=0`",
"Do not force-kill Pulse or clear its failed state",
"an old exit result is not evidence of a new shutdown",
"Stopping Pulse does not cancel a guest-agent request already issued",
"If their state is unknown, do not start the backup",
):
self.assertIn(phrase, precaution)
class ServerLogSafetyDocsTest(unittest.TestCase):
"""Check help against existing controls, not browser or native recovery."""
def log_help(self):
guide = (ROOT / "docs/CONFIGURATION.md").read_text()
return " ".join(guide.split("#### Log Levels\n", 1)[1].split("\n| Variable |", 1)[0].split())
def contains(self, text, *phrases):
for phrase in phrases:
with self.subTest(phrase=phrase):
self.assertTrue(phrase in text, f"Missing safety explanation: {phrase}")
def assert_display_only_controls(self, state):
# Buffered lines are still browser-local. Follow the complete helper
# chain, not just the old one-line toggles, so a backend mutation in a
# pending-work helper cannot hide behind unchanged button handlers.
expected = {
"togglePaused": "if (!isPaused()) flushPendingLogs(); setIsPaused((prev) => !prev);",
"clearLogs": "resetPending(); setLogs([]);",
"resetPending": (
"pendingLogs = []; pendingCount = 0; pendingNext = 0; "
"if (renderFrame !== null) { cancelAnimationFrame(renderFrame); renderFrame = null; }"
),
"flushPendingLogs": (
"if (disposed || pendingCount === 0) return; "
"const incoming = Array.from( { length: pendingCount }, "
"(_, index) => pendingLogs[(pendingNext - pendingCount + MAX_LOGS + index) % MAX_LOGS], ); "
"resetPending(); setLogs((prev) => [ "
"...prev.slice(Math.max(0, prev.length + incoming.length - MAX_LOGS)), ...incoming, ]); "
"scrollToBottom();"
),
"scrollToBottom": "if (logContainer) { logContainer.scrollTop = logContainer.scrollHeight; }",
}
for name, body in expected.items():
marker = f"const {name} = () => {{"
self.assertEqual(state.count(marker), 1, name)
actual = state.split(marker, 1)[1].split("\n };", 1)[0]
actual = " ".join(re.sub(r"//[^\n]*", "", actual).split())
self.assertEqual(actual, body, name)
self.assertIn("if (disposed || isPaused()) return;", state)
def test_log_pause_cannot_substitute_for_the_backup_server_stop(self):
guide = " ".join(DOC.read_text().split("### Pause Pulse", 1)[0].split())
self.contains(guide, "**Pause Stream**", "only pauses the browser display",
"does not stop the Pulse server", "guest-agent requests",
"actual server stop procedure")
state = (ROOT / "frontend-modern/src/components/Settings/useSystemLogsPanelState.ts").read_text()
self.assert_display_only_controls(state)
self.assertIn("toggleTitle: 'Pause Stream'", (ROOT / "frontend-modern/src/utils/systemLogsPresentation.ts").read_text())
def test_log_level_is_server_wide_not_a_browser_filter(self):
self.contains(self.log_help(), "server-wide logging threshold", "not a browser-only filter",
"`warn` also suppresses `info` operational records",
"absence of a log line is not evidence that an event did not occur",
"`LOG_LEVEL` takes precedence at startup")
handler = (ROOT / "internal/api/log_handlers.go").read_text().split("func (h *LogHandlers) HandleSetLevel", 1)[1]
self.assertIn("logging.SetGlobalLevel(level)", handler)
self.assertIn("settings.LogLevel = level", handler)
loader = (ROOT / "internal/config/config.go").read_text()
self.assertIn('if logLevel := os.Getenv("LOG_LEVEL"); logLevel != "" {\n\t\tcfg.LogLevel = logLevel', loader)
def test_display_controls_do_not_delete_logs_or_reduce_server_logging(self):
self.contains(self.log_help(), "**Pause Stream**", "**Clear Log Output**",
"only affect the browser display", "do not stop monitoring",
"reduce server logging or erase the server's logs",
"VM_DISK_MONITORING.md#backup-safety")
state = (ROOT / "frontend-modern/src/components/Settings/useSystemLogsPanelState.ts").read_text()
self.assert_display_only_controls(state)
def test_display_only_controls_reject_backend_calls_and_stale_pending_work(self):
state = (ROOT / "frontend-modern/src/components/Settings/useSystemLogsPanelState.ts").read_text()
for name, operation in (
("togglePaused", "apiFetchJSON('/api/monitoring/stop', { method: 'POST' });"),
("clearLogs", "apiFetchJSON('/api/logs', { method: 'DELETE' });"),
("resetPending", "handleLevelChange('warn');"),
("flushPendingLogs", "handleLevelChange('error');"),
("scrollToBottom", "mutateServer();"),
):
marker = f"const {name} = () => {{"
mutated = state.replace(marker, marker + "\n " + operation)
with self.subTest(control=name), self.assertRaises(AssertionError):
self.assert_display_only_controls(mutated)
for before, after in (
("const clearLogs = () => {\n resetPending();", "const clearLogs = () => {"),
("if (disposed || isPaused()) return;", "if (disposed) return;"),
("if (!isPaused()) flushPendingLogs();", "flushPendingLogs();"),
):
with self.subTest(missing=before), self.assertRaises(AssertionError):
self.assert_display_only_controls(state.replace(before, after))
def test_bundle_scope_matches_the_real_export_not_the_visible_lines(self):
self.contains(self.log_help(), "complete configured log file", "retained server log buffer",
"configuration and environment information", "not just the visible lines",
"Keep the archive private", "manually reviewed, redacted excerpts",
"not a guarantee that all content is safe to publish")
handler = (ROOT / "internal/api/log_handlers.go").read_text().split("func (h *LogHandlers) HandleDownloadBundle", 1)[1].split("type SetLogLevelRequest", 1)[0]
for source in ('os.Open(h.config.LogFile)', 'zipWriter.Create("pulse.log")',
'io.Copy(wr, f)', 'zipWriter.Create("pulse-tail.log")',
'logging.GetBroadcaster().GetHistory()', 'zipWriter.Create("system-info.json")',
'h.config.DeepCopy()', 'os.Environ()'):
self.assertIn(source, handler)
state = (ROOT / "frontend-modern/src/components/Settings/useSystemLogsPanelState.ts").read_text()
self.assertIn("window.location.href = '/api/logs/download'", state)
trouble = " ".join((ROOT / "docs/TROUBLESHOOTING.md").read_text().split("### Collect diagnostics safely\n", 1)[1].split("### Inspect Notification Logs", 1)[0].split())
self.contains(trouble, "**Support Bundle**", "different export",
"not the **GitHub (review first)** diagnostics export",
"not limited to the lines visible", "CONFIGURATION.md#log-levels")
def test_debug_guidance_preserves_working_monitoring_and_private_evidence(self):
self.contains(self.log_help(), "Start with the current level", "raw guest-filesystem responses",
"private names or paths", "do not enable it just to obtain a report",
"previous working level", "without repeating the stall",
"not proof that the underlying problem is fixed",
"TROUBLESHOOTING.md#inspect-notification-logs")
def test_safety_help_is_shipped_without_new_diagnostic_commands(self):
for name in ("VM_DISK_MONITORING.md", "CONFIGURATION.md", "TROUBLESHOOTING.md"):
self.assertEqual((ROOT / "docs" / name).read_bytes(),
(ROOT / "frontend-modern/public/docs" / name).read_bytes())
self.assertNotIn("```", self.log_help(), "reuse bounded readers, not another copied command")
if __name__ == "__main__":
unittest.main()