Files
vmware-api-simulator/app/vsphere/profiles.py
T
inecs f8d3cbdd59 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.
2026-07-18 04:42:11 +03:00

469 lines
16 KiB
Python

"""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)