Initial commit: VMware vSphere API simulator scaffold.
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.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user