Refactor documentation structure and improve validation checks; update VM state handling and error management.

This commit is contained in:
2026-09-09 17:47:50 +05:30
parent d8a25209b9
commit ee26962902
9 changed files with 86 additions and 31 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ class VmRecord:
name=str(value["name"]) if value.get("name") is not None else None,
ports=ports,
disk_ready=value.get("disk_ready", legacy_ready),
runtime_owned=value.get("runtime_owned", bool(value.get("disk"))),
runtime_owned=value.get("runtime_owned", True),
tap_owned=value.get("tap_owned", bool(value.get("pid"))),
operation=str(value["operation"]) if value.get("operation") is not None else None,
boot_id=str(value["boot_id"]) if value.get("boot_id") is not None else None,
+14 -2
View File
@@ -7,6 +7,7 @@ import shutil
import time
from collections.abc import Callable, Sequence
from dataclasses import dataclass, replace
from ipaddress import IPv4Network
from pathlib import Path
from .config import Settings
@@ -99,6 +100,9 @@ class LifecycleService:
raise UvmError(message) from error
def _check_boot(self) -> None:
if not any(self._settings.network.subnet_of(IPv4Network(network))
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()
manifest = load_manifest(self._settings.integrity_manifest_path)
checksum = self._settings.firecracker_binary_sha256
@@ -258,12 +262,13 @@ class LifecycleService:
self._network.firewall.remove(vm)
def set_ports(self, identifier: str, ports: tuple[PortRule, ...], *, mode: str = "set") -> VmRecord:
"""Mutate a frozen registry ID; the CLI resolves names before confirmation."""
require_root()
ports = self._validated_ports(ports)
if mode not in {"open", "close", "set"}:
raise ValidationError(f"invalid port operation: {mode}")
with self._state_store.operation_lock():
vm = self._load_for_operation(identifier)
vm = self._load_for_operation(identifier, exact_id=True)
self._not_deleting(vm)
observation = self._process.observe(vm)
if observation == "unknown":
@@ -321,7 +326,8 @@ class LifecycleService:
containment_errors = []
for contain in (
lambda: self._network.deactivate_tap(vm),
lambda: self._network.firewall.apply(vm, quarantine=True),
lambda: (self._network.firewall.apply(vm, quarantine=True)
if self._network.has_tap(vm) else None),
):
try:
contain()
@@ -457,6 +463,12 @@ class LifecycleService:
def _reserve_vm(self, spec: VmSpec) -> VmRecord:
with self._state_store.transaction() as state:
for existing in state.vms.values():
if (existing.guest_ip == str(self._settings.gateway)
or existing.gateway != str(self._settings.gateway)
or existing.network not in {None, str(self._settings.network)}
or existing.bridge not in {None, self._settings.bridge}):
raise ConflictError("configured topology conflicts with retained VMs; keep their network settings")
if spec.name is not None and any(spec.name in {vm.name, vm.id} for vm in state.vms.values()):
raise ConflictError(f"VM name already exists: {spec.name}")
vm_id = new_vm_id()
+7 -2
View File
@@ -141,7 +141,10 @@ class NetworkManager:
result = self._runner.run(("ip", "-j", "-d", "link", "show"), capture=True)
try:
links = json.loads(result.stdout)
if not isinstance(links, list) or any(not isinstance(link, dict) for link in links):
if not isinstance(links, list) or any(
not isinstance(link, dict) or not isinstance(link.get("ifname"), str)
or not link["ifname"] for link in links
):
raise ValueError("invalid interface inventory")
matches = [link for link in links if link.get("ifname") == name]
if len(matches) > 1:
@@ -155,7 +158,9 @@ class NetworkManager:
if link is None:
return None
info = link.get("linkinfo", {})
is_tap = info.get("info_kind") == "tun" and info.get("info_data", {}).get("type") == "tap"
is_tap = (isinstance(info, dict) and info.get("info_kind") == "tun"
and isinstance(info.get("info_data"), dict)
and info["info_data"].get("type") == "tap")
legacy = vm.ports is None and vm.tap_owned and not link.get("ifalias")
if not is_tap or (link.get("ifalias") != f"uvm:{vm.id}" and not legacy):
raise ConflictError(f"refusing to modify unowned TAP {vm.tap}")
+5 -1
View File
@@ -86,7 +86,7 @@ def parse_password(value: str) -> str:
def parse_name(value: str) -> str:
if re.fullmatch(r"[a-z][a-z0-9-]{0,62}", value) is None:
if not isinstance(value, str) or re.fullmatch(r"[a-z][a-z0-9-]{0,62}", value) is None:
raise ValidationError("VM name must be 1-63 lowercase letters, digits, or hyphens, starting with a letter")
if re.fullmatch(r"vm-[0-9a-f]{32}", value):
raise ValidationError("VM names must not impersonate generated VM IDs")
@@ -94,8 +94,12 @@ def parse_name(value: str) -> str:
def parse_ports(values: Sequence[str]) -> tuple[PortRule, ...]:
if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
raise ValidationError("ports must be a sequence of PORT[:TYPE] strings")
ports: dict[int, PortRule] = {}
for value in values:
if not isinstance(value, str):
raise ValidationError("each port specification must be a string")
for item in value.split(","):
match = re.fullmatch(r"\s*(\d+)(?::(TCP|HTTP|HTTPS))?\s*", item, re.IGNORECASE)
if match is None: