Files
vmware-api-simulator/app/vsphere/profiles.py
T

579 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
extras_scale: int = 1
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,
extra_vm_folders: int = 0,
) -> 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(extra_vm_folders):
specs.append(
ObjectSpec(
f"group-v{103 + index}",
"Folder",
f"team-{index + 1:02d}",
"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,
datastore_count: int = 4,
folder_choices: tuple[str, ...] | None = None,
) -> 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))
folders = folder_choices or ("group-v100", "group-v101", "group-v23")
folder = folders[index % len(folders)]
guest = GUEST_OS[index % len(GUEST_OS)]
ds_index = 1 + (index % max(1, datastore_count))
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),
)
@dataclass(frozen=True, slots=True)
class ProfileSize:
"""Canonical lab sizes: hosts / VMs / datastores / networks / platform extras scale."""
name: str
host_count: int
vm_count: int
datastore_count: int
network_count: int
extras_scale: int
# Proportional inventory tiers shown in the Web UI DATA panel.
PROFILE_SIZES: dict[str, ProfileSize] = {
# Reset / unload target — cookbook-only inventory.
"minimal": ProfileSize(
"minimal", host_count=3, vm_count=5, datastore_count=1, network_count=1, extras_scale=1
),
"small": ProfileSize("small", host_count=3, vm_count=50, datastore_count=2, network_count=2, extras_scale=1),
"large": ProfileSize(
"large", host_count=10, vm_count=1000, datastore_count=4, network_count=4, extras_scale=2
),
"big": ProfileSize("big", host_count=20, vm_count=2000, datastore_count=8, network_count=8, extras_scale=4),
}
def _named_lab_vms() -> list[ObjectSpec]:
"""Stable cookbook VMs (vm-101..vm-105) present in every profile."""
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 vms
def _build_sized_profile(size: ProfileSize) -> VsphereSeedProfile:
if size.host_count < 1 or size.vm_count < 1:
raise ValueError("host_count and vm_count must be positive")
# Folders scale with extras: minimal/small=0, large=2, big=6.
extra_vm_folders = max(0, (size.extras_scale - 1) * 2)
objects = _topology(
host_count=size.host_count,
datastore_count=size.datastore_count,
network_count=size.network_count,
extra_vm_folders=extra_vm_folders,
)
named = _named_lab_vms()
# Minimal / single-datastore profiles still reference datastore-31.
if size.datastore_count < 1:
raise ValueError("datastore_count must be positive")
folder_choices = (
"group-v100",
"group-v101",
"group-v23",
*(f"group-v{103 + i}" for i in range(extra_vm_folders)),
)
vms: list[ObjectSpec] = list(named)
if size.vm_count > len(named):
vms.extend(
_vm_spec(
index,
host_count=size.host_count,
datastore_count=size.datastore_count,
folder_choices=folder_choices,
)
for index in range(len(named) + 1, size.vm_count + 1)
)
elif size.vm_count < len(named):
vms = vms[: size.vm_count]
return VsphereSeedProfile(
name=size.name,
objects=tuple(objects + vms),
credentials=lab_credentials(),
permissions=lab_permissions(),
host_count=size.host_count,
vm_count=len(vms),
extras_scale=size.extras_scale,
)
def minimal_vsphere_profile() -> VsphereSeedProfile:
"""Reset target: 3 hosts · 5 cookbook VMs · 1 datastore · 1 network."""
return _build_sized_profile(PROFILE_SIZES["minimal"])
def small_vsphere_profile() -> VsphereSeedProfile:
"""Lab tier: 3 hosts · 50 VMs · 2 datastores · 2 networks."""
return _build_sized_profile(PROFILE_SIZES["small"])
def large_vsphere_profile(*, host_count: int = 10, vm_count: int = 1000) -> VsphereSeedProfile:
"""Lab tier: 10 hosts · 1000 VMs (defaults); kwargs keep Makefile overrides."""
size = PROFILE_SIZES["large"]
if host_count == size.host_count and vm_count == size.vm_count:
return _build_sized_profile(size)
# Custom scale: keep datastore/network proportion to hosts (≈0.4× hosts, min 2).
datastore_count = max(2, round(host_count * 0.4))
network_count = max(2, round(host_count * 0.4))
return _build_sized_profile(
ProfileSize(
name="large",
host_count=host_count,
vm_count=vm_count,
datastore_count=datastore_count,
network_count=network_count,
extras_scale=max(1, datastore_count // 2),
)
)
def big_vsphere_profile() -> VsphereSeedProfile:
"""Lab tier: 20 hosts · 2000 VMs · 8 datastores · 8 networks."""
return _build_sized_profile(PROFILE_SIZES["big"])
def demo_cluster_vsphere_profile() -> VsphereSeedProfile:
"""Backward-compatible alias for ``big`` (UI / older docs used demo-cluster)."""
return big_vsphere_profile()
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 in {"minimal", "mini", "reset"}:
return minimal_vsphere_profile()
if profile_name in {"small"}:
return small_vsphere_profile()
if profile_name in {"big", "demo-cluster", "demo", "enterprise"}:
return big_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 infer_profile_hint(*, hosts: int, vms: int, datastores: int = 0) -> str:
"""Map live inventory counts back to a DATA-panel profile name."""
for size in PROFILE_SIZES.values():
if hosts == size.host_count and vms == size.vm_count:
if datastores and datastores != size.datastore_count:
continue
return size.name
if hosts >= 15 and vms >= 1500:
return "big"
if hosts >= 8 and vms >= 500:
return "large"
if hosts <= 4 and vms <= 10:
return "minimal"
if hosts <= 4:
return "small"
return "custom"
def props_json(props: dict[str, Any]) -> str:
return json.dumps(props)