Expand lab seed tiers, OpenAPI PARAMS, and console DATA/Params UX.

This commit is contained in:
2026-07-18 08:46:39 +03:00
parent f8d3cbdd59
commit 63cc409424
71 changed files with 38380 additions and 796 deletions
+152 -42
View File
@@ -40,6 +40,7 @@ class VsphereSeedProfile:
permissions: tuple[PermissionSpec, ...]
host_count: int
vm_count: int
extras_scale: int = 1
POWER_CYCLE = ("POWERED_ON", "POWERED_ON", "POWERED_ON", "POWERED_OFF", "SUSPENDED")
@@ -63,6 +64,7 @@ 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"}),
@@ -111,6 +113,16 @@ def _topology(
"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(
@@ -291,7 +303,13 @@ def _vm_device_props(*, name: str, power: str, index: int, nic_mac: str) -> dict
}
def _vm_spec(index: int, *, host_count: int) -> ObjectSpec:
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}"
@@ -299,9 +317,10 @@ def _vm_spec(index: int, *, host_count: int) -> ObjectSpec:
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]
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 % 4)
ds_index = 1 + (index % max(1, datastore_count))
nic_tail = f"{(index % 250):02x}"
devices = _vm_device_props(
name=name,
@@ -351,10 +370,35 @@ def lab_permissions() -> tuple[PermissionSpec, ...]:
)
def small_vsphere_profile() -> VsphereSeedProfile:
"""Compact seed used by unit/integration tests (named VMs)."""
@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."""
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),
@@ -396,52 +440,97 @@ def small_vsphere_profile() -> VsphereSeedProfile:
},
)
)
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="small",
name=size.name,
objects=tuple(objects + vms),
credentials=lab_credentials(),
permissions=lab_permissions(),
host_count=3,
vm_count=5,
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:
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),
"""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:
"""Enterprise-shaped cluster: 20 hosts, 1000 VMs (aligned with Proxmox demo-cluster)."""
"""Backward-compatible alias for ``big`` (UI / older docs used 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,
)
return big_vsphere_profile()
def build_vsphere_profile(
@@ -455,14 +544,35 @@ def build_vsphere_profile(
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"}:
if profile_name in {"minimal", "mini", "reset"}:
return minimal_vsphere_profile()
if profile_name in {"small"}:
return small_vsphere_profile()
if profile_name in {"demo-cluster", "demo", "enterprise"}:
return demo_cluster_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)