Enhance documentation and validation for VM lifecycle and network management; add tests for legacy cleanup and address validation.
This commit is contained in:
+20
-1
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user