f8d3cbdd59
Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API contracts, docs, client examples, and the unit/integration/compatibility test suite for local client and tooling labs without a real vCenter.
228 lines
7.7 KiB
Python
228 lines
7.7 KiB
Python
"""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}
|