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:
2026-07-18 04:42:11 +03:00
commit f8d3cbdd59
422 changed files with 361335 additions and 0 deletions
+227
View File
@@ -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}