Add integration tests based on VM

This commit is contained in:
2026-10-26 22:34:00 +02:00
parent f4a6540df9
commit 7d0f006601
22 changed files with 3826 additions and 69 deletions
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Create and validate the one approved M9 VirtualBox VM; never start it."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import selectors
import stat
import subprocess
import sys
import time
from typing import Callable, Sequence
import uuid
import guard
LAB_ROOT = guard.LAB_ROOT
ISO_PATH = LAB_ROOT / guard.EXPECTED_ISO
VM_NAME = guard.TARGET_NAME
OS_TYPE = "Debian13_64"
DISK_SIZE_MIB = 8192
MEMORY_MIB = 1024
CPUS = 1
MAX_CAPTURE = 1024 * 1024
TIMEOUT_SECONDS = 30
class CreateError(Exception):
"""Creation could not safely complete; partial target is retained."""
Runner = Callable[[Sequence[str]], subprocess.CompletedProcess[str]]
def run_command(argv: Sequence[str]) -> subprocess.CompletedProcess[str]:
process = subprocess.Popen(
list(argv), stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env={"PATH": os.environ.get("PATH", "/usr/bin:/bin"), "LANG": "C"},
)
assert process.stdout is not None and process.stderr is not None
output = {process.stdout: bytearray(), process.stderr: bytearray()}
selector = selectors.DefaultSelector()
for stream in output:
os.set_blocking(stream.fileno(), False)
selector.register(stream, selectors.EVENT_READ)
deadline = time.monotonic() + TIMEOUT_SECONDS
try:
while selector.get_map():
remaining = deadline - time.monotonic()
if remaining <= 0:
raise subprocess.TimeoutExpired(argv, TIMEOUT_SECONDS)
for key, _ in selector.select(remaining):
stream = key.fileobj
chunk = os.read(stream.fileno(), min(65536, MAX_CAPTURE + 1 - len(output[stream])))
if not chunk:
selector.unregister(stream)
continue
output[stream].extend(chunk)
if len(output[stream]) > MAX_CAPTURE:
raise CreateError("VBoxManage output exceeded the creation limit")
returncode = process.wait(timeout=max(0.0, deadline - time.monotonic()))
except (OSError, subprocess.TimeoutExpired, CreateError):
process.kill()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
pass
raise
finally:
selector.close()
process.stdout.close()
process.stderr.close()
return subprocess.CompletedProcess(
argv, returncode,
bytes(output[process.stdout]).decode("utf-8", errors="replace"),
bytes(output[process.stderr]).decode("utf-8", errors="replace"),
)
def _call(runner: Runner, argv: Sequence[str], *, created_uuid: str | None = None) -> str:
try:
result = runner(tuple(argv))
except (OSError, subprocess.TimeoutExpired, CreateError) as exc:
suffix = f"; retained partial VM {VM_NAME} ({created_uuid})" if created_uuid else ""
raise CreateError(f"bounded VBoxManage call failed: {argv[1]}{suffix}") from exc
if len(result.stdout) > MAX_CAPTURE or len(result.stderr) > MAX_CAPTURE:
raise CreateError("VBoxManage output exceeded the creation limit")
if result.returncode != 0:
suffix = f"; retained partial VM {VM_NAME} ({created_uuid})" if created_uuid else ""
raise CreateError(f"VBoxManage {argv[1]} failed with status {result.returncode}{suffix}")
return result.stdout
def _check_lab(lab_root: Path, iso_path: Path, owner_uid: int) -> None:
try:
root_info = lab_root.lstat()
except OSError as exc:
raise CreateError("private lab root is unavailable") from exc
if (not stat.S_ISDIR(root_info.st_mode) or stat.S_ISLNK(root_info.st_mode)
or root_info.st_uid != owner_uid or stat.S_IMODE(root_info.st_mode) != 0o700):
raise CreateError("lab root must be a real current-user directory with mode 0700")
if iso_path.parent.resolve(strict=True) != lab_root.resolve(strict=True):
raise CreateError("installer ISO must be directly inside the exact lab root")
try:
iso_info = iso_path.lstat()
except OSError as exc:
raise CreateError("pinned installer ISO is unavailable") from exc
if not stat.S_ISREG(iso_info.st_mode) or stat.S_ISLNK(iso_info.st_mode):
raise CreateError("installer ISO must be a regular non-symlink file")
digest = hashlib.sha256()
try:
with iso_path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
except OSError as exc:
raise CreateError("pinned installer ISO cannot be read") from exc
if digest.hexdigest() != guard.EXPECTED_ISO_SHA256:
raise CreateError("installer ISO SHA-256 does not match the pinned digest")
for path, description in (
(lab_root / guard.IDENTITY_FILE, "target identity manifest"),
(lab_root / VM_NAME, "exact target VM path"),
):
try:
path.lstat()
except FileNotFoundError:
continue
except OSError as exc:
raise CreateError(f"could not inspect {description}; refusing creation") from exc
if description == "target identity manifest":
raise CreateError(f"{description} already exists; refusing overwrite")
raise CreateError(f"{description} already exists; refusing creation")
def _check_absent(runner: Runner) -> None:
output = _call(runner, ("VBoxManage", "list", "vms"))
names: set[str] = set()
uuids: set[str] = set()
for line in output.splitlines():
if not line.strip():
continue
match = re.fullmatch(r'"([^"\r\n]+)" \{([0-9A-Fa-f-]{36})\}', line)
if not match:
raise CreateError("malformed VBoxManage inventory; refusing creation")
name, raw_uuid = match.groups()
try:
vm_uuid = str(uuid.UUID(raw_uuid))
except ValueError as exc:
raise CreateError("malformed VBoxManage inventory UUID; refusing creation") from exc
if name in names or vm_uuid in uuids:
raise CreateError("duplicate VBoxManage inventory identity; refusing creation")
names.add(name)
uuids.add(vm_uuid)
if VM_NAME in names:
raise CreateError("exact target name is already registered; refusing creation")
def _write_manifest(lab_root: Path, target_uuid: str, owner_uid: int) -> None:
marker = f"{VM_NAME}:{target_uuid}"
data = {
"schema": 1,
"name": VM_NAME,
"owner_uid": owner_uid,
"uuid": target_uuid,
"host_marker": marker,
}
path = lab_root / guard.IDENTITY_FILE
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
fd = os.open(path, flags, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as destination:
os.fchmod(destination.fileno(), 0o600)
json.dump(data, destination, sort_keys=True)
destination.write("\n")
destination.flush()
os.fsync(destination.fileno())
except OSError as exc:
raise CreateError(f"could not write identity manifest; retained partial VM {VM_NAME} ({target_uuid})") from exc
def create_target(
*, lab_root: Path = LAB_ROOT,
iso_path: Path = ISO_PATH,
owner_uid: int | None = None,
runner: Runner = run_command,
) -> str:
"""Preflight, create/configure the exact VM, and run the read-only guard."""
owner_uid = os.getuid() if owner_uid is None else owner_uid
try:
_check_lab(lab_root, iso_path, owner_uid)
_check_absent(runner)
except OSError as exc:
raise CreateError("creation preflight could not inspect the lab root or ISO") from exc
create_output = _call(runner, (
"VBoxManage", "createvm", "--name", VM_NAME, "--ostype", OS_TYPE,
"--basefolder", str(lab_root), "--register",
))
created_match = re.search(r"(?m)^UUID:\s*([0-9A-Fa-f-]{36})\s*$", create_output)
target_uuid: str | None = None
if created_match:
try:
target_uuid = str(uuid.UUID(created_match.group(1)))
except ValueError:
target_uuid = None
try:
info_text = _call(runner, (
"VBoxManage", "showvminfo", target_uuid or VM_NAME, "--machinereadable",
), created_uuid=target_uuid)
info = guard._machine_values(info_text)
if info.get("name") != VM_NAME or "UUID" not in info:
raise CreateError(f"created VM identity is unreadable; retained partial VM {VM_NAME} ({target_uuid or 'UUID unavailable'})")
reported_uuid = str(uuid.UUID(info["UUID"]))
if target_uuid is not None and target_uuid != reported_uuid:
raise CreateError(f"created VM UUID changed unexpectedly; retained partial VM {VM_NAME} ({target_uuid})")
target_uuid = reported_uuid
expected_cfg = lab_root / VM_NAME / f"{VM_NAME}.vbox"
if Path(info.get("CfgFile", "")) != expected_cfg:
raise CreateError(f"created VM path is unexpected; retained partial VM {VM_NAME} ({target_uuid})")
_call(runner, (
"VBoxManage", "modifyvm", target_uuid,
"--memory", str(MEMORY_MIB), "--cpus", str(CPUS),
"--nic1", "nat", "--nic2", "none", "--nic3", "none", "--nic4", "none",
"--nic5", "none", "--nic6", "none", "--nic7", "none", "--nic8", "none",
"--audio", "none", "--usb", "off",
"--boot1", "dvd", "--boot2", "disk", "--boot3", "none", "--boot4", "none",
), created_uuid=target_uuid)
disk = lab_root / VM_NAME / f"{VM_NAME}.vdi"
_call(runner, (
"VBoxManage", "createmedium", "disk", "--filename", str(disk),
"--size", str(DISK_SIZE_MIB), "--format", "VDI", "--variant", "Standard",
), created_uuid=target_uuid)
_call(runner, (
"VBoxManage", "storagectl", target_uuid, "--name", "SATA",
"--add", "sata", "--controller", "IntelAhci",
), created_uuid=target_uuid)
_call(runner, (
"VBoxManage", "storageattach", target_uuid, "--storagectl", "SATA",
"--port", "0", "--device", "0", "--type", "hdd", "--medium", str(disk),
), created_uuid=target_uuid)
_call(runner, (
"VBoxManage", "storageattach", target_uuid, "--storagectl", "SATA",
"--port", "1", "--device", "0", "--type", "dvddrive", "--medium", str(iso_path),
), created_uuid=target_uuid)
_write_manifest(lab_root, target_uuid, owner_uid)
_call(runner, (
"VBoxManage", "modifyvm", target_uuid,
"--description", f"{VM_NAME}:{target_uuid}",
), created_uuid=target_uuid)
try:
guard.inspect_target(lab_root=lab_root, owner_uid=owner_uid, runner=runner)
except (guard.GuardError, OSError) as exc:
raise CreateError(f"final read-only guard failed; retained partial VM {VM_NAME} ({target_uuid})") from exc
return target_uuid
except CreateError as exc:
if "retained partial" in str(exc):
raise
raise CreateError(f"{exc}; retained partial VM {VM_NAME} ({locals().get('target_uuid', 'UUID unavailable')})") from exc
except (ValueError, KeyError, OSError) as exc:
raise CreateError(f"creation identity/configuration failed; retained partial VM {VM_NAME} ({locals().get('target_uuid', 'UUID unavailable')})") from exc
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.parse_args(argv)
try:
target_uuid = create_target()
except CreateError as exc:
print(f"VM creation refused/stopped: {exc}", file=sys.stderr)
return 2
print(f"Created and guarded {VM_NAME} ({target_uuid}); VM remains powered off")
return 0
if __name__ == "__main__":
raise SystemExit(main())