"""VM hardware, snapshots, clone, tools — REST extensions.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Query, Response from app.db.pool import Database from app.dependencies import get_database from app.vsphere.domain import vm_ops from app.vsphere.errors import invalid_argument from app.vsphere.rest import mappers from app.vsphere.security.authz import require_privilege, require_read from app.vsphere.security.session import SessionInfo router = APIRouter(tags=["vSphere VM Ext"]) @router.get("/api/vcenter/vm/{vm}/tools") async def vm_tools( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> dict[str, Any]: obj = await vm_ops.require_vm(database, vm) tools = obj.props.get("tools") if isinstance(tools, dict) and tools: out = dict(tools) out["run_state"] = ( "RUNNING" if obj.props.get("power_state") == "POWERED_ON" else "NOT_RUNNING" ) return out return {} @router.get("/api/vcenter/vm/{vm}/hardware") async def vm_hardware( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> dict[str, Any]: obj = await vm_ops.require_vm(database, vm) return mappers.vm_info(obj)["hardware"] @router.get("/api/vcenter/vm/{vm}/hardware/cpu") async def get_cpu( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> dict[str, Any]: obj = await vm_ops.require_vm(database, vm) cpu = dict(obj.props.get("cpu") or {}) return { "count": obj.props.get("cpu_count") if obj.props.get("cpu_count") is not None else cpu.get("count"), "cores_per_socket": cpu.get("cores_per_socket"), "hot_add_enabled": cpu.get("hot_add_enabled"), } @router.patch("/api/vcenter/vm/{vm}/hardware/cpu") async def patch_cpu( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Config.CPUCount")), ) -> Response: await vm_ops.update_hardware_cpu(database, vm, int(body.get("count") or 1)) return Response(status_code=204) @router.get("/api/vcenter/vm/{vm}/hardware/memory") async def get_memory( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> dict[str, Any]: obj = await vm_ops.require_vm(database, vm) memory = dict(obj.props.get("memory") or {}) return { "size_MiB": obj.props.get("memory_size_mib") if obj.props.get("memory_size_mib") is not None else memory.get("size_MiB"), "hot_add_enabled": memory.get("hot_add_enabled"), } @router.patch("/api/vcenter/vm/{vm}/hardware/memory") async def patch_memory( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Config.Memory")), ) -> Response: await vm_ops.update_hardware_memory( database, vm, int(body.get("size_MiB") or body.get("size_mib") or 1024) ) return Response(status_code=204) @router.get("/api/vcenter/vm/{vm}/hardware/disk") async def list_disks( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> list[dict[str, Any]]: obj = await vm_ops.require_vm(database, vm) return list(obj.props.get("disks") or []) @router.post("/api/vcenter/vm/{vm}/hardware/disk") async def create_disk( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Config.AddNewDisk")), ) -> str: capacity = int( (body.get("new_vmdk") or {}).get("capacity") or body.get("capacity") or 10737418240 ) disk = await vm_ops.add_disk(database, vm, capacity) return str(disk["key"]) @router.get("/api/vcenter/vm/{vm}/hardware/ethernet") async def list_nics( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> list[dict[str, Any]]: obj = await vm_ops.require_vm(database, vm) return list(obj.props.get("nics") or []) @router.post("/api/vcenter/vm/{vm}/hardware/ethernet") async def create_nic( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Config.AddRemoveDevice")), ) -> str: network = ((body.get("backing") or {}).get("network")) or body.get("network") or "network-41" nic = await vm_ops.add_nic(database, vm, str(network)) return str(nic["key"]) @router.get("/api/vcenter/vm/{vm}/hardware/boot") async def get_boot( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> dict[str, Any]: obj = await vm_ops.require_vm(database, vm) boot = obj.props.get("boot") if isinstance(boot, dict) and boot: return boot from app.vsphere.domain import api_state payload = await api_state.get_payload(database, "GET", "/api/vcenter/vm/{vm}/hardware/boot") return payload if isinstance(payload, dict) else {} @router.get("/api/vcenter/vm/{vm}/snapshots") async def list_snapshots( vm: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read) ) -> list[dict[str, Any]]: return await vm_ops.list_snapshots(database, vm) @router.post("/api/vcenter/vm/{vm}/snapshots") async def create_snapshot( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.State.CreateSnapshot")), ) -> dict[str, str]: snap_id, task_id = await vm_ops.create_snapshot( database, vm, name=str(body.get("name") or "snapshot"), description=str(body.get("description") or ""), memory=bool(body.get("memory")), ) return {"snapshot": snap_id, "task": task_id} @router.delete("/api/vcenter/vm/{vm}/snapshots/{snapshot}") async def delete_snapshot( vm: str, snapshot: str, database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.State.RemoveSnapshot")), ) -> dict[str, str]: task_id = await vm_ops.delete_snapshot(database, vm, snapshot) return {"task": task_id} @router.post("/api/vcenter/vm/{vm}/snapshots/{snapshot}") async def revert_snapshot( vm: str, snapshot: str, action: str = Query("revert"), database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.State.RevertToSnapshot")), ) -> dict[str, str]: if action != "revert": raise invalid_argument(f"unsupported action {action}") task_id = await vm_ops.revert_snapshot(database, vm, snapshot) return {"task": task_id} @router.post("/api/vcenter/vm/{vm}/clone") async def clone_vm( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Provisioning.Clone")), ) -> dict[str, str]: name = str(body.get("name") or f"{vm}-clone") folder = (body.get("placement") or {}).get("folder") moid, task_id = await vm_ops.clone_vm( database, source_vm=vm, name=name, folder=folder, power_on=bool(body.get("power_on")) ) return {"vm": moid, "task": task_id} @router.post("/api/vcenter/vm/{vm}/relocate") async def relocate_vm( vm: str, body: dict[str, Any], database: Database = Depends(get_database), _: SessionInfo = Depends(require_privilege("VirtualMachine.Inventory.Move")), ) -> dict[str, str]: placement = body.get("placement") or body task_id = await vm_ops.relocate_vm( database, vm, host=placement.get("host"), datastore=placement.get("datastore") ) return {"task": task_id}