579 lines
19 KiB
Python
579 lines
19 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
|
||
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)
|