Enhance documentation and validation for VM lifecycle and network management; add tests for legacy cleanup and address validation.

This commit is contained in:
2026-09-09 19:21:00 +05:30
parent ee26962902
commit 3d7af4adb8
9 changed files with 366 additions and 35 deletions
+20 -1
View File
@@ -89,6 +89,8 @@ class FirewallManager:
current = self._read_table(table["name"])
if current is not None:
self._transaction([self._delete_owned(current, table)])
if self._read_table(table["name"]) is not None:
raise UvmError("firewall table remains or was replaced after removal; keep the VM reservation")
def status(self, vm: VmRecord) -> str:
"""Inspect actual hooks, ordered rules, identity and complete port set.
@@ -137,9 +139,26 @@ class FirewallManager:
All four tuple addresses are checked, including NAT reply tuples. A
failed deletion is tolerated only if a successful list proves absence.
The caller must retain the address reservation until this succeeds.
Legacy non-RFC1918 addresses are allowed here; only the recorded subnet,
when present, determines host-address boundaries, never current settings.
"""
self._require_offline(vm)
address = self._guest_ip(vm)
try:
address = IPv4Address(str(vm.guest_ip))
stored_network = getattr(vm, "network", None)
network = IPv4Network(str(stored_network)) if stored_network is not None else None
except (ValueError, TypeError) as error:
raise ValidationError("conntrack cleanup requires a valid persisted IPv4 address/network") from error
if (address.is_unspecified or address.is_loopback or address.is_link_local
or address.is_multicast or address == IPv4Address("255.255.255.255")):
raise ValidationError("conntrack cleanup requires a usable persisted unicast IPv4 address")
# /31 point-to-point subnets and /32 host routes have no broadcast address.
if network is not None and (
address not in network or (network.prefixlen < 31
and address in {network.network_address, network.broadcast_address})
):
raise ValidationError("conntrack cleanup requires a usable guest IP inside its persisted network")
address = str(address)
for field in ("--orig-src", "--orig-dst", "--reply-src", "--reply-dst"):
selector = ("--family", "ipv4", field, address)
self._run(("conntrack", "--delete", *selector), check=False)
+5 -1
View File
@@ -104,6 +104,10 @@ class LifecycleService:
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")):
raise ValidationError("private ports require a network inside an RFC 1918 IPv4 range")
check_kvm()
self._verify_firecracker()
self._network.firewall.check_available()
def _verify_firecracker(self) -> None:
manifest = load_manifest(self._settings.integrity_manifest_path)
checksum = self._settings.firecracker_binary_sha256
if manifest.verified or self._settings.allow_unverified_downloads:
@@ -111,7 +115,6 @@ class LifecycleService:
verify_file(self._settings.firecracker_binary, checksum, "Firecracker binary",
"UVM_FIRECRACKER_BINARY_SHA256",
allow_unverified=self._settings.allow_unverified_downloads)
self._network.firewall.check_available()
def _boot(self, vm: VmRecord, kernel: Path) -> None:
vm.status, vm.operation = "starting", "network"
@@ -414,6 +417,7 @@ class LifecycleService:
def diagnostics(self) -> list[dict]:
checks = [("root", require_root), ("kvm", check_kvm),
("Firecracker", self._verify_firecracker),
("guest assets", self._images.installed_assets),
("private firewall", self._network.firewall.check_available)]
results = []
+1 -22
View File
@@ -9,7 +9,7 @@ from ipaddress import IPv4Address
from .config import Settings
from .domain import VmRecord
from .errors import ConflictError, TapCreationError, UvmError, ValidationError
from .errors import ConflictError, UvmError, ValidationError
from .firewall import FirewallManager
from .system import CommandRunner
@@ -166,27 +166,6 @@ class NetworkManager:
raise ConflictError(f"refusing to modify unowned TAP {vm.tap}")
return link
def create_tap(self, name: str) -> None:
existing = self._runner.run(("ip", "link", "show", name), check=False, capture=True)
if existing.returncode == 0:
raise UvmError(f"TAP device already exists: {name}")
created = False
try:
self._runner.run(("ip", "tuntap", "add", "dev", name, "mode", "tap"))
created = True
self._runner.run(("ip", "link", "set", name, "master", self._settings.bridge))
self._runner.run(("ip", "link", "set", name, "up"))
except BaseException as error:
if created:
try:
self.delete_tap(name)
except UvmError as cleanup_error:
raise TapCreationError(
f"could not configure TAP device {name}; cleanup also failed: {cleanup_error}",
tap_created=True,
) from error
raise
def delete_tap(self, name: str) -> None:
deleted = self._runner.run(("ip", "link", "del", name), check=False, capture=True)
if deleted.returncode == 0: