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,763 @@
|
||||
"""DB-backed Automation API surface state (keyed by verb + path template)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
from app.db.pool import Database
|
||||
from app.vsphere import inventory
|
||||
|
||||
_STATE_SQL = """
|
||||
INSERT INTO vsphere_api_state (state_key, verb, path_template, payload, seed_payload, updated_at)
|
||||
VALUES ($1, $2, $3, $4::jsonb, $4::jsonb, now())
|
||||
ON CONFLICT (state_key) DO UPDATE
|
||||
SET payload = EXCLUDED.payload, updated_at = now()
|
||||
"""
|
||||
|
||||
_SEED_STATE_SQL = """
|
||||
INSERT INTO vsphere_api_state (state_key, verb, path_template, payload, seed_payload, updated_at)
|
||||
VALUES ($1, $2, $3, $4::jsonb, $4::jsonb, now())
|
||||
ON CONFLICT (state_key) DO UPDATE
|
||||
SET payload = EXCLUDED.payload,
|
||||
seed_payload = EXCLUDED.seed_payload,
|
||||
updated_at = now()
|
||||
"""
|
||||
|
||||
|
||||
def state_key(verb: str, path_template: str) -> str:
|
||||
return f"{verb.upper()} {path_template}"
|
||||
|
||||
|
||||
def _pool(database: Database) -> Any:
|
||||
return database.pool # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _decode_json(value: Any) -> Any:
|
||||
"""asyncpg may return jsonb as str depending on codec configuration."""
|
||||
|
||||
current = value
|
||||
while isinstance(current, str):
|
||||
try:
|
||||
current = json.loads(current)
|
||||
except json.JSONDecodeError:
|
||||
break
|
||||
return current
|
||||
|
||||
|
||||
def is_empty_payload(payload: Any) -> bool:
|
||||
if payload is None or payload == "" or payload == {} or payload == []:
|
||||
return True
|
||||
if isinstance(payload, dict):
|
||||
for key in ("data", "value", "messages", "items", "results"):
|
||||
if key in payload and payload[key] in ([], None, {}):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def get_payload(database: Database, verb: str, path_template: str) -> Any | None:
|
||||
pool = _pool(database)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"SELECT payload FROM vsphere_api_state WHERE state_key = $1",
|
||||
state_key(verb, path_template),
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
return _decode_json(row["payload"])
|
||||
|
||||
|
||||
async def get_payload_or_seed(database: Database, verb: str, path_template: str) -> Any | None:
|
||||
"""Return runtime payload, restoring seed_payload from DB when missing/empty."""
|
||||
|
||||
payload = await get_payload(database, verb, path_template)
|
||||
if not is_empty_payload(payload):
|
||||
return payload
|
||||
restored = await restore_seed_payload(database, verb, path_template)
|
||||
if restored is not None:
|
||||
return restored
|
||||
return payload
|
||||
|
||||
|
||||
async def put_payload(database: Database, verb: str, path_template: str, payload: Any) -> None:
|
||||
"""Update runtime payload; preserves existing seed_payload baseline when present."""
|
||||
|
||||
pool = _pool(database)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
_STATE_SQL,
|
||||
state_key(verb, path_template),
|
||||
verb.upper(),
|
||||
path_template,
|
||||
json.dumps(payload),
|
||||
)
|
||||
|
||||
|
||||
async def put_seed_payload(database: Database, verb: str, path_template: str, payload: Any) -> None:
|
||||
"""Write both runtime payload and immutable seed baseline (used only by seed)."""
|
||||
|
||||
pool = _pool(database)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
_SEED_STATE_SQL,
|
||||
state_key(verb, path_template),
|
||||
verb.upper(),
|
||||
path_template,
|
||||
json.dumps(payload),
|
||||
)
|
||||
|
||||
|
||||
async def restore_seed_payload(database: Database, verb: str, path_template: str) -> Any | None:
|
||||
"""Restore payload from DB seed_payload baseline (no Python templates)."""
|
||||
|
||||
pool = _pool(database)
|
||||
key = state_key(verb, path_template)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"SELECT seed_payload FROM vsphere_api_state WHERE state_key = $1",
|
||||
key,
|
||||
)
|
||||
if row is None or row["seed_payload"] is None:
|
||||
return None
|
||||
payload = _decode_json(row["seed_payload"])
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE vsphere_api_state
|
||||
SET payload = seed_payload, updated_at = now()
|
||||
WHERE state_key = $1
|
||||
""",
|
||||
key,
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
async def delete_payload(database: Database, verb: str, path_template: str) -> bool:
|
||||
"""Remove a leaf resource document; collection roots use restore_seed_payload instead."""
|
||||
|
||||
pool = _pool(database)
|
||||
async with pool.acquire() as conn:
|
||||
result = await conn.execute(
|
||||
"DELETE FROM vsphere_api_state WHERE state_key = $1",
|
||||
state_key(verb, path_template),
|
||||
)
|
||||
return result.endswith("1")
|
||||
|
||||
|
||||
async def list_collection(database: Database, path_template: str) -> list[Any]:
|
||||
"""Return GET payload for a collection path; always a list."""
|
||||
|
||||
payload = await get_payload(database, "GET", path_template)
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
if payload is None:
|
||||
return []
|
||||
return [payload]
|
||||
|
||||
|
||||
def _lab_row(leaf: str, path: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"id": f"{leaf}-lab-1",
|
||||
"name": f"{leaf}-lab-1",
|
||||
"status": "ENABLED",
|
||||
"path": path,
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def ensure_nonempty(payload: Any, path: str) -> Any:
|
||||
"""Guarantee lab payloads are never empty lists/objects/null (incl. nested data)."""
|
||||
|
||||
if payload is None or payload == "" or payload == {} or payload == []:
|
||||
return default_for_get_path(path)
|
||||
if isinstance(payload, dict) and len(payload) == 0:
|
||||
return default_for_get_path(path)
|
||||
if isinstance(payload, list) and len(payload) == 0:
|
||||
return default_for_get_path(path)
|
||||
if isinstance(payload, dict):
|
||||
# Fill known nested empties that clients treat as "no data".
|
||||
out = dict(payload)
|
||||
changed = False
|
||||
for key in ("data", "value", "messages", "items", "results"):
|
||||
if key in out and out[key] in ([], None, {}):
|
||||
filled = default_for_get_path(path)
|
||||
if (
|
||||
key == "messages"
|
||||
and isinstance(filled, dict)
|
||||
and isinstance(filled.get("messages"), list)
|
||||
):
|
||||
out[key] = filled["messages"]
|
||||
elif isinstance(filled, list):
|
||||
out[key] = filled
|
||||
elif key in {"data", "value"}:
|
||||
# Prefer wrapping a realistic list when the key is a collection.
|
||||
out[key] = filled
|
||||
if out[key] in ([], None, {}):
|
||||
out[key] = [{"id": "lab-1", "name": "lab", "path": path}]
|
||||
else:
|
||||
out[key] = [{"id": "lab-ok", "default_message": "Healthy", "args": []}]
|
||||
changed = True
|
||||
if changed:
|
||||
return out
|
||||
return payload
|
||||
|
||||
|
||||
def default_for_get_path(path: str) -> Any:
|
||||
"""Realistic lab defaults — never returns empty list/object/null."""
|
||||
|
||||
leaf = path.rstrip("/").rsplit("/", 1)[-1]
|
||||
if leaf.startswith("{") and leaf.endswith("}"):
|
||||
name = leaf[1:-1]
|
||||
return {
|
||||
"id": f"lab-{name}",
|
||||
"name": f"lab-{name}",
|
||||
"path": path,
|
||||
"status": "ENABLED",
|
||||
}
|
||||
|
||||
if "/appliance/access/ssh" in path:
|
||||
return {"enabled": True}
|
||||
if "/appliance/access/dcui" in path:
|
||||
return {"enabled": True}
|
||||
if "/appliance/access/consolecli" in path:
|
||||
return {"enabled": True}
|
||||
if "/appliance/access/shell" in path:
|
||||
return {"enabled": True, "timeout": 300}
|
||||
if path.endswith("/dns/hostname"):
|
||||
return {"name": "vcenter.lab.local"}
|
||||
if path.endswith("/dns/servers"):
|
||||
return {"mode": "DHCP", "servers": ["8.8.8.8", "1.1.1.1"]}
|
||||
if path.endswith("/dns/domains"):
|
||||
return ["lab.local", "vsphere.local"]
|
||||
if "/appliance/timesync" in path:
|
||||
return {"mode": "NTP", "servers": ["time.lab.local"]}
|
||||
if "/appliance/networking" in path:
|
||||
return {
|
||||
"hostname": "vcenter.lab.local",
|
||||
"node_name": "vcenter.lab.local",
|
||||
"default_gateway": "192.168.1.1",
|
||||
"dns": {"mode": "DHCP", "servers": ["8.8.8.8"], "domains": ["lab.local"]},
|
||||
"interfaces": [
|
||||
{"name": "nic0", "status": "up", "ipv4": {"address": "192.168.1.50", "prefix": 24}}
|
||||
],
|
||||
}
|
||||
if "/appliance/health" in path:
|
||||
return {"status": "green", "messages": [{"id": "ok", "default_message": "Healthy"}]}
|
||||
if "/appliance/update" in path:
|
||||
return {"state": "UP_TO_DATE", "version": "8.0.2"}
|
||||
if "/appliance/recovery" in path:
|
||||
return {"status": "IDLE", "parts": [{"part": "VCSA", "status": "OK"}]}
|
||||
if "/appliance/system/storage" in path:
|
||||
return [{"disk": "sda", "capacity": 100000000000, "used": 40000000000}]
|
||||
if "/appliance/system/time" in path:
|
||||
return {"seconds_since_epoch": 1767225600, "datetime": "2026-01-01T00:00:00.000Z"}
|
||||
if "/certificate-management" in path:
|
||||
return {
|
||||
"cert": "-----BEGIN CERTIFICATE-----\nMIIBstub\n-----END CERTIFICATE-----",
|
||||
"valid_from": "2026-01-01T00:00:00.000Z",
|
||||
"valid_to": "2028-01-01T00:00:00.000Z",
|
||||
}
|
||||
if "/identity/providers" in path:
|
||||
return [{"provider": "vsphere.local", "name": "vsphere.local", "type_id": "LocalOS"}]
|
||||
if "/crypto-manager" in path or "/crypto/" in path:
|
||||
return [{"provider": "native-kms", "type": "NATIVE", "status": "READY"}]
|
||||
if "/activity-history" in path:
|
||||
return [
|
||||
{
|
||||
"activity": "activity-lab-1",
|
||||
"description": "Seed inventory",
|
||||
"status": "SUCCEEDED",
|
||||
"start_time": "2026-01-01T00:00:00.000Z",
|
||||
"user": "administrator@vsphere.local",
|
||||
}
|
||||
]
|
||||
if "/namespace-management/virtual-machine-classes" in path:
|
||||
return [
|
||||
{
|
||||
"id": "best-effort-small",
|
||||
"cpu_count": 2,
|
||||
"memory_mb": 2048,
|
||||
"description": "Lab small class",
|
||||
},
|
||||
{
|
||||
"id": "guaranteed-large",
|
||||
"cpu_count": 8,
|
||||
"memory_mb": 16384,
|
||||
"description": "Lab large class",
|
||||
},
|
||||
]
|
||||
if "/namespace-management/supervisors" in path or "/namespaces/" in path:
|
||||
if "{" in path:
|
||||
return {
|
||||
"supervisor": "supervisor-1",
|
||||
"name": "supervisor-lab",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
}
|
||||
return [
|
||||
{
|
||||
"supervisor": "supervisor-1",
|
||||
"name": "supervisor-lab",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
}
|
||||
]
|
||||
if "/namespace-management" in path:
|
||||
return [
|
||||
{
|
||||
"cluster": "domain-c21",
|
||||
"cluster_name": "Cluster",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
}
|
||||
]
|
||||
if "/esx/settings" in path:
|
||||
if "{" in path and not path.endswith("}"):
|
||||
return {
|
||||
"status": "COMPLIANT",
|
||||
"software_info": {
|
||||
"base_image": {"version": "8.0.2-0.0"},
|
||||
"components": [{"name": "VMware-VMTools", "version": "12.0"}],
|
||||
},
|
||||
}
|
||||
return [{"cluster": "domain-c21", "status": "COMPLIANT", "commit": "commit-lab-1"}]
|
||||
if "/trusted-infrastructure" in path or "/trustedinfrastructure" in path:
|
||||
return [{"cluster": "domain-c21", "state": "ENABLED", "attestation": "READY"}]
|
||||
if "/services/service" in path or path.endswith("/services"):
|
||||
return [
|
||||
{"service": "vsphere-ui", "state": "STARTED", "description": "vSphere Client"},
|
||||
{"service": "vpxd", "state": "STARTED", "description": "vCenter Server"},
|
||||
{"service": "vapi-endpoint", "state": "STARTED", "description": "vAPI Endpoint"},
|
||||
]
|
||||
if "/storage/policies" in path:
|
||||
return [
|
||||
{
|
||||
"policy": "policy-default",
|
||||
"name": "vSAN Default Storage Policy",
|
||||
"description": "Lab default",
|
||||
},
|
||||
{"policy": "policy-thin", "name": "Thin provision", "description": "Thin disks"},
|
||||
]
|
||||
if "/guest/customization-specs" in path or "/guest/customization" in path:
|
||||
return [
|
||||
{"name": "linux-lab", "description": "Linux cloud-init lab spec", "os_type": "LINUX"}
|
||||
]
|
||||
if "/vcha" in path:
|
||||
return {"mode": "DISABLED", "cluster_mode": "DISABLED"}
|
||||
if "/content/registries/health" in path:
|
||||
return [{"registry": "harbor-lab-1", "status": "HEALTHY", "details": "ok"}]
|
||||
if "/content/registries/harbor" in path:
|
||||
return [{"registry": "harbor-lab-1", "name": "harbor-lab", "version": "2.9"}]
|
||||
if "/content/security-policies" in path:
|
||||
return [{"policy": "sec-policy-lab-1", "name": "Default security", "status": "ENABLED"}]
|
||||
if "/content/trusted-certificates" in path:
|
||||
return [
|
||||
{
|
||||
"certificate": "cert-lab-1",
|
||||
"name": "lab-ca",
|
||||
"valid_to": "2028-01-01T00:00:00.000Z",
|
||||
}
|
||||
]
|
||||
if path.endswith("/content/type") or path.endswith("/content/types"):
|
||||
return [
|
||||
{"type": "ovf", "description": "OVF template"},
|
||||
{"type": "iso", "description": "ISO image"},
|
||||
{"type": "vm-template", "description": "VM template"},
|
||||
]
|
||||
if path.rstrip("/") == "/api/content/library":
|
||||
return ["lib-local-1", "lib-published-1"]
|
||||
if path.rstrip("/") == "/api/content/library/item":
|
||||
return ["item-ubuntu", "item-centos"]
|
||||
if "/content/" in path:
|
||||
if "download-session" in path or "update-session" in path:
|
||||
if path.endswith("/file"):
|
||||
return [
|
||||
{
|
||||
"name": "descriptor.ovf",
|
||||
"size": 256,
|
||||
"status": "READY",
|
||||
"download_endpoint": {
|
||||
"uri": "/api/content/library/item/download-session/session-lab-1/file/descriptor.ovf"
|
||||
},
|
||||
}
|
||||
]
|
||||
return {
|
||||
"id": "session-lab-1",
|
||||
"library_item_id": "item-ubuntu",
|
||||
"state": "ACTIVE",
|
||||
"name": "lab-session",
|
||||
}
|
||||
if leaf in {"file", "changes", "storage"}:
|
||||
return [
|
||||
{
|
||||
"name": "descriptor.ovf",
|
||||
"size": 4096,
|
||||
"checksum_info": {"algorithm": "SHA256", "checksum": "lab"},
|
||||
"storage_uris": ["ds:///vmfs/volumes/datastore-31/content/descriptor.ovf"],
|
||||
"version": "1",
|
||||
"cached": True,
|
||||
}
|
||||
]
|
||||
if leaf in {"item", "library", "local-library", "subscribed-library"}:
|
||||
return [_lab_row(leaf, path, type="LOCAL")]
|
||||
return [_lab_row(leaf or "content", path)]
|
||||
if "/cis/tagging/category" in path and "{" not in path:
|
||||
return ["cat-lab-1", "urn:vmomi:InventoryServiceCategory:environment:GLOBAL"]
|
||||
if "/cis/tagging/tag" in path and "{" not in path and "association" not in path:
|
||||
return ["tag-lab-1", "urn:vmomi:InventoryServiceTag:prod:GLOBAL"]
|
||||
if "/cis/tagging" in path:
|
||||
return [_lab_row("tagging", path, category_id="cat-lab-1")]
|
||||
if "/vcenter/vm/" in path and "/hardware/adapter/nvme" in path:
|
||||
return [{"adapter": "19000", "bus": 0, "pci_slot_number": 160}]
|
||||
if "/vcenter/vm/" in path and "/hardware/adapter/sata" in path:
|
||||
return [{"adapter": "15000", "bus": 0, "pci_slot_number": 33}]
|
||||
if "/vcenter/vm/" in path and "/hardware/parallel" in path:
|
||||
return [{"port": "10000", "yield_on_poll": True}]
|
||||
if "/vcenter/vm/" in path and "/hardware/" in path:
|
||||
return [_lab_row(leaf or "device", path, key="2000")]
|
||||
if "/vcenter/vm/" in path and "/guest/" in path:
|
||||
return {
|
||||
"family": "LINUX",
|
||||
"full_name": {"name": "Ubuntu Linux (64-bit)"},
|
||||
"host_name": "lab-guest",
|
||||
"ip_address": "192.168.1.100",
|
||||
}
|
||||
if "/vcenter/host/" in path:
|
||||
return {"connection_state": "CONNECTED", "power_state": "POWERED_ON", "status": "green"}
|
||||
# VM power state is served by GET /api/vcenter/vm/{vm}/power (live inventory).
|
||||
if "/stats/" in path or "/metrics" in path:
|
||||
return {
|
||||
"interval": "PT5M",
|
||||
"data_points": [{"time": "2026-01-01T00:00:00.000Z", "value": 1.0}],
|
||||
}
|
||||
if "/vapi/metadata/authentication" in path:
|
||||
return [
|
||||
"com.vmware.cis.session",
|
||||
"com.vmware.vcenter",
|
||||
"com.vmware.appliance",
|
||||
"com.vmware.content",
|
||||
]
|
||||
if "/vapi/metadata" in path:
|
||||
return [
|
||||
"com.vmware.vcenter",
|
||||
"com.vmware.appliance",
|
||||
"com.vmware.cis",
|
||||
"com.vmware.content",
|
||||
]
|
||||
|
||||
if any(token in leaf for token in ("list",)) or (
|
||||
"{" not in path and leaf not in {"version", "system", "networking", "timesync"}
|
||||
):
|
||||
if path.count("{") == 0 and leaf not in {
|
||||
"ssh",
|
||||
"dcui",
|
||||
"shell",
|
||||
"consolecli",
|
||||
"timesync",
|
||||
"version",
|
||||
"networking",
|
||||
"system",
|
||||
"tls",
|
||||
"evc-mode",
|
||||
"hostname",
|
||||
"servers",
|
||||
"domains",
|
||||
}:
|
||||
return [_lab_row(leaf, path)]
|
||||
return {
|
||||
"id": f"lab-{leaf}",
|
||||
"name": f"lab-{leaf}",
|
||||
"status": "ENABLED",
|
||||
"path": path,
|
||||
"value": True,
|
||||
}
|
||||
|
||||
|
||||
async def seed_api_surface(database: Database) -> dict[str, int]:
|
||||
"""Populate vsphere_api_state for every GET route in the Broadcom universe + lab extras.
|
||||
|
||||
Templates in ``default_for_get_path`` are used ONLY here at seed time — request handlers
|
||||
must read ``vsphere_api_state`` / domain tables without inventing payloads.
|
||||
"""
|
||||
|
||||
from app.vsphere.rest.coverage import IMPLEMENTED
|
||||
from app.vsphere.security.authz import PRIVILEGES, ROLE_PRIVILEGES
|
||||
|
||||
hosts = await inventory.list_objects(database, type_name="HostSystem")
|
||||
clusters = await inventory.list_objects(database, type_name="ClusterComputeResource")
|
||||
vms = await inventory.list_objects(database, type_name="VirtualMachine")
|
||||
datastores = await inventory.list_objects(database, type_name="Datastore")
|
||||
|
||||
host_ids = [h.moid for h in hosts] or ["host-11"]
|
||||
cluster_ids = [c.moid for c in clusters] or ["domain-c21"]
|
||||
vm_ids = [v.moid for v in vms[:50]] or ["vm-101"]
|
||||
ds_ids = [d.moid for d in datastores] or ["datastore-31"]
|
||||
|
||||
# Inventory-derived collections overwrite generic defaults.
|
||||
extras: dict[tuple[str, str], Any] = {
|
||||
("GET", "/api/vcenter/privilege"): [
|
||||
{"id": key, "name": name, "description": name}
|
||||
for key, name in sorted(PRIVILEGES.items())
|
||||
],
|
||||
("GET", "/api/vcenter/authorization/roles"): [
|
||||
{"role": role, "privileges": sorted(privs)}
|
||||
for role, privs in sorted(ROLE_PRIVILEGES.items())
|
||||
],
|
||||
("GET", "/api/appliance/health/system"): {
|
||||
"status": "green",
|
||||
"value": "green",
|
||||
"messages": [{"id": "ok", "default_message": "Healthy", "args": []}],
|
||||
},
|
||||
("GET", "/api/appliance/system/version"): {
|
||||
"version": "8.0.2",
|
||||
"product": "VMware vCenter Server",
|
||||
"type": "vCenter Server",
|
||||
"build": "simulator",
|
||||
"install_time": "2026-01-01T00:00:00.000Z",
|
||||
"releasedate": "2026-01-01",
|
||||
"summary": "VMware API Simulator",
|
||||
},
|
||||
("GET", "/api/vcenter/certificate-management/vcenter/tls-csr"): {
|
||||
"csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBLabCSR\n-----END CERTIFICATE REQUEST-----",
|
||||
"status": "AVAILABLE",
|
||||
"subject_dn": "CN=vcenter.lab.local",
|
||||
},
|
||||
("GET", "/api/vcenter/certificate-management/vcenter/trusted-root-chains"): [
|
||||
{
|
||||
"chain": "chain-lab-1",
|
||||
"cert_chain": ["-----BEGIN CERTIFICATE-----\nMIIBRoot\n-----END CERTIFICATE-----"],
|
||||
"thumbprint": "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD",
|
||||
}
|
||||
],
|
||||
("GET", "/api/vapi/metadata/metamodel/service"): [
|
||||
"com.vmware.vcenter.vm",
|
||||
"com.vmware.vcenter.host",
|
||||
"com.vmware.cis.tagging.category",
|
||||
"com.vmware.cis.tagging.tag",
|
||||
"com.vmware.content.library",
|
||||
],
|
||||
("GET", "/api/vapi/metadata/authentication/component"): [
|
||||
"com.vmware.cis.session",
|
||||
"com.vmware.vcenter",
|
||||
"com.vmware.appliance",
|
||||
"com.vmware.content",
|
||||
],
|
||||
("GET", "/api/vcenter/activity-history"): [
|
||||
{
|
||||
"activity": "activity-lab-1",
|
||||
"description": "Seed inventory",
|
||||
"status": "SUCCEEDED",
|
||||
"start_time": "2026-01-01T00:00:00.000Z",
|
||||
"user": "administrator@vsphere.local",
|
||||
},
|
||||
{
|
||||
"activity": "activity-lab-2",
|
||||
"description": "Appliance networking update",
|
||||
"status": "SUCCEEDED",
|
||||
"start_time": "2026-01-01T00:05:00.000Z",
|
||||
"user": "administrator@vsphere.local",
|
||||
},
|
||||
],
|
||||
("GET", "/api/vcenter/services"): [
|
||||
{"service": "vsphere-ui", "state": "STARTED"},
|
||||
{"service": "vpxd", "state": "STARTED"},
|
||||
{"service": "vapi-endpoint", "state": "STARTED"},
|
||||
{"service": "rhttpproxy", "state": "STARTED"},
|
||||
],
|
||||
("GET", "/api/vcenter/namespace-management/clusters"): [
|
||||
{
|
||||
"cluster": cid,
|
||||
"cluster_name": "Cluster",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
}
|
||||
for cid in cluster_ids
|
||||
],
|
||||
("GET", "/api/vcenter/namespace-management/supervisors/{supervisor}/summary"): {
|
||||
"supervisor": "supervisor-1",
|
||||
"name": "supervisor-lab",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
"status": "ENABLED",
|
||||
"clusters": cluster_ids,
|
||||
},
|
||||
("GET", "/api/vcenter/namespace-management/supervisor-services"): [
|
||||
{"supervisor_service": "service-lab-1", "name": "demo-operator", "state": "ACTIVATED"}
|
||||
],
|
||||
("GET", "/api/esx/settings/clusters/{cluster}/software"): {
|
||||
"base_image": {"version": "8.0.2-0.0"},
|
||||
"components": {},
|
||||
"commit": "commit-lab-1",
|
||||
"status": "COMPLIANT",
|
||||
"clusters": cluster_ids,
|
||||
},
|
||||
("GET", "/api/esx/settings/hosts/{host}/software"): {
|
||||
"base_image": {"version": "8.0.2-0.0"},
|
||||
"status": "COMPLIANT",
|
||||
"hosts": host_ids[:20],
|
||||
},
|
||||
("GET", "/api/vcenter/crypto-manager/kms/providers"): [
|
||||
{"provider": "native-kms", "type": "NATIVE", "status": "READY", "health": "OK"}
|
||||
],
|
||||
("GET", "/api/vcenter/trusted-infrastructure/trust-authority-clusters"): [
|
||||
{"cluster": cid, "state": "ENABLED"} for cid in cluster_ids
|
||||
],
|
||||
("GET", "/api/vcenter/storage/policies"): [
|
||||
{"policy": "policy-default", "name": "vSAN Default Storage Policy"},
|
||||
{"policy": "policy-thin", "name": "Thin provision"},
|
||||
],
|
||||
("GET", "/api/vcenter/guest/customization-specs"): [
|
||||
{"name": "linux-lab", "description": "Linux lab", "os_type": "LINUX"},
|
||||
{"name": "windows-lab", "description": "Windows lab", "os_type": "WINDOWS"},
|
||||
],
|
||||
("GET", "/api/appliance/access/ssh"): {"enabled": True},
|
||||
("GET", "/api/appliance/access/dcui"): {"enabled": True},
|
||||
("GET", "/api/appliance/access/shell"): {"enabled": True, "timeout": 300},
|
||||
("GET", "/api/appliance/access/consolecli"): {"enabled": True},
|
||||
("GET", "/api/appliance/services"): [
|
||||
{"service": "vsphere-ui", "state": "STARTED", "description": "vSphere Client"},
|
||||
{"service": "vpxd", "state": "STARTED", "description": "vCenter Server"},
|
||||
{"service": "vapi-endpoint", "state": "STARTED", "description": "vAPI Endpoint"},
|
||||
],
|
||||
("GET", "/api/vcenter/identity/providers"): [
|
||||
{"provider": "vsphere.local", "name": "vsphere.local", "type_id": "LocalOS"}
|
||||
],
|
||||
("GET", "/api/vcenter/certificate-management/vcenter/tls"): {
|
||||
"cert": "-----BEGIN CERTIFICATE-----\nMIIBlab\n-----END CERTIFICATE-----",
|
||||
"valid_from": "2026-01-01T00:00:00.000Z",
|
||||
"valid_to": "2028-01-01T00:00:00.000Z",
|
||||
"subject_dn": "CN=vcenter.lab.local",
|
||||
},
|
||||
}
|
||||
# Drop Nones
|
||||
extras = {k: v for k, v in extras.items() if v is not None}
|
||||
|
||||
inserted = 0
|
||||
pool = _pool(database)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("DELETE FROM vsphere_api_state")
|
||||
batch: list[tuple[str, str, str, str]] = []
|
||||
seen: set[str] = set()
|
||||
for (verb, path), _status in IMPLEMENTED.items():
|
||||
if verb != "GET" or not path.startswith("/api/"):
|
||||
continue
|
||||
payload = extras.get((verb, path))
|
||||
if payload is None:
|
||||
payload = default_for_get_path(path)
|
||||
payload = ensure_nonempty(payload, path)
|
||||
if path in {
|
||||
"/api/vcenter/namespace-management/clusters",
|
||||
"/api/vcenter/namespace-management/clusters/{cluster}",
|
||||
}:
|
||||
payload = [
|
||||
{
|
||||
"cluster": cid,
|
||||
"cluster_name": "Cluster",
|
||||
"config_status": "RUNNING",
|
||||
"status": "COMPLIANT",
|
||||
"kubernetes_status": "READY",
|
||||
}
|
||||
for cid in cluster_ids
|
||||
]
|
||||
if path in {
|
||||
"/api/esx/settings/clusters/{cluster}/software",
|
||||
"/api/esx/settings/clusters/software",
|
||||
}:
|
||||
payload = {
|
||||
"base_image": {"version": "8.0.2-0.0"},
|
||||
"components": {},
|
||||
"commit": "commit-lab-1",
|
||||
"status": "COMPLIANT",
|
||||
"clusters": cluster_ids,
|
||||
}
|
||||
if path == "/api/vcenter/namespace-management/supervisors/{supervisor}/summary":
|
||||
payload = {
|
||||
"supervisor": "supervisor-1",
|
||||
"name": "supervisor-lab",
|
||||
"config_status": "RUNNING",
|
||||
"kubernetes_status": "READY",
|
||||
"status": "ENABLED",
|
||||
"clusters": cluster_ids,
|
||||
}
|
||||
key = state_key(verb, path)
|
||||
batch.append((key, verb, path, json.dumps(payload)))
|
||||
seen.add(key)
|
||||
inserted += 1
|
||||
|
||||
for (verb, path), payload in extras.items():
|
||||
key = state_key(verb, path)
|
||||
if key in seen:
|
||||
continue
|
||||
batch.append((key, verb, path, json.dumps(payload)))
|
||||
seen.add(key)
|
||||
inserted += 1
|
||||
|
||||
# Template hardware fallbacks (live inventory overrides when VM exists).
|
||||
hardware_seed = {
|
||||
"/api/vcenter/vm/{vm}/hardware/cdrom": [
|
||||
{
|
||||
"cdrom": "3000",
|
||||
"label": "CD/DVD drive 1",
|
||||
"state": "CONNECTED",
|
||||
"backing": {"type": "ISO_FILE", "iso_file": "[datastore1] ISO/ubuntu.iso"},
|
||||
}
|
||||
],
|
||||
"/api/vcenter/vm/{vm}/hardware/floppy": [{"floppy": "8000", "state": "NOT_CONNECTED"}],
|
||||
"/api/vcenter/vm/{vm}/hardware/serial": [{"port": "9000", "yield_on_poll": True}],
|
||||
"/api/vcenter/vm/{vm}/hardware/adapter/scsi": [
|
||||
{"adapter": "1000", "type": "LSILOGIC", "sharing": "NONE", "pci_slot_number": 16}
|
||||
],
|
||||
"/api/vcenter/vm/{vm}/hardware/adapter/sata": [
|
||||
{"adapter": "15000", "bus": 0, "pci_slot_number": 33}
|
||||
],
|
||||
"/api/vcenter/vm/{vm}/hardware/adapter/nvme": [
|
||||
{"adapter": "19000", "bus": 0, "pci_slot_number": 160}
|
||||
],
|
||||
"/api/vcenter/vm/{vm}/hardware/parallel": [{"port": "10000", "yield_on_poll": True}],
|
||||
"/api/vcenter/vm/{vm}/hardware/boot": {
|
||||
"type": "BIOS",
|
||||
"delay": 0,
|
||||
"retry": False,
|
||||
"retry_delay": 10000,
|
||||
"enter_setup_mode": False,
|
||||
},
|
||||
"/api/vcenter/vm/{vm}/hardware/boot/device": [
|
||||
{"type": "CDROM"},
|
||||
{"type": "DISK"},
|
||||
{"type": "ETHERNET"},
|
||||
],
|
||||
"/api/vcenter/vm/{vm}/guest/local-filesystem": {
|
||||
"filesystems": {"/": {"capacity": 21474836480, "free_space": 10737418240}}
|
||||
},
|
||||
"/api/vcenter/host/{host}/storage/storage-device": [
|
||||
{
|
||||
"device": "naa.lab1",
|
||||
"display_name": "Local Disk",
|
||||
"capacity": 1099511627776,
|
||||
"ssd": False,
|
||||
}
|
||||
],
|
||||
"/api/vcenter/host/{host}/networking": {
|
||||
"dns": {"servers": ["8.8.8.8"], "domains": ["lab.local"]},
|
||||
"routing": {"default_gateway": "192.168.1.1"},
|
||||
},
|
||||
"/api/vcenter/storage/policies/{policy}/vm": [
|
||||
{"vm": vid, "vm_home": True, "disks": []} for vid in vm_ids[:10]
|
||||
],
|
||||
}
|
||||
for path, payload in hardware_seed.items():
|
||||
key = state_key("GET", path)
|
||||
if key in seen:
|
||||
continue
|
||||
batch.append((key, "GET", path, json.dumps(payload)))
|
||||
seen.add(key)
|
||||
inserted += 1
|
||||
|
||||
await conn.executemany(_SEED_STATE_SQL, batch)
|
||||
|
||||
_ = ds_ids
|
||||
return {"api_state_rows": inserted, "hosts": len(host_ids), "vms_sampled": len(vm_ids)}
|
||||
|
||||
|
||||
async def new_id(prefix: str = "id") -> str:
|
||||
return f"{prefix}-{secrets.token_hex(4)}"
|
||||
Reference in New Issue
Block a user