- Introduced a standalone guest execution agent to facilitate command execution within the guest VM over Firecracker's vsock. - Enhanced FirecrackerClient to support vsock configuration, validating guest CID and UDS path. - Updated configuration to include vsock settings by default. - Refactored FirecrackerProcessManager to manage runtime sockets more effectively. - Implemented guest agent installation and provisioning in the image store. - Added API endpoints for executing commands in the VM and installing the guest agent. - Improved error handling for guest execution failures and agent availability.
205 lines
5.7 KiB
Python
205 lines
5.7 KiB
Python
"""Pydantic request and response models for the UVM HTTP API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ipaddress import IPv4Address
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
|
|
from .domain import VmRecord
|
|
from .errors import ValidationError
|
|
from .guest_agent import DEFAULT_TIMEOUT_SECONDS, MAX_COMMAND_BYTES, MAX_TIMEOUT_SECONDS
|
|
from .guest_exec import validate_exec
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
status: str = "ok"
|
|
|
|
|
|
class ServerExitResponse(BaseModel):
|
|
status: str = "stopping"
|
|
|
|
|
|
class InstallRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
force: bool = False
|
|
|
|
|
|
class InstallResponse(BaseModel):
|
|
firecracker: str
|
|
kernel: str
|
|
rootfs: str
|
|
|
|
|
|
class PortRuleModel(BaseModel):
|
|
"""A private guest TCP port; HTTP and HTTPS are descriptive labels only."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
port: int = Field(strict=True, ge=1, le=65535)
|
|
protocol: Literal["TCP", "HTTP", "HTTPS"] = "TCP"
|
|
|
|
|
|
class VmCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
cpu: float = Field(default=1, gt=0)
|
|
ram: str | int | float = "512"
|
|
storage: str | int | float = Field(default="30G", description="Disk capacity, minimum 30 GiB; bare numbers use GiB")
|
|
guest_ip: IPv4Address | None = None
|
|
username: str = "root"
|
|
password: SecretStr = SecretStr("root")
|
|
name: str | None = None
|
|
ports: list[PortRuleModel] | None = None
|
|
no_default_ports: bool = Field(default=False, strict=True)
|
|
|
|
|
|
class VmResizeRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
cpu: float | None = Field(default=None, gt=0, allow_inf_nan=False, strict=True)
|
|
ram: str | int | float | None = None
|
|
storage: str | int | float | None = Field(default=None, description="Disk capacity, minimum 30 GiB; bare numbers use GiB")
|
|
|
|
|
|
class VmResourcesModel(BaseModel):
|
|
cpu: float
|
|
ram_mib: int
|
|
storage_mib: int
|
|
|
|
|
|
class VmResponse(BaseModel):
|
|
id: str
|
|
name: str | None
|
|
cpu: float
|
|
ram_mib: int
|
|
storage_mib: int | None
|
|
pending_resize: VmResourcesModel | None
|
|
guest_ip: str
|
|
gateway: str
|
|
mac: str
|
|
username: str
|
|
status: str
|
|
observed_status: str
|
|
ports: list[PortRuleModel] | None
|
|
policy_status: str
|
|
operation: str | None
|
|
pid: int | None
|
|
created_at: int
|
|
updated_at: int
|
|
last_error: str | None
|
|
|
|
|
|
class DestroyResponse(BaseModel):
|
|
id: str
|
|
status: str = "terminated"
|
|
|
|
|
|
class PortUpdateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
mode: Literal["set", "open", "close"] = "set"
|
|
ports: list[PortRuleModel] = Field(default_factory=list)
|
|
|
|
|
|
class PortSyncResponse(BaseModel):
|
|
id: str
|
|
policy_status: str
|
|
status: str
|
|
|
|
|
|
class VmLogResponse(BaseModel):
|
|
content: str
|
|
truncated: bool
|
|
|
|
|
|
class VmExecRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
command: str = Field(strict=True, min_length=1, max_length=MAX_COMMAND_BYTES, repr=False,
|
|
description="Command or multiline script interpreted by /bin/sh inside the guest; at most 64 KiB UTF-8")
|
|
sudo: bool = Field(default=False, strict=True, description="Run as guest root instead of the VM's configured login user")
|
|
timeout_seconds: float = Field(default=DEFAULT_TIMEOUT_SECONDS, strict=True, gt=0,
|
|
le=MAX_TIMEOUT_SECONDS, allow_inf_nan=False)
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def validate_execution(cls, value: object) -> object:
|
|
# Use the domain error envelope: echoing NaN or unpaired surrogates in
|
|
# FastAPI's validation-error input can itself fail JSON serialization.
|
|
if not isinstance(value, dict) or set(value) - {"command", "sudo", "timeout_seconds"}:
|
|
raise ValidationError("execution requires an object with command, sudo, and timeout_seconds fields only")
|
|
validate_exec(value.get("command"), value.get("sudo", False), value.get("timeout_seconds", DEFAULT_TIMEOUT_SECONDS))
|
|
return value
|
|
|
|
|
|
class VmExecResponse(BaseModel):
|
|
exit_code: int | None
|
|
stdout: str
|
|
stderr: str
|
|
timed_out: bool
|
|
truncated: bool
|
|
|
|
|
|
class VmAgentInstallResponse(BaseModel):
|
|
id: str
|
|
status: Literal["installed"] = "installed"
|
|
restart_required: bool = True
|
|
|
|
|
|
class DiagnosticResponse(BaseModel):
|
|
name: str
|
|
ok: bool
|
|
message: str
|
|
|
|
|
|
class VmBulkActionRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
action: Literal["start", "stop", "restart", "delete", "ports-sync"]
|
|
identifiers: list[str] = Field(default_factory=list)
|
|
all_vms: bool = Field(default=False, strict=True)
|
|
timeout: float | None = None
|
|
|
|
|
|
class VmActionResultResponse(BaseModel):
|
|
id: str
|
|
action: str
|
|
outcome: str
|
|
error: str | None = None
|
|
|
|
|
|
def vm_response(
|
|
vm: VmRecord,
|
|
*,
|
|
observed_status: str | None = None,
|
|
policy_status: str = "unknown",
|
|
) -> VmResponse:
|
|
return VmResponse(
|
|
id=vm.id,
|
|
name=vm.name,
|
|
cpu=vm.cpu,
|
|
ram_mib=vm.ram_mib,
|
|
storage_mib=vm.storage_mib,
|
|
pending_resize=VmResourcesModel(**vm.pending_resize.to_dict()) if vm.pending_resize is not None else None,
|
|
guest_ip=vm.guest_ip,
|
|
gateway=vm.gateway,
|
|
mac=vm.mac,
|
|
username=vm.username,
|
|
status=vm.status,
|
|
observed_status=observed_status if observed_status is not None else vm.status,
|
|
ports=(
|
|
[PortRuleModel(port=rule.port, protocol=rule.protocol) for rule in vm.ports]
|
|
if vm.ports is not None
|
|
else None
|
|
),
|
|
policy_status=policy_status,
|
|
operation=vm.operation,
|
|
pid=vm.pid,
|
|
created_at=vm.created_at,
|
|
updated_at=vm.updated_at,
|
|
last_error=vm.last_error,
|
|
)
|