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,468 @@
|
||||
"""Declarative vSphere inventory profiles (small lab / large cluster)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObjectSpec:
|
||||
moid: str
|
||||
type: str
|
||||
name: str
|
||||
parent_moid: str | None
|
||||
props: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CredentialSpec:
|
||||
username: str
|
||||
password: str
|
||||
roles: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PermissionSpec:
|
||||
principal: str
|
||||
role: str
|
||||
entity_moid: str | None
|
||||
propagate: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VsphereSeedProfile:
|
||||
name: str
|
||||
objects: tuple[ObjectSpec, ...]
|
||||
credentials: tuple[CredentialSpec, ...]
|
||||
permissions: tuple[PermissionSpec, ...]
|
||||
host_count: int
|
||||
vm_count: int
|
||||
|
||||
|
||||
POWER_CYCLE = ("POWERED_ON", "POWERED_ON", "POWERED_ON", "POWERED_OFF", "SUSPENDED")
|
||||
GUEST_OS = ("UBUNTU_64", "CENTOS_7_64", "WINDOWS_9_64", "RHEL_8_64", "OTHER_GUEST_64")
|
||||
ROLE_PREFIX = (
|
||||
"web",
|
||||
"app",
|
||||
"db",
|
||||
"cache",
|
||||
"batch",
|
||||
"jump",
|
||||
"ci",
|
||||
"mon",
|
||||
"log",
|
||||
"ml",
|
||||
)
|
||||
|
||||
|
||||
def _topology(
|
||||
*,
|
||||
host_count: int,
|
||||
datastore_count: int = 4,
|
||||
network_count: int = 3,
|
||||
) -> list[ObjectSpec]:
|
||||
specs: list[ObjectSpec] = [
|
||||
ObjectSpec("group-d1", "Folder", "Datacenters", None, {"folder_type": "DATACENTER"}),
|
||||
ObjectSpec(
|
||||
"datacenter-21",
|
||||
"Datacenter",
|
||||
"Datacenter",
|
||||
"group-d1",
|
||||
{
|
||||
"datastore_folder": "group-s23",
|
||||
"host_folder": "group-h23",
|
||||
"vm_folder": "group-v23",
|
||||
"network_folder": "group-n23",
|
||||
},
|
||||
),
|
||||
ObjectSpec("group-h23", "Folder", "host", "datacenter-21", {"folder_type": "HOST"}),
|
||||
ObjectSpec(
|
||||
"group-v23", "Folder", "vm", "datacenter-21", {"folder_type": "VIRTUAL_MACHINE"}
|
||||
),
|
||||
ObjectSpec(
|
||||
"group-s23", "Folder", "datastore", "datacenter-21", {"folder_type": "DATASTORE"}
|
||||
),
|
||||
ObjectSpec("group-n23", "Folder", "network", "datacenter-21", {"folder_type": "NETWORK"}),
|
||||
ObjectSpec(
|
||||
"domain-c21",
|
||||
"ClusterComputeResource",
|
||||
"Cluster",
|
||||
"group-h23",
|
||||
{"drs_enabled": True, "ha_enabled": True, "resource_pool": "resgroup-22"},
|
||||
),
|
||||
ObjectSpec(
|
||||
"resgroup-22",
|
||||
"ResourcePool",
|
||||
"Resources",
|
||||
"domain-c21",
|
||||
{"cpu_limit_mhz": -1, "memory_limit_mib": -1},
|
||||
),
|
||||
# Workload folders for realism
|
||||
ObjectSpec(
|
||||
"group-v100", "Folder", "production", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
|
||||
),
|
||||
ObjectSpec(
|
||||
"group-v101", "Folder", "staging", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
|
||||
),
|
||||
ObjectSpec(
|
||||
"group-v102", "Folder", "templates", "group-v23", {"folder_type": "VIRTUAL_MACHINE"}
|
||||
),
|
||||
]
|
||||
for index in range(1, host_count + 1):
|
||||
moid = f"host-{10 + index}"
|
||||
specs.append(
|
||||
ObjectSpec(
|
||||
moid,
|
||||
"HostSystem",
|
||||
f"esxi{index:02d}.lab.local",
|
||||
"domain-c21",
|
||||
{
|
||||
"connection_state": "CONNECTED",
|
||||
"power_state": "POWERED_ON",
|
||||
"cpu_cores": 32 if index % 3 else 64,
|
||||
"cpu_mhz": 2500,
|
||||
"memory_size_mib": 262144 if index % 2 else 524288,
|
||||
"ip_address": f"192.168.1.{10 + index}",
|
||||
"version": "8.0.2",
|
||||
"cluster": "domain-c21",
|
||||
"maintenance_mode": False,
|
||||
"networking": {
|
||||
"dns": {"servers": ["8.8.8.8", "1.1.1.1"], "domains": ["lab.local"]},
|
||||
"routing": {"default_gateway": "192.168.1.1"},
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "vmk0",
|
||||
"mac": f"00:50:56:00:{index:02x}:01",
|
||||
"ipv4": {"address": f"192.168.1.{10 + index}", "prefix": 24},
|
||||
}
|
||||
],
|
||||
},
|
||||
"storage_devices": [
|
||||
{
|
||||
"device": f"naa.lab{index:04d}",
|
||||
"display_name": f"Local Disk {index}",
|
||||
"capacity": 1099511627776 * (1 + index % 3),
|
||||
"ssd": index % 2 == 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
for index in range(1, datastore_count + 1):
|
||||
moid = f"datastore-{30 + index}"
|
||||
capacity = 1099511627776 * (1 + (index % 3))
|
||||
specs.append(
|
||||
ObjectSpec(
|
||||
moid,
|
||||
"Datastore",
|
||||
f"ds-{index:02d}" if index > 1 else "datastore1",
|
||||
"group-s23",
|
||||
{
|
||||
"type": "VMFS" if index % 2 else "NFS",
|
||||
"capacity": capacity,
|
||||
"free_space": capacity // 2,
|
||||
"accessible": True,
|
||||
"multiple_host_access": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
specs.append(
|
||||
ObjectSpec(
|
||||
"network-41",
|
||||
"Network",
|
||||
"VM Network",
|
||||
"group-n23",
|
||||
{"type": "STANDARD_PORTGROUP"},
|
||||
)
|
||||
)
|
||||
for index in range(2, network_count + 1):
|
||||
specs.append(
|
||||
ObjectSpec(
|
||||
f"dvportgroup-{40 + index}",
|
||||
"DistributedVirtualPortgroup",
|
||||
f"dvpg-vlan{100 + index}",
|
||||
"group-n23",
|
||||
{"type": "DISTRIBUTED_PORTGROUP", "vlan_id": 100 + index},
|
||||
)
|
||||
)
|
||||
specs.append(
|
||||
ObjectSpec(
|
||||
"dvs-51",
|
||||
"VmwareDistributedVirtualSwitch",
|
||||
"DSwitch",
|
||||
"group-n23",
|
||||
{"version": "8.0.0", "mtu": 9000},
|
||||
)
|
||||
)
|
||||
return specs
|
||||
|
||||
|
||||
def _vm_device_props(*, name: str, power: str, index: int, nic_mac: str) -> dict[str, Any]:
|
||||
"""Shared hardware / guest fields for lab VMs (used by API surface + SOAP)."""
|
||||
|
||||
guest_ip = f"10.0.{(index // 250) % 250}.{(index % 250) or 1}"
|
||||
return {
|
||||
"nics": [
|
||||
{
|
||||
"key": "4000",
|
||||
"value": {
|
||||
"label": "Network adapter 1",
|
||||
"mac": nic_mac,
|
||||
"state": "CONNECTED" if power == "POWERED_ON" else "NOT_CONNECTED",
|
||||
"type": "VMXNET3",
|
||||
"backing": {"type": "STANDARD_PORTGROUP", "network": "network-41"},
|
||||
},
|
||||
}
|
||||
],
|
||||
"disks": [
|
||||
{
|
||||
"key": "2000",
|
||||
"value": {
|
||||
"label": "Hard disk 1",
|
||||
"capacity": 42949672960 + (index % 5) * 10737418240,
|
||||
"type": "SCSI",
|
||||
},
|
||||
}
|
||||
],
|
||||
"cdroms": [
|
||||
{
|
||||
"cdrom": "3000",
|
||||
"label": "CD/DVD drive 1",
|
||||
"state": "CONNECTED",
|
||||
"backing": {"type": "ISO_FILE", "iso_file": "[datastore1] ISO/ubuntu.iso"},
|
||||
}
|
||||
],
|
||||
"floppies": [{"floppy": "8000", "state": "NOT_CONNECTED"}],
|
||||
"serials": [{"port": "9000", "yield_on_poll": True}],
|
||||
"parallels": [{"port": "10000", "yield_on_poll": True}],
|
||||
"scsi_adapters": [
|
||||
{"adapter": "1000", "type": "LSILOGIC", "sharing": "NONE", "pci_slot_number": 16}
|
||||
],
|
||||
"sata_adapters": [{"adapter": "15000", "bus": 0, "pci_slot_number": 33}],
|
||||
"nvme_adapters": [{"adapter": "19000", "bus": 0, "pci_slot_number": 160}],
|
||||
"boot": {
|
||||
"type": "BIOS",
|
||||
"delay": 0,
|
||||
"retry": False,
|
||||
"retry_delay": 10000,
|
||||
"enter_setup_mode": False,
|
||||
},
|
||||
"boot_devices": [{"type": "CDROM"}, {"type": "DISK"}, {"type": "ETHERNET"}],
|
||||
"guest_ip": guest_ip,
|
||||
"guest_filesystems": {
|
||||
"filesystems": {
|
||||
"/": {"capacity": 42949672960, "free_space": 21474836480},
|
||||
}
|
||||
},
|
||||
"guest_networking": {
|
||||
"dns": {"ip_addresses": ["8.8.8.8"], "host_name": name, "domain_name": "lab.local"},
|
||||
"ip": {
|
||||
"ip_addresses": [
|
||||
{
|
||||
"ip_address": guest_ip,
|
||||
"prefix_length": 24,
|
||||
"state": "PREFERRED",
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"customization": {
|
||||
"name": name,
|
||||
"status": "PENDING",
|
||||
"spec": {"hostname": name, "domain": "lab.local"},
|
||||
},
|
||||
"tools": {
|
||||
"auto_update_supported": True,
|
||||
"install_attempted": True,
|
||||
"run_state": "RUNNING" if power == "POWERED_ON" else "NOT_RUNNING",
|
||||
"upgrade_policy": "MANUAL",
|
||||
"version_number": 12320,
|
||||
"version_status": "CURRENT",
|
||||
},
|
||||
"cpu": {"cores_per_socket": 1, "hot_add_enabled": False},
|
||||
"memory": {"hot_add_enabled": False},
|
||||
"identity": {
|
||||
"name": name,
|
||||
"instance_uuid": f"5029{index:04d}-0000-0000-0000-{index:012d}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _vm_spec(index: int, *, host_count: int) -> ObjectSpec:
|
||||
moid = f"vm-{100 + index}"
|
||||
role = ROLE_PREFIX[index % len(ROLE_PREFIX)]
|
||||
name = f"{role}-{index:04d}"
|
||||
power = POWER_CYCLE[index % len(POWER_CYCLE)]
|
||||
host = f"host-{10 + (index % host_count) + 1}"
|
||||
cpus = 1 + (index % 8)
|
||||
memory = 1024 * (1 + (index % 16))
|
||||
folder = ("group-v100", "group-v101", "group-v23")[index % 3]
|
||||
guest = GUEST_OS[index % len(GUEST_OS)]
|
||||
ds_index = 1 + (index % 4)
|
||||
nic_tail = f"{(index % 250):02x}"
|
||||
devices = _vm_device_props(
|
||||
name=name,
|
||||
power=power,
|
||||
index=index,
|
||||
nic_mac=f"00:50:56:01:{(index // 256) % 256:02x}:{nic_tail}",
|
||||
)
|
||||
return ObjectSpec(
|
||||
moid,
|
||||
"VirtualMachine",
|
||||
name,
|
||||
folder,
|
||||
{
|
||||
"power_state": power,
|
||||
"cpu_count": cpus,
|
||||
"memory_size_mib": memory,
|
||||
"guest_OS": guest,
|
||||
"hardware_version": "VMX_19",
|
||||
"host": host,
|
||||
"datastore": f"datastore-{30 + ds_index}",
|
||||
"resource_pool": "resgroup-22",
|
||||
"networks": ["network-41"],
|
||||
"template": False,
|
||||
"tools_status": "GUEST_TOOLS_RUNNING"
|
||||
if power == "POWERED_ON"
|
||||
else "GUEST_TOOLS_NOT_RUNNING",
|
||||
**devices,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def lab_credentials() -> tuple[CredentialSpec, ...]:
|
||||
return (
|
||||
CredentialSpec("administrator@vsphere.local", "VMware1!", ("Administrator",)),
|
||||
CredentialSpec("readonly@vsphere.local", "VMware1!", ("ReadOnly",)),
|
||||
CredentialSpec("operator@vsphere.local", "VMware1!", ("VirtualMachinePowerUser",)),
|
||||
CredentialSpec("vmadmin@vsphere.local", "VMware1!", ("VirtualMachineAdministrator",)),
|
||||
)
|
||||
|
||||
|
||||
def lab_permissions() -> tuple[PermissionSpec, ...]:
|
||||
return (
|
||||
PermissionSpec("administrator@vsphere.local", "Administrator", None, True),
|
||||
PermissionSpec("readonly@vsphere.local", "ReadOnly", "datacenter-21", True),
|
||||
PermissionSpec("operator@vsphere.local", "VirtualMachinePowerUser", "group-v23", True),
|
||||
PermissionSpec("vmadmin@vsphere.local", "VirtualMachineAdministrator", "group-v23", True),
|
||||
)
|
||||
|
||||
|
||||
def small_vsphere_profile() -> VsphereSeedProfile:
|
||||
"""Compact seed used by unit/integration tests (named VMs)."""
|
||||
|
||||
objects = _topology(host_count=3, datastore_count=2, network_count=2)
|
||||
named = (
|
||||
("web-01", "POWERED_ON", "host-11", 2, 4096),
|
||||
("web-02", "POWERED_ON", "host-12", 2, 4096),
|
||||
("db-01", "POWERED_ON", "host-13", 4, 8192),
|
||||
("app-01", "POWERED_OFF", "host-11", 2, 2048),
|
||||
("jumpbox", "SUSPENDED", "host-12", 1, 1024),
|
||||
)
|
||||
vms: list[ObjectSpec] = []
|
||||
for index, (name, power, host, cpus, memory) in enumerate(named, start=1):
|
||||
moid = f"vm-{100 + index}"
|
||||
devices = _vm_device_props(
|
||||
name=name,
|
||||
power=power,
|
||||
index=index,
|
||||
nic_mac=f"00:50:56:01:00:{moid[-2:]}",
|
||||
)
|
||||
devices["identity"]["instance_uuid"] = f"5029{moid[-3:]}-0000-0000-0000-000000000000"
|
||||
vms.append(
|
||||
ObjectSpec(
|
||||
moid,
|
||||
"VirtualMachine",
|
||||
name,
|
||||
"group-v23",
|
||||
{
|
||||
"power_state": power,
|
||||
"cpu_count": cpus,
|
||||
"memory_size_mib": memory,
|
||||
"guest_OS": "UBUNTU_64",
|
||||
"hardware_version": "VMX_19",
|
||||
"host": host,
|
||||
"datastore": "datastore-31",
|
||||
"resource_pool": "resgroup-22",
|
||||
"networks": ["network-41"],
|
||||
"template": False,
|
||||
"tools_status": "GUEST_TOOLS_RUNNING"
|
||||
if power == "POWERED_ON"
|
||||
else "GUEST_TOOLS_NOT_RUNNING",
|
||||
**devices,
|
||||
},
|
||||
)
|
||||
)
|
||||
return VsphereSeedProfile(
|
||||
name="small",
|
||||
objects=tuple(objects + vms),
|
||||
credentials=lab_credentials(),
|
||||
permissions=lab_permissions(),
|
||||
host_count=3,
|
||||
vm_count=5,
|
||||
)
|
||||
|
||||
|
||||
def large_vsphere_profile(*, host_count: int = 10, vm_count: int = 1000) -> VsphereSeedProfile:
|
||||
if host_count < 1 or vm_count < 1:
|
||||
raise ValueError("host_count and vm_count must be positive")
|
||||
objects = _topology(host_count=host_count, datastore_count=4, network_count=4)
|
||||
# Keep first five named VMs for cookbook / smoke compatibility.
|
||||
base = small_vsphere_profile()
|
||||
named_vms = [obj for obj in base.objects if obj.type == "VirtualMachine"]
|
||||
generated = [_vm_spec(index, host_count=host_count) for index in range(6, vm_count + 1)]
|
||||
# Ensure first 5 from small keep stable ids/names; replace generated slots 1-5.
|
||||
vms = list(named_vms)
|
||||
if vm_count > 5:
|
||||
vms.extend(generated)
|
||||
elif vm_count < 5:
|
||||
vms = vms[:vm_count]
|
||||
return VsphereSeedProfile(
|
||||
name="large",
|
||||
objects=tuple(objects + vms),
|
||||
credentials=lab_credentials(),
|
||||
permissions=lab_permissions(),
|
||||
host_count=host_count,
|
||||
vm_count=len(vms),
|
||||
)
|
||||
|
||||
|
||||
def demo_cluster_vsphere_profile() -> VsphereSeedProfile:
|
||||
"""Enterprise-shaped cluster: 20 hosts, 1000 VMs (aligned with Proxmox demo-cluster)."""
|
||||
|
||||
profile = large_vsphere_profile(host_count=20, vm_count=1000)
|
||||
return VsphereSeedProfile(
|
||||
name="demo-cluster",
|
||||
objects=profile.objects,
|
||||
credentials=profile.credentials,
|
||||
permissions=profile.permissions,
|
||||
host_count=profile.host_count,
|
||||
vm_count=profile.vm_count,
|
||||
)
|
||||
|
||||
|
||||
def build_vsphere_profile(
|
||||
name: str | None = None,
|
||||
*,
|
||||
large_hosts: int | None = None,
|
||||
large_vms: int | None = None,
|
||||
) -> VsphereSeedProfile:
|
||||
profile_name = (name or os.getenv("SEED_VSPHERE_PROFILE") or "large").strip().lower()
|
||||
hosts = (
|
||||
large_hosts if large_hosts is not None else int(os.getenv("SEED_VSPHERE_LARGE_HOSTS", "10"))
|
||||
)
|
||||
vms = large_vms if large_vms is not None else int(os.getenv("SEED_VSPHERE_LARGE_VMS", "1000"))
|
||||
if profile_name.lower() in {"small", "minimal"}:
|
||||
return small_vsphere_profile()
|
||||
if profile_name in {"demo-cluster", "demo", "enterprise"}:
|
||||
return demo_cluster_vsphere_profile()
|
||||
if profile_name == "large":
|
||||
return large_vsphere_profile(host_count=hosts, vm_count=vms)
|
||||
raise ValueError(f"unknown vSphere seed profile: {profile_name}")
|
||||
|
||||
|
||||
def props_json(props: dict[str, Any]) -> str:
|
||||
return json.dumps(props)
|
||||
Reference in New Issue
Block a user