Refactor documentation structure and improve validation checks; update VM state handling and error management.
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user