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.
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
"""Enterprise-scale demo cluster profile for realistic emulator workloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
|
||||
from app.simulation.seed import (
|
||||
SeedNode,
|
||||
SeedProfile,
|
||||
SeedResource,
|
||||
SeedTask,
|
||||
_node,
|
||||
_resource,
|
||||
stable_id,
|
||||
)
|
||||
|
||||
DEMO_NODE_COUNT = 20
|
||||
DEMO_QEMU_COUNT = 850
|
||||
DEMO_LXC_COUNT = 150
|
||||
DEMO_CEPH_OSD_COUNT = 300
|
||||
CEPH_TOTAL_BYTES = 5 * 1024**5
|
||||
QEMU_VMID_START = 100
|
||||
LXC_VMID_START = 10_000
|
||||
|
||||
QEMU_PREFIXES = (
|
||||
"web",
|
||||
"api",
|
||||
"db",
|
||||
"cache",
|
||||
"mq",
|
||||
"batch",
|
||||
"ml",
|
||||
"monitor",
|
||||
"log",
|
||||
"ci",
|
||||
"k8s",
|
||||
"vpn",
|
||||
"ldap",
|
||||
"git",
|
||||
"proxy",
|
||||
)
|
||||
LXC_PREFIXES = (
|
||||
"svc-nginx",
|
||||
"svc-haproxy",
|
||||
"svc-dns",
|
||||
"svc-vault",
|
||||
"svc-redis",
|
||||
"mon-agent",
|
||||
"backup-agent",
|
||||
"ceph-mgr",
|
||||
"lb-vip",
|
||||
"proxy-squid",
|
||||
"jump-host",
|
||||
"ntp",
|
||||
"syslog",
|
||||
"metrics",
|
||||
"bastion",
|
||||
)
|
||||
TIERS = ("prod", "staging", "dev", "qa", "dr")
|
||||
POOLS = (
|
||||
("production", 280),
|
||||
("staging", 160),
|
||||
("development", 130),
|
||||
("qa", 100),
|
||||
("gpu-workloads", 80),
|
||||
("legacy", 100),
|
||||
)
|
||||
TASK_TYPES = (
|
||||
"vzdump",
|
||||
"qmstart",
|
||||
"qmstop",
|
||||
"qmmigrate",
|
||||
"qmreboot",
|
||||
"qmclone",
|
||||
"aptupdate",
|
||||
"startall",
|
||||
"stopall",
|
||||
"cephosd",
|
||||
"pct-start",
|
||||
"pct-stop",
|
||||
)
|
||||
|
||||
|
||||
def _even_node_slots(node_count: int, total: int, *, phase: int = 0) -> tuple[int, ...]:
|
||||
"""Return `total` node indices distributed as evenly as possible."""
|
||||
|
||||
if total <= 0:
|
||||
return ()
|
||||
base, remainder = divmod(total, node_count)
|
||||
slots: list[int] = []
|
||||
for node_index in range(node_count):
|
||||
slots.extend([node_index] * (base + (1 if node_index < remainder else 0)))
|
||||
if phase:
|
||||
phase %= len(slots)
|
||||
slots = slots[phase:] + slots[:phase]
|
||||
return tuple(slots)
|
||||
|
||||
|
||||
def _even_sample(resources: Sequence[SeedResource], count: int) -> list[str]:
|
||||
"""Pick `count` resource IDs spread evenly across the provided sequence."""
|
||||
|
||||
if count <= 0 or not resources:
|
||||
return []
|
||||
if count >= len(resources):
|
||||
return [resource.external_id for resource in resources]
|
||||
step = len(resources) / count
|
||||
return [resources[int(index * step)].external_id for index in range(count)]
|
||||
|
||||
|
||||
def _guest_name(prefixes: tuple[str, ...], index: int) -> str:
|
||||
prefix = prefixes[index % len(prefixes)]
|
||||
tier = TIERS[index % len(TIERS)]
|
||||
return f"{tier}-{prefix}-{index:04d}"
|
||||
|
||||
|
||||
def _qemu_state(vmid: int, index: int) -> dict[str, object]:
|
||||
statuses = ("running", "running", "running", "running", "stopped", "paused")
|
||||
cpus = (1, 2, 2, 4, 4, 8, 8, 16, 32)[index % 9]
|
||||
memory_mb = (512, 1024, 2048, 4096, 8192, 16_384, 32_768, 65_536)[index % 8]
|
||||
pool_name = POOLS[index % len(POOLS)][0]
|
||||
return {
|
||||
"name": _guest_name(QEMU_PREFIXES, index),
|
||||
"status": statuses[index % len(statuses)],
|
||||
"cpus": cpus,
|
||||
"cores": cpus,
|
||||
"memory": memory_mb,
|
||||
"maxmem": memory_mb,
|
||||
"pool": pool_name,
|
||||
"tags": f"{TIERS[index % len(TIERS)]};{pool_name}",
|
||||
"agent": index % 3 != 0,
|
||||
"template": index % 97 == 0,
|
||||
"onboot": index % 5 != 0,
|
||||
"vmid": vmid,
|
||||
}
|
||||
|
||||
|
||||
def _lxc_state(vmid: int, index: int) -> dict[str, object]:
|
||||
statuses = ("running", "running", "stopped", "stopped")
|
||||
memory_mb = (256, 512, 1024, 2048, 4096)[index % 5]
|
||||
pool_name = POOLS[(index + 2) % len(POOLS)][0]
|
||||
return {
|
||||
"name": _guest_name(LXC_PREFIXES, index),
|
||||
"status": statuses[index % len(statuses)],
|
||||
"cpus": (1, 1, 2, 2, 4)[index % 5],
|
||||
"memory": memory_mb,
|
||||
"maxmem": memory_mb,
|
||||
"pool": pool_name,
|
||||
"tags": f"container;{pool_name}",
|
||||
"unprivileged": index % 4 != 0,
|
||||
"template": index % 41 == 0,
|
||||
"vmid": vmid,
|
||||
}
|
||||
|
||||
|
||||
def _demo_task(index: int, node: SeedNode, task_type: str, resource_id: str) -> SeedTask:
|
||||
return SeedTask(
|
||||
stable_id(f"demo-task:{index}:{task_type}:{resource_id}"),
|
||||
f"UPID:{node.name}:{index:07X}:{index:07X}:67{index:06X}:"
|
||||
f"{task_type}:{resource_id}:root@pam:",
|
||||
task_type,
|
||||
{"resource_id": resource_id, "node": node.name, "seeded": True},
|
||||
)
|
||||
|
||||
|
||||
def demo_cluster_profile() -> SeedProfile:
|
||||
nodes = tuple(
|
||||
_node(f"pve{index:02d}", "offline" if index == 19 else "online")
|
||||
for index in range(1, DEMO_NODE_COUNT + 1)
|
||||
)
|
||||
node_count = len(nodes)
|
||||
resources: list[SeedResource] = []
|
||||
|
||||
qemu_slots = _even_node_slots(node_count, DEMO_QEMU_COUNT, phase=0)
|
||||
lxc_slots = _even_node_slots(node_count, DEMO_LXC_COUNT, phase=node_count // 2)
|
||||
osd_slots = _even_node_slots(node_count, DEMO_CEPH_OSD_COUNT, phase=node_count // 4)
|
||||
|
||||
qemu_resources: list[SeedResource] = []
|
||||
for offset, node_index in enumerate(qemu_slots):
|
||||
vmid = QEMU_VMID_START + offset
|
||||
resource = _resource(nodes[node_index], "qemu", str(vmid), _qemu_state(vmid, offset))
|
||||
qemu_resources.append(resource)
|
||||
resources.append(resource)
|
||||
|
||||
lxc_resources: list[SeedResource] = []
|
||||
for offset, node_index in enumerate(lxc_slots):
|
||||
vmid = LXC_VMID_START + offset
|
||||
resource = _resource(nodes[node_index], "lxc", str(vmid), _lxc_state(vmid, offset))
|
||||
lxc_resources.append(resource)
|
||||
resources.append(resource)
|
||||
|
||||
guests_by_node: dict[uuid.UUID, list[SeedResource]] = defaultdict(list)
|
||||
for guest in (*qemu_resources, *lxc_resources):
|
||||
guests_by_node[guest.node_id].append(guest)
|
||||
|
||||
for node in nodes:
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"storage",
|
||||
f"local-{node.name}",
|
||||
{
|
||||
"content": ["iso", "vztmpl", "backup"],
|
||||
"status": "available",
|
||||
"storage_type": "dir",
|
||||
},
|
||||
)
|
||||
)
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"storage",
|
||||
f"local-lvm-{node.name}",
|
||||
{
|
||||
"content": ["images", "rootdir"],
|
||||
"status": "available",
|
||||
"storage_type": "lvmthin",
|
||||
"shared": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"storage",
|
||||
f"backup-{node.name}",
|
||||
{
|
||||
"content": ["backup"],
|
||||
"status": "available",
|
||||
"storage_type": "dir",
|
||||
"shared": False,
|
||||
"total_bytes": 4 * 1024**4,
|
||||
"used_bytes": int(2.2 * 1024**4),
|
||||
},
|
||||
)
|
||||
)
|
||||
if int(node.name[3:]) % 2 == 0:
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"storage",
|
||||
f"local-zfs-{node.name}",
|
||||
{
|
||||
"content": ["images", "rootdir"],
|
||||
"status": "available",
|
||||
"storage_type": "zfspool",
|
||||
"shared": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
used_bytes = int(CEPH_TOTAL_BYTES * 0.62)
|
||||
resources.append(
|
||||
_resource(
|
||||
nodes[0],
|
||||
"storage",
|
||||
"ceph-prod",
|
||||
{
|
||||
"content": ["images", "rootdir", "backup"],
|
||||
"shared": True,
|
||||
"status": "available",
|
||||
"storage_type": "ceph",
|
||||
"ceph_pool": "rbd",
|
||||
"total_bytes": CEPH_TOTAL_BYTES,
|
||||
"used_bytes": used_bytes,
|
||||
"osd_count": DEMO_CEPH_OSD_COUNT,
|
||||
},
|
||||
)
|
||||
)
|
||||
resources.append(
|
||||
_resource(
|
||||
nodes[node_count // 2],
|
||||
"storage",
|
||||
"nfs-backup",
|
||||
{
|
||||
"content": ["backup", "iso"],
|
||||
"shared": True,
|
||||
"status": "available",
|
||||
"storage_type": "nfs",
|
||||
"total_bytes": 80 * 1024**4,
|
||||
"used_bytes": 52 * 1024**4,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
for osd_index, node_index in enumerate(osd_slots):
|
||||
node = nodes[node_index]
|
||||
osd_id = osd_index
|
||||
weight = round(0.8 + (osd_index % 17) * 0.05, 2)
|
||||
size_bytes = CEPH_TOTAL_BYTES // DEMO_CEPH_OSD_COUNT
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"ceph-osd",
|
||||
f"osd.{osd_id}",
|
||||
{
|
||||
"osd_id": osd_id,
|
||||
"status": "up" if osd_index != 42 else "down",
|
||||
"in": osd_index != 42,
|
||||
"weight": weight,
|
||||
"size_bytes": size_bytes,
|
||||
"used_bytes": int(size_bytes * (0.55 + (osd_index % 10) * 0.03)),
|
||||
"device_class": "ssd" if osd_index % 4 else "hdd",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
qemu_by_node = [
|
||||
sorted(guests_by_node[node.id], key=lambda resource: int(resource.external_id))
|
||||
for node in nodes
|
||||
]
|
||||
pool_guest_cursor = 0
|
||||
for pool_index, (pool_id, member_count) in enumerate(POOLS):
|
||||
pool_guests: list[SeedResource] = []
|
||||
per_node, extra = divmod(member_count, node_count)
|
||||
for node_index, node_guests in enumerate(qemu_by_node):
|
||||
take = per_node + (1 if node_index < extra else 0)
|
||||
start = (pool_guest_cursor + node_index) % len(node_guests) if node_guests else 0
|
||||
for offset in range(take):
|
||||
if not node_guests:
|
||||
break
|
||||
pool_guests.append(node_guests[(start + offset) % len(node_guests)])
|
||||
pool_guest_cursor += member_count
|
||||
pool_guests.sort(key=lambda resource: int(resource.external_id))
|
||||
resources.append(
|
||||
_resource(
|
||||
nodes[pool_index % node_count],
|
||||
"pool",
|
||||
pool_id,
|
||||
{
|
||||
"members": _even_sample(pool_guests, min(40, len(pool_guests))),
|
||||
"member_count": len(pool_guests),
|
||||
"comment": f"Simulated {pool_id} pool",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
ha_guests = [
|
||||
qemu_resources[int(index * len(qemu_resources) / min(120, len(qemu_resources)))]
|
||||
for index in range(min(120, len(qemu_resources)))
|
||||
]
|
||||
for ha_index, guest in enumerate(ha_guests):
|
||||
node = next(node for node in nodes if node.id == guest.node_id)
|
||||
resources.append(
|
||||
_resource(
|
||||
node,
|
||||
"ha",
|
||||
f"vm:{guest.external_id}",
|
||||
{
|
||||
"state": "started" if ha_index % 5 else "stopped",
|
||||
"group": "critical-services",
|
||||
"max_relocate": 2,
|
||||
"max_restart": 3,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
tasks: list[SeedTask] = []
|
||||
guest_cycle = sorted(
|
||||
(*qemu_resources, *lxc_resources),
|
||||
key=lambda resource: (resource.node_id, int(resource.external_id)),
|
||||
)
|
||||
for index in range(1, 251):
|
||||
guest = guest_cycle[(index - 1) % len(guest_cycle)]
|
||||
node = next(node for node in nodes if node.id == guest.node_id)
|
||||
task_type = TASK_TYPES[index % len(TASK_TYPES)]
|
||||
tasks.append(_demo_task(index, node, task_type, guest.external_id))
|
||||
|
||||
return SeedProfile("demo-cluster", nodes, tuple(resources), tuple(tasks))
|
||||
Reference in New Issue
Block a user