feat: Add storage management for VM creation and resizing
- Introduced storage parameter in VmCreateRequest and VmResizeRequest models. - Implemented storage validation to ensure minimum and maximum limits. - Enhanced CLI to support storage options during VM creation and resizing. - Updated lifecycle management to handle pending resize operations for VM resources. - Added methods for resizing VM disks and validating storage capacity. - Updated API endpoints to accommodate new storage functionality. - Refactored related validation and configuration logic to support storage management.
This commit is contained in:
+49
-1
@@ -2,16 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any
|
||||
|
||||
from .config import MAX_STORAGE_MIB, MIN_STORAGE_MIB
|
||||
from .errors import StateError, ValidationError
|
||||
|
||||
|
||||
STATE_SCHEMA_VERSION = 2
|
||||
STATE_SCHEMA_VERSION = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -52,6 +54,40 @@ class VmSpec:
|
||||
password: str = field(default="root", repr=False)
|
||||
name: str | None = None
|
||||
ports: tuple[PortRule, ...] = (PortRule(22),)
|
||||
storage_mib: int = MIN_STORAGE_MIB
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VmResources:
|
||||
"""Complete resource target retained while a stop/resize/start is pending."""
|
||||
|
||||
cpu: float
|
||||
ram_mib: int
|
||||
storage_mib: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
try:
|
||||
valid_cpu = type(self.cpu) in (int, float) and math.isfinite(self.cpu) and self.cpu > 0
|
||||
except OverflowError:
|
||||
valid_cpu = False
|
||||
if not valid_cpu:
|
||||
raise ValidationError("CPU must be a finite value greater than 0")
|
||||
if type(self.ram_mib) is not int or self.ram_mib < 128:
|
||||
raise ValidationError("RAM must be an integer of at least 128 MiB")
|
||||
if type(self.storage_mib) is not int or not MIN_STORAGE_MIB <= self.storage_mib <= MAX_STORAGE_MIB:
|
||||
raise ValidationError(f"storage must be between {MIN_STORAGE_MIB} and {MAX_STORAGE_MIB} MiB (minimum 30 GiB)")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {"cpu": self.cpu, "ram_mib": self.ram_mib, "storage_mib": self.storage_mib}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> "VmResources":
|
||||
if not isinstance(value, dict) or set(value) != {"cpu", "ram_mib", "storage_mib"}:
|
||||
raise StateError("invalid pending resize resources")
|
||||
try:
|
||||
return cls(**value)
|
||||
except ValidationError as error:
|
||||
raise StateError(f"invalid pending resize resources: {error}") from error
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -87,12 +123,16 @@ class VmRecord:
|
||||
spawn_pending: bool = False
|
||||
network: str | None = None
|
||||
bridge: str | None = None
|
||||
storage_mib: int | None = None
|
||||
pending_resize: VmResources | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"id": self.id,
|
||||
"cpu": self.cpu,
|
||||
"ram_mib": self.ram_mib,
|
||||
"storage_mib": self.storage_mib,
|
||||
"pending_resize": self.pending_resize.to_dict() if self.pending_resize is not None else None,
|
||||
"guest_ip": self.guest_ip,
|
||||
"gateway": self.gateway,
|
||||
"tap": self.tap,
|
||||
@@ -155,6 +195,12 @@ class VmRecord:
|
||||
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"}
|
||||
storage_mib = value.get("storage_mib")
|
||||
if storage_mib is not None and (
|
||||
type(storage_mib) is not int or not MIN_STORAGE_MIB <= storage_mib <= MAX_STORAGE_MIB
|
||||
):
|
||||
raise StateError("VM storage_mib must be a supported integer capacity of at least 30 GiB or null")
|
||||
pending_resize = value.get("pending_resize")
|
||||
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")
|
||||
@@ -200,6 +246,8 @@ class VmRecord:
|
||||
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,
|
||||
storage_mib=storage_mib,
|
||||
pending_resize=VmResources.from_dict(pending_resize) if pending_resize is not None else None,
|
||||
)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise StateError(f"invalid VM record: {error}") from error
|
||||
|
||||
Reference in New Issue
Block a user