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