- 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.
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
"""VM lifecycle endpoints backed by the existing UVM lifecycle service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, Query, status
|
|
|
|
from ..api_models import (
|
|
DestroyResponse,
|
|
PortSyncResponse,
|
|
PortUpdateRequest,
|
|
VmActionResultResponse,
|
|
VmBulkActionRequest,
|
|
VmCreateRequest,
|
|
VmLogResponse,
|
|
VmResponse,
|
|
VmResizeRequest,
|
|
vm_response,
|
|
)
|
|
from ..app import Application
|
|
from ..domain import PortRule, VmRecord, VmSpec
|
|
from ..errors import ValidationError
|
|
from ..validation import (
|
|
parse_cpu,
|
|
parse_name,
|
|
parse_password,
|
|
parse_ports,
|
|
parse_ram,
|
|
parse_storage,
|
|
parse_timeout,
|
|
parse_username,
|
|
)
|
|
from .dependencies import get_authorized_application
|
|
|
|
|
|
router = APIRouter(prefix="/vms", tags=["vms"])
|
|
|
|
|
|
def _mutation_policy_status(vm: VmRecord) -> str:
|
|
"""Return the policy state that follows a successful lifecycle mutation."""
|
|
if vm.ports is None:
|
|
return "unmanaged"
|
|
if vm.operation is not None:
|
|
return "unknown"
|
|
if vm.status == "running":
|
|
return "enforced"
|
|
if vm.status == "stopped":
|
|
return "inactive"
|
|
return "unknown"
|
|
|
|
|
|
@router.get("", response_model=list[VmResponse])
|
|
def list_vms(
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> list[VmResponse]:
|
|
return [
|
|
vm_response(
|
|
listed.vm,
|
|
observed_status=listed.observed_status,
|
|
policy_status=listed.policy_status,
|
|
)
|
|
for listed in application.lifecycle.list_vms()
|
|
]
|
|
|
|
|
|
@router.post("", response_model=VmResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_vm(
|
|
request: VmCreateRequest,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
ports = parse_ports([f"{rule.port}:{rule.protocol}" for rule in request.ports or ()])
|
|
if not request.no_default_ports and not any(rule.port == 22 for rule in ports):
|
|
ports = tuple(sorted((*ports, PortRule(22)), key=lambda rule: rule.port))
|
|
vm = application.lifecycle.create(
|
|
VmSpec(
|
|
cpu=parse_cpu(str(request.cpu)),
|
|
ram_mib=parse_ram(str(request.ram)),
|
|
storage_mib=parse_storage(str(request.storage)),
|
|
guest_ip=request.guest_ip,
|
|
username=parse_username(request.username),
|
|
password=parse_password(request.password.get_secret_value()),
|
|
name=parse_name(request.name) if request.name is not None else None,
|
|
ports=ports,
|
|
)
|
|
)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.post("/actions", response_model=list[VmActionResultResponse])
|
|
def run_bulk_action(
|
|
request: VmBulkActionRequest,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> list[VmActionResultResponse]:
|
|
timeout = parse_timeout(str(request.timeout)) if request.timeout is not None else None
|
|
targets = application.lifecycle.resolve_targets(request.identifiers, all_vms=request.all_vms)
|
|
results = application.lifecycle.run_many(
|
|
request.action,
|
|
[vm.id for vm in targets],
|
|
timeout=timeout,
|
|
)
|
|
return [
|
|
VmActionResultResponse(
|
|
id=result.id,
|
|
action=result.action,
|
|
outcome=result.outcome,
|
|
error=result.error,
|
|
)
|
|
for result in results
|
|
]
|
|
|
|
|
|
@router.post("/{vm_id}/start", response_model=VmResponse)
|
|
def start_vm(
|
|
vm_id: str,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
vm = application.lifecycle.start(vm_id)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.post("/{vm_id}/restart", response_model=VmResponse)
|
|
def restart_vm(
|
|
vm_id: str,
|
|
timeout: float | None = None,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
vm = (
|
|
application.lifecycle.restart(vm_id)
|
|
if timeout is None
|
|
else application.lifecycle.restart(vm_id, timeout=timeout)
|
|
)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.post("/{vm_id}/stop", response_model=VmResponse)
|
|
def stop_vm(
|
|
vm_id: str,
|
|
timeout: float | None = None,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
vm = (
|
|
application.lifecycle.stop(vm_id)
|
|
if timeout is None
|
|
else application.lifecycle.stop(vm_id, timeout=timeout)
|
|
)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.post("/{vm_id}/resize", response_model=VmResponse)
|
|
def resize_vm(
|
|
vm_id: str,
|
|
request: VmResizeRequest,
|
|
timeout: float | None = None,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
if request.cpu is None and request.ram is None and request.storage is None:
|
|
raise ValidationError("resize requires at least one of CPU, RAM, or storage")
|
|
vm = application.lifecycle.resize(
|
|
vm_id,
|
|
cpu=parse_cpu(str(request.cpu)) if request.cpu is not None else None,
|
|
ram_mib=parse_ram(str(request.ram)) if request.ram is not None else None,
|
|
storage_mib=parse_storage(str(request.storage)) if request.storage is not None else None,
|
|
timeout=parse_timeout(str(timeout)) if timeout is not None else None,
|
|
)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.delete("/{vm_id}", response_model=DestroyResponse)
|
|
def destroy_vm(
|
|
vm_id: str,
|
|
timeout: float | None = None,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> DestroyResponse:
|
|
vm = (
|
|
application.lifecycle.destroy(vm_id)
|
|
if timeout is None
|
|
else application.lifecycle.destroy(vm_id, timeout=timeout)
|
|
)
|
|
return DestroyResponse(id=vm.id)
|
|
|
|
|
|
@router.post("/{vm_id}/ports", response_model=VmResponse)
|
|
def update_ports(
|
|
vm_id: str,
|
|
request: PortUpdateRequest,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
ports = parse_ports([f"{rule.port}:{rule.protocol}" for rule in request.ports])
|
|
vm = application.lifecycle.set_ports(vm_id, ports, mode=request.mode)
|
|
return vm_response(vm, policy_status=_mutation_policy_status(vm))
|
|
|
|
|
|
@router.post("/{vm_id}/ports/sync", response_model=PortSyncResponse)
|
|
def sync_ports(
|
|
vm_id: str,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> PortSyncResponse:
|
|
vm = application.lifecycle.sync_ports(vm_id)
|
|
return PortSyncResponse(
|
|
id=vm.id,
|
|
policy_status=_mutation_policy_status(vm),
|
|
status=vm.status,
|
|
)
|
|
|
|
|
|
@router.get("/{identifier}/logs", response_model=VmLogResponse)
|
|
def get_vm_logs(
|
|
identifier: str,
|
|
tail: int = Query(default=100, ge=0, le=1000),
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmLogResponse:
|
|
log = application.lifecycle.read_log_tail(identifier, tail=tail)
|
|
return VmLogResponse(content=log.content, truncated=log.truncated)
|
|
|
|
|
|
@router.get("/{identifier}", response_model=VmResponse)
|
|
def get_vm(
|
|
identifier: str,
|
|
application: Application = Depends(get_authorized_application),
|
|
) -> VmResponse:
|
|
listed = application.lifecycle.inspect(identifier)
|
|
return vm_response(
|
|
listed.vm,
|
|
observed_status=listed.observed_status,
|
|
policy_status=listed.policy_status,
|
|
)
|