Files
pulse/scripts/tests/test_docker_troubleshooting_docs.py
T
pulse-triage[bot] c08583dd54 Distinguish command permission from a connected agent channel
The LXC discovery report reached command-capable credentials but could not distinguish them from live command-channel admission. Explain the blocked state, keep successful guest checks, and provide a bounded local journal check without credential resets, broader permissions or full-log disclosure. Bind the guidance and command syntax to the existing service events.

Change-source: pulse-maintainer
2026-10-01 23:25:48 +01:00

236 lines
11 KiB
Python

#!/usr/bin/env python3
"""Execute the shipped Docker diagnosis against bounded synthetic tools.
This proves command syntax, disclosure and failure handling, not native PVE or
Docker compatibility. No real service, socket or container is touched.
"""
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
SECRET = "synthetic-docker-docs-secret"
def section(heading):
guide = (ROOT / "docs/UNIFIED_AGENT.md").read_text()
marker = "### " + heading + "\n"
if marker not in guide:
raise AssertionError(f"missing diagnostic section: {heading}")
return guide.split(marker, 1)[1].split("\n### ", 1)[0]
def command(heading):
blocks = re.findall(r"```bash\n(.*?)```", section(heading), re.DOTALL)
if len(blocks) != 1:
raise AssertionError("expected one diagnostic recipe")
return blocks[0]
PCT = r'''#!/usr/bin/python3
import json, os, sys
from pathlib import Path
args = sys.argv[1:]
assert args[:4] == ["exec", "123", "--", "sh"]
assert len(args) == 6 and args[4] == "-c"
Path(os.environ["PCT_RECEIPT"]).write_text(json.dumps(args))
mode = os.environ["PCT_MODE"]
if mode == "locked":
print("pct: guest is locked", file=sys.stderr)
sys.exit(255)
if mode == "hang":
import time
time.sleep(25)
env = dict(os.environ, PATH=os.environ["GUEST_BIN"])
# Model only the socket-presence builtin. The documented sh program and its
# Docker invocations otherwise execute unchanged, with no host socket access.
probe = """test() {
[ "$#" = 2 ] && [ "$1" = -S ] && [ "$2" = /var/run/docker.sock ] || exit 91
[ "$SOCKET_STATE" = present ]
}
"""
os.execve("/bin/sh", ["sh", "-c", probe + args[5]], env)
'''
DOCKER = r'''#!/usr/bin/python3
import json, os, sys
from pathlib import Path
args = sys.argv[1:]
with Path(os.environ["DOCKER_RECEIPT"]).open("a") as output:
output.write(json.dumps(args) + "\n")
if args == ["version", "--format", "server={{.Server.Version}}"]:
if os.environ["DOCKER_MODE"] == "daemon-error":
print("Cannot connect to the Docker daemon", file=sys.stderr)
sys.exit(1)
print("server=29.8.2")
elif args == ["ps", "-a", "--format", "{{.State}}"]:
if os.environ["DOCKER_MODE"] == "list-error":
print("container listing failed", file=sys.stderr)
sys.exit(1)
if os.environ["DOCKER_MODE"] != "empty":
print("running\nexited")
else:
raise AssertionError("unexpected diagnostic operation")
'''
class DockerTroubleshootingDocsTest(unittest.TestCase):
def test_service_account_guidance_does_not_grant_socket_access(self):
text = section("Permission Denied (Docker)")
self.assertNotIn("usermod", text)
self.assertNotRegex(text, r"chmod\s+(?:666|777)|--property=Environment")
for required in ("root-equivalent", "interactive `$USER`", "summary-only",
"collector-owned rootless socket", "unauthenticated TCP"):
self.assertIn(required, text)
recipe = command("Permission Denied (Docker)")
self.assertEqual(recipe.strip().splitlines(), [
"systemctl show pulse-agent.service --property=User --property=Group --property=SupplementaryGroups",
"ls -l /var/run/docker.sock",
])
def test_guest_guidance_preserves_opt_in_and_avoids_speculative_mutations(self):
text = section("Docker visible in one LXC but missing in another")
for required in ("owning node", "command-capable", "allowlist", "online guest-local agent",
"root `pct exec` context", "run this **once**", "Exit `124`",
"do not enable debug", "Do not\nchange LXC privilege, `keyctl`"):
self.assertIn(required, text)
recipe = command("Docker visible in one LXC but missing in another")
self.assertNotRegex(recipe, r"(?i)inspect|printenv|usermod|chmod|systemctl|restart|--token|https?://")
self.assertIn("timeout --kill-after=2s 15s", recipe)
self.assertNotIn("2>/dev/null", recipe)
source = (ROOT / "internal/monitoring/docker_detection.go").read_text()
for operation in ("test -S /var/run/docker.sock", "docker version --format", "docker ps -a"):
self.assertIn(operation, recipe)
self.assertIn(operation, source)
def test_command_channel_guidance_does_not_confuse_scope_with_admission(self):
text = section("Commands enabled but remote control blocked")
for required in (
"no admitted command channel connected", "not a connected session",
"Automatic updates ready", "Changing Proxmox API permissions cannot",
"locally on the affected node", "`dial websocket`", "`registration failed`",
"No matching entry is\ninconclusive", "not the full journal",
"Omit tokens, cookies, URLs, hostnames, addresses", "Keep saved identity and credentials intact",
"do not delete state, loosen TLS", "commands disabled remains an alternative",
):
with self.subTest(required=required):
self.assertIn(required, text)
for label in ("Connected and registered with Pulse command server",
"WebSocket connection failed repeatedly, reconnecting"):
self.assertIn(label, text)
self.assertIn(label, (ROOT / "internal/hostagent/commands.go").read_text())
policy = (ROOT / "internal/api/connections_aggregator.go").read_text()
self.assertIn("agent reports command execution enabled, but no admitted command channel is connected", policy)
guest = section("Docker visible in one LXC but missing in another")
self.assertIn("#commands-enabled-but-remote-control-blocked", guest)
self.assertIn("do not repeat the guest probe", guest)
self.assertNotRegex(text, r"--property=Environment|systemctl (?:cat|restart)|--insecure")
def test_command_channel_journal_check_is_bounded_and_read_only(self):
recipe = command("Commands enabled but remote control blocked")
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for name, body in (
("sudo", '#!/bin/sh\nexec "$@"\n'),
("journalctl", '#!/usr/bin/python3\nimport json, sys\nfrom pathlib import Path\n'
'Path("' + str(root / "argv.json") + '").write_text(json.dumps(sys.argv[1:]))\n'
'print("WebSocket connection failed repeatedly, reconnecting")\n'),
):
path = root / name
path.write_text(body)
path.chmod(0o700)
env = dict(os.environ, PATH=f"{root}:{os.environ['PATH']}", SYNTHETIC_SECRET=SECRET)
result = subprocess.run(["bash", "-eu", "-c", recipe], env=env,
capture_output=True, text=True, timeout=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads((root / "argv.json").read_text()), [
"-u", "pulse-agent.service", "--since", "15 minutes ago", "-n", "200",
"--no-pager", "--output=cat",
])
self.assertNotIn(SECRET, result.stdout + result.stderr)
def run_guest(self, *, socket="present", docker="ok", pct="ok", cli=True):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
host_bin = root / "host"
guest_bin = root / "guest"
host_bin.mkdir()
guest_bin.mkdir()
(host_bin / "pct").write_text(PCT)
(host_bin / "pct").chmod(0o700)
if cli:
(guest_bin / "docker").write_text(DOCKER)
(guest_bin / "docker").chmod(0o700)
env = dict(os.environ, PATH=f"{host_bin}:{os.environ['PATH']}",
GUEST_BIN=str(guest_bin), SOCKET_STATE=socket,
DOCKER_MODE=docker, PCT_MODE=pct,
PCT_RECEIPT=str(root / "pct.json"),
DOCKER_RECEIPT=str(root / "docker.jsonl"),
SYNTHETIC_SECRET=SECRET)
result = subprocess.run(["bash", "-c", command("Docker visible in one LXC but missing in another")],
env=env, capture_output=True, text=True, timeout=22)
self.assertNotIn(SECRET, result.stdout + result.stderr)
self.assertEqual(json.loads((root / "pct.json").read_text())[:4], ["exec", "123", "--", "sh"])
path = root / "docker.jsonl"
operations = [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []
return result, operations
def test_working_guest_reports_only_socket_daemon_and_states(self):
result, operations = self.run_guest()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertRegex(result.stdout, r"/guest/docker\ndefault_socket=present\nserver=29\.8\.2\nrunning\nexited\n$")
self.assertEqual(len(operations), 2)
def test_nondefault_context_does_not_hide_absent_default_socket(self):
result, _ = self.run_guest(socket="absent")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("default_socket=absent\nserver=29.8.2", result.stdout)
def test_empty_inventory_is_success_not_daemon_failure(self):
result, operations = self.run_guest(docker="empty")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(result.stdout.endswith("server=29.8.2\n"))
self.assertEqual(len(operations), 2)
def test_daemon_failure_is_visible_and_stops_listing(self):
result, operations = self.run_guest(docker="daemon-error")
self.assertEqual(result.returncode, 1)
self.assertIn("Cannot connect", result.stderr)
self.assertEqual(len(operations), 1)
def test_listing_failure_is_visible_and_nonzero(self):
result, _ = self.run_guest(docker="list-error")
self.assertEqual(result.returncode, 1)
self.assertIn("container listing failed", result.stderr)
def test_missing_cli_stops_before_socket_or_daemon_claim(self):
result, operations = self.run_guest(cli=False)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("default_socket=", result.stdout)
self.assertEqual(operations, [])
def test_pct_failure_is_not_reported_as_empty_inventory(self):
result, operations = self.run_guest(pct="locked")
self.assertEqual(result.returncode, 255)
self.assertIn("guest is locked", result.stderr)
self.assertEqual(operations, [])
def test_hung_guest_is_stopped_by_actual_timeout(self):
result, operations = self.run_guest(pct="hang")
self.assertEqual(result.returncode, 124)
self.assertEqual(operations, [])
def test_shipped_guide_matches_the_tested_source(self):
self.assertEqual((ROOT / "docs/UNIFIED_AGENT.md").read_bytes(),
(ROOT / "frontend-modern/public/docs/UNIFIED_AGENT.md").read_bytes())
if __name__ == "__main__":
unittest.main()