feat: Implement UVM Dashboard with frontend and backend integration
- Added dashboard.js for frontend functionality, including VM management and system health monitoring. - Created dashboard route to serve the dashboard HTML. - Introduced diagnostics route for token-protected host diagnostics. - Enhanced VMs router with bulk actions, VM log retrieval, and port management. - Updated lifecycle service to read VM log tails with truncation handling. - Integrated static file serving for dashboard assets in the FastAPI server. - Redirect root URL to the dashboard for improved user experience.
This commit is contained in:
+129
-10
@@ -2,18 +2,49 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
|
||||
from ..api_models import DestroyResponse, VmCreateRequest, VmResponse, vm_response
|
||||
from ..api_models import (
|
||||
DestroyResponse,
|
||||
PortSyncResponse,
|
||||
PortUpdateRequest,
|
||||
VmActionResultResponse,
|
||||
VmBulkActionRequest,
|
||||
VmCreateRequest,
|
||||
VmLogResponse,
|
||||
VmResponse,
|
||||
vm_response,
|
||||
)
|
||||
from ..app import Application
|
||||
from ..domain import PortRule, VmSpec
|
||||
from ..validation import parse_cpu, parse_name, parse_password, parse_ports, parse_ram, parse_username
|
||||
from ..domain import PortRule, VmRecord, VmSpec
|
||||
from ..validation import (
|
||||
parse_cpu,
|
||||
parse_name,
|
||||
parse_password,
|
||||
parse_ports,
|
||||
parse_ram,
|
||||
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),
|
||||
@@ -47,29 +78,117 @@ def create_vm(
|
||||
ports=ports,
|
||||
)
|
||||
)
|
||||
policy_status = "unmanaged" if vm.ports is None else "unknown"
|
||||
if vm.ports is not None and vm.status == "running" and vm.operation is None:
|
||||
policy_status = "enforced"
|
||||
return vm_response(vm, policy_status=policy_status)
|
||||
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:
|
||||
return vm_response(application.lifecycle.stop(vm_id), policy_status="inactive")
|
||||
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.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)
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user