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
+279
View File
@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""Resume configuration of the one reviewed, powered-off partial M9 VM."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import sys
from typing import Sequence
import uuid
import guard
import create
LAB_ROOT = guard.LAB_ROOT
ISO_PATH = LAB_ROOT / guard.EXPECTED_ISO
VM_NAME = "wga-m9-debian137-20261025"
TARGET_UUID = "b14c850b-a21e-4a56-acca-785590f0c4ac"
PINNED_ISO_SHA256 = "a7ef94ac2fb9a7fec454552abd629b7cc9d5155c886165a45649f5ce6167e355"
class ResumeError(Exception):
"""The partial VM could not safely be resumed or configured."""
Runner = create.Runner
def _require_absent(path: Path, description: str) -> None:
try:
path.lstat()
except FileNotFoundError:
return
except OSError as exc:
raise ResumeError(f"could not inspect {description}; refusing resume") from exc
raise ResumeError(f"{description} already exists; refusing resume")
def _check_iso(iso_path: Path) -> None:
if guard.EXPECTED_ISO_SHA256 != PINNED_ISO_SHA256:
raise ResumeError("configured ISO digest differs from the pinned digest")
if iso_path != ISO_PATH:
raise ResumeError("installer ISO path is not the pinned lab path")
try:
if iso_path.parent.resolve(strict=True) != LAB_ROOT.resolve(strict=True):
raise ResumeError("installer ISO must be directly inside the exact lab root")
info = iso_path.lstat()
except OSError as exc:
raise ResumeError("pinned installer ISO is unavailable") from exc
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode):
raise ResumeError("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 ResumeError("pinned installer ISO cannot be read") from exc
if digest.hexdigest() != PINNED_ISO_SHA256:
raise ResumeError("installer ISO SHA-256 does not match the pinned digest")
def _inventory(runner: Runner) -> None:
output = guard._invoke(runner, ("VBoxManage", "list", "vms"))
names: set[str] = set()
uuids: set[str] = set()
matches: list[tuple[str, str]] = []
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 ResumeError("malformed VBoxManage inventory; refusing resume")
name, raw_uuid = match.groups()
try:
vm_uuid = str(uuid.UUID(raw_uuid))
except ValueError as exc:
raise ResumeError("malformed VBoxManage inventory UUID; refusing resume") from exc
if name in names or vm_uuid in uuids:
raise ResumeError("duplicate VBoxManage inventory identity; refusing resume")
names.add(name)
uuids.add(vm_uuid)
if name == VM_NAME or vm_uuid == TARGET_UUID:
matches.append((name, vm_uuid))
if matches != [(VM_NAME, TARGET_UUID)]:
raise ResumeError("registered VM inventory does not match the pinned partial target")
def _preflight_info(info: dict[str, str], lab_root: Path, owner_uid: int) -> None:
if info.get("name") != VM_NAME or info.get("UUID", "").lower() != TARGET_UUID:
raise ResumeError("showvminfo identity does not match the pinned partial target")
if info.get("VMState") != "poweroff":
raise ResumeError("partial target must be powered off")
if info.get("description", ""):
raise ResumeError("partial target already has a host marker; refusing resume")
if info.get("memory") != "1024" or info.get("cpus") != "1":
raise ResumeError("partial target default memory or CPU state is unexpected")
cfg_file = Path(info.get("CfgFile", ""))
if cfg_file != lab_root / VM_NAME / f"{VM_NAME}.vbox" or cfg_file.is_symlink():
raise ResumeError("partial target configuration path is not the exact lab path")
try:
folder_info = (lab_root / VM_NAME).lstat()
config_info = cfg_file.lstat()
except OSError as exc:
raise ResumeError("partial target configuration file is unavailable") from exc
if (not stat.S_ISDIR(folder_info.st_mode) or stat.S_ISLNK(folder_info.st_mode)
or folder_info.st_uid != owner_uid
or (lab_root / VM_NAME).resolve(strict=True) != lab_root / VM_NAME):
raise ResumeError("partial target folder must be a real directory at the exact lab path")
if (not stat.S_ISREG(config_info.st_mode) or stat.S_ISLNK(config_info.st_mode)
or config_info.st_uid != owner_uid):
raise ResumeError("partial target configuration must be a current-user-owned regular file")
nics = {int(match.group(1)): value for key, value in info.items()
if (match := re.fullmatch(r"nic([1-8])", key))}
if (len(nics) != 8 or nics.get(1) != "nat"
or any(nics.get(slot) not in {"none", "null"} for slot in range(2, 9))):
raise ResumeError("partial target NIC state must be NIC 1 in NAT with NICs 2-8 disabled")
if any((key.lower().startswith("natpf") or key.lower().startswith("forwarding(")) and value
for key, value in info.items()):
raise ResumeError("partial target has NAT forwarding; refusing resume")
if any(key.startswith("SharedFolder") and value for key, value in info.items()):
raise ResumeError("partial target has shared folders; refusing resume")
storage_rows = [
(key, value) for key, value in info.items()
if re.fullmatch(r"[A-Za-z0-9_-]+-[0-9]+-[0-9]+", key)
]
if any(value not in {"", "emptydrive", "none"} for _, value in storage_rows):
raise ResumeError("partial target has attached storage; refusing resume")
if any(key.startswith("storagecontroller") for key in info):
raise ResumeError("partial target has a storage controller; refusing resume")
def _preflight(
*, lab_root: Path, iso_path: Path, owner_uid: int, runner: Runner,
) -> None:
if lab_root != LAB_ROOT:
raise ResumeError("resume is restricted to the pinned lab root")
try:
root_info = lab_root.lstat()
except OSError as exc:
raise ResumeError("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 ResumeError("lab root must be a real current-user directory with mode 0700")
_check_iso(iso_path)
_require_absent(lab_root / guard.IDENTITY_FILE, "target identity manifest")
_require_absent(lab_root / VM_NAME / f"{VM_NAME}.vdi", "target VDI")
_inventory(runner)
info_text = guard._invoke(runner, (
"VBoxManage", "showvminfo", TARGET_UUID, "--machinereadable",
))
try:
info = guard._machine_values(info_text)
except guard.GuardError as exc:
raise ResumeError("partial target machine-readable state is invalid") from exc
_preflight_info(info, lab_root, owner_uid)
def _write_manifest(lab_root: Path, 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 ResumeError(f"could not write identity manifest; retained partial VM {VM_NAME} ({TARGET_UUID})") from exc
def _call(runner: Runner, argv: Sequence[str]) -> str:
try:
result = runner(tuple(argv))
except (OSError, subprocess.TimeoutExpired, create.CreateError, guard.GuardError) as exc:
raise ResumeError(f"bounded VBoxManage call failed: {argv[1]}") from exc
if len(result.stdout) > create.MAX_CAPTURE or len(result.stderr) > create.MAX_CAPTURE:
raise ResumeError("VBoxManage output exceeded the resume limit")
if result.returncode != 0:
raise ResumeError(f"VBoxManage {argv[1]} failed with status {result.returncode}")
return result.stdout
def resume_target(
*, lab_root: Path = LAB_ROOT,
iso_path: Path = ISO_PATH,
owner_uid: int | None = None,
runner: Runner = create.run_command,
) -> str:
"""Preflight the exact partial VM, finish setup, and run the read-only guard."""
owner_uid = os.getuid() if owner_uid is None else owner_uid
try:
_preflight(lab_root=lab_root, iso_path=iso_path, owner_uid=owner_uid, runner=runner)
except (OSError, guard.GuardError) as exc:
raise ResumeError("resume preflight failed closed") from exc
disk_path = lab_root / VM_NAME / f"{VM_NAME}.vdi"
try:
_call(runner, (
"VBoxManage", "modifyvm", TARGET_UUID,
"--memory", str(create.MEMORY_MIB), "--cpus", str(create.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",
))
_call(runner, (
"VBoxManage", "createmedium", "disk", "--filename", str(disk_path),
"--size", str(create.DISK_SIZE_MIB), "--format", "VDI", "--variant", "Standard",
))
_call(runner, (
"VBoxManage", "storagectl", TARGET_UUID, "--name", "SATA",
"--add", "sata", "--controller", "IntelAhci",
))
_call(runner, (
"VBoxManage", "storageattach", TARGET_UUID, "--storagectl", "SATA",
"--port", "0", "--device", "0", "--type", "hdd", "--medium", str(disk_path),
))
_call(runner, (
"VBoxManage", "storageattach", TARGET_UUID, "--storagectl", "SATA",
"--port", "1", "--device", "0", "--type", "dvddrive", "--medium", str(iso_path),
))
_write_manifest(lab_root, owner_uid)
_call(runner, (
"VBoxManage", "modifyvm", TARGET_UUID,
"--description", f"{VM_NAME}:{TARGET_UUID}",
))
try:
selected_uuid, _ = guard.inspect_target(
lab_root=lab_root, owner_uid=owner_uid, runner=runner,
)
except (guard.GuardError, OSError) as exc:
raise ResumeError("final read-only guard failed") from exc
if selected_uuid != TARGET_UUID:
raise ResumeError("final guard returned an unexpected UUID")
return TARGET_UUID
except ResumeError as exc:
if "retained partial VM" in str(exc):
raise
raise ResumeError(f"{exc}; retained partial VM {VM_NAME} ({TARGET_UUID})") from exc
except (OSError, ValueError, KeyError, create.CreateError, guard.GuardError) as exc:
raise ResumeError(f"resume configuration failed; retained partial VM {VM_NAME} ({TARGET_UUID})") from exc
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.parse_args(argv)
try:
target_uuid = resume_target()
except ResumeError as exc:
print(f"VM resume refused/stopped: {exc}", file=sys.stderr)
return 2
print(f"Resumed and guarded {VM_NAME} ({target_uuid}); VM remains powered off")
return 0
if __name__ == "__main__":
raise SystemExit(main())