Refactor VM lifecycle management and enhance validation
- Introduced a new method `installed_kernel` in `ImageStore` to handle kernel verification and checksum validation. - Updated `Installer` to include additional packages: `nftables` and `conntrack`. - Refined `LifecycleService` to improve error handling and state management during VM creation, starting, stopping, and deletion. - Enhanced port management with new methods for setting and syncing ports, including validation for port rules. - Improved error handling in `StateStore` to raise specific `NotFoundError` and `ConflictError` exceptions. - Updated network management to ensure proper TAP device handling and firewall integration. - Enhanced API responses in `vms.py` to include policy status for better client-side handling. - Added validation for VM names and ports in `validation.py` to enforce stricter rules and prevent conflicts.
This commit is contained in:
+73
-2
@@ -8,10 +8,37 @@ from dataclasses import dataclass, field
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any
|
||||
|
||||
from .errors import StateError
|
||||
from .errors import StateError, ValidationError
|
||||
|
||||
|
||||
STATE_SCHEMA_VERSION = 1
|
||||
STATE_SCHEMA_VERSION = 2
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PortRule:
|
||||
"""One private guest TCP port; HTTP/HTTPS are descriptive service labels."""
|
||||
|
||||
port: int
|
||||
protocol: str = "TCP"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if type(self.port) is not int or not 1 <= self.port <= 65535:
|
||||
raise ValidationError("port must be an integer between 1 and 65535")
|
||||
if not isinstance(self.protocol, str) or self.protocol.upper() not in {"TCP", "HTTP", "HTTPS"}:
|
||||
raise ValidationError("port type must be TCP, HTTP, or HTTPS")
|
||||
object.__setattr__(self, "protocol", self.protocol.upper())
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {"port": self.port, "protocol": self.protocol}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> "PortRule":
|
||||
if not isinstance(value, dict) or set(value) - {"port", "protocol"}:
|
||||
raise StateError("invalid port rule")
|
||||
try:
|
||||
return cls(port=value["port"], protocol=value.get("protocol", "TCP"))
|
||||
except (KeyError, ValidationError) as error:
|
||||
raise StateError(f"invalid port rule: {error}") from error
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -23,6 +50,8 @@ class VmSpec:
|
||||
guest_ip: IPv4Address | None = None
|
||||
username: str = "root"
|
||||
password: str = field(default="root", repr=False)
|
||||
name: str | None = None
|
||||
ports: tuple[PortRule, ...] = (PortRule(22),)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -48,6 +77,16 @@ class VmRecord:
|
||||
created_at: int = field(default_factory=lambda: int(time.time()))
|
||||
updated_at: int = field(default_factory=lambda: int(time.time()))
|
||||
last_error: str | None = None
|
||||
name: str | None = None
|
||||
ports: tuple[PortRule, ...] | None = None
|
||||
disk_ready: bool = False
|
||||
runtime_owned: bool = False
|
||||
tap_owned: bool = False
|
||||
operation: str | None = None
|
||||
boot_id: str | None = None
|
||||
spawn_pending: bool = False
|
||||
network: str | None = None
|
||||
bridge: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
@@ -67,6 +106,16 @@ class VmRecord:
|
||||
"status": self.status,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"name": self.name,
|
||||
"ports": [port.to_dict() for port in self.ports] if self.ports is not None else None,
|
||||
"disk_ready": self.disk_ready,
|
||||
"runtime_owned": self.runtime_owned,
|
||||
"tap_owned": self.tap_owned,
|
||||
"operation": self.operation,
|
||||
"boot_id": self.boot_id,
|
||||
"spawn_pending": self.spawn_pending,
|
||||
"network": self.network,
|
||||
"bridge": self.bridge,
|
||||
}
|
||||
if self.pid is not None:
|
||||
data["pid"] = self.pid
|
||||
@@ -99,6 +148,16 @@ class VmRecord:
|
||||
|
||||
try:
|
||||
pid_value = value.get("pid")
|
||||
raw_ports = value.get("ports")
|
||||
if raw_ports is not None and not isinstance(raw_ports, list):
|
||||
raise StateError("VM ports must be a list or null for a legacy VM")
|
||||
ports = tuple(PortRule.from_dict(port) for port in raw_ports) if raw_ports is not None else None
|
||||
if ports is not None and len({port.port for port in ports}) != len(ports):
|
||||
raise StateError("VM has duplicate TCP port rules")
|
||||
legacy_ready = bool(value.get("disk")) and value.get("status") in {"running", "stopped"}
|
||||
for name in ("disk_ready", "runtime_owned", "tap_owned", "spawn_pending"):
|
||||
if name in value and type(value[name]) is not bool:
|
||||
raise StateError(f"VM {name} must be a boolean")
|
||||
return cls(
|
||||
id=str(value["id"]),
|
||||
cpu=float(value["cpu"]),
|
||||
@@ -131,6 +190,16 @@ class VmRecord:
|
||||
if value.get("last_error") is not None
|
||||
else None
|
||||
),
|
||||
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"))),
|
||||
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,
|
||||
spawn_pending=value.get("spawn_pending", value.get("operation") == "spawning"),
|
||||
network=str(value["network"]) if value.get("network") is not None else None,
|
||||
bridge=str(value["bridge"]) if value.get("bridge") is not None else None,
|
||||
)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise StateError(f"invalid VM record: {error}") from error
|
||||
@@ -170,6 +239,8 @@ class State:
|
||||
raise StateError("state file has an invalid vms collection")
|
||||
|
||||
vms = {str(vm_id): VmRecord.from_dict(vm) for vm_id, vm in raw_vms.items()}
|
||||
if any(vm_id != vm.id for vm_id, vm in vms.items()):
|
||||
raise StateError("VM record ID does not match its registry key")
|
||||
next_mac_index = value.get("next_mac_index")
|
||||
if next_mac_index is None:
|
||||
next_mac_index = _next_mac_index(vms.values())
|
||||
|
||||
Reference in New Issue
Block a user