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
+133 -20
View File
@@ -65,19 +65,92 @@ async def create_library(
return lib_id
def _props(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return value
if isinstance(value, str):
import json
try:
decoded = json.loads(value)
except json.JSONDecodeError:
return {}
return decoded if isinstance(decoded, dict) else {}
return {}
def _library_info(row: Any) -> dict[str, Any]:
props = _props(row["props"])
return {
"id": row["id"],
"name": row["name"],
"description": row["description"] or "",
"type": row["type"],
"creation_time": props.get("creation_time"),
"last_modified_time": props.get("last_modified_time"),
"storage_backings": props.get("storage_backings")
or [{"type": "DATASTORE", "datastore_id": "datastore-31"}],
"state": props.get("state") or "ACTIVE",
"version": str(props.get("version") or "1"),
}
def _library_item_info(row: Any) -> dict[str, Any]:
props = _props(row["props"])
return {
"id": row["id"],
"library_id": row["library_id"],
"name": row["name"],
"type": row["type"],
"description": row["description"] or "",
"content_version": str(props.get("content_version") or "1"),
"creation_time": props.get("creation_time"),
"last_modified_time": props.get("last_modified_time"),
"size": int(props.get("size") or 0),
"cached": bool(props.get("cached", True)),
"security_compliance": bool(props.get("security_compliance", True)),
}
async def list_libraries(database: Database) -> list[dict[str, Any]]:
pool = _pool(database)
async with pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM vsphere_libraries ORDER BY name")
return [
{
"id": row["id"],
"name": row["name"],
"description": row["description"],
"type": row["type"],
}
for row in rows
]
return [_library_info(row) for row in rows]
async def get_library(database: Database, library_id: str) -> dict[str, Any]:
pool = _pool(database)
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM vsphere_libraries WHERE id = $1", library_id)
if row is None:
raise not_found(f"Library {library_id} not found")
return _library_info(row)
async def delete_library(database: Database, library_id: str) -> None:
pool = _pool(database)
async with pool.acquire() as conn:
result = await conn.execute("DELETE FROM vsphere_libraries WHERE id = $1", library_id)
if result == "DELETE 0":
raise not_found(f"Library {library_id} not found")
async def get_library_item(database: Database, item_id: str) -> dict[str, Any]:
pool = _pool(database)
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM vsphere_library_items WHERE id = $1", item_id)
if row is None:
raise not_found(f"Library item {item_id} not found")
return _library_item_info(row)
async def delete_library_item(database: Database, item_id: str) -> None:
pool = _pool(database)
async with pool.acquire() as conn:
result = await conn.execute("DELETE FROM vsphere_library_items WHERE id = $1", item_id)
if result == "DELETE 0":
raise not_found(f"Library item {item_id} not found")
async def create_library_item(
@@ -138,16 +211,7 @@ async def list_library_items(database: Database, library_id: str) -> list[dict[s
"SELECT * FROM vsphere_library_items WHERE library_id = $1 ORDER BY name",
library_id,
)
return [
{
"id": row["id"],
"library_id": row["library_id"],
"name": row["name"],
"type": row["type"],
"description": row["description"],
}
for row in rows
]
return [_library_item_info(row) for row in rows]
_LAB_SESSION_ID = "session-lab-1"
@@ -411,6 +475,7 @@ async def deploy_ovf_from_library(
folder: str = "group-v23",
host: str = "host-11",
datastore: str = "datastore-31",
resource_pool: str | None = None,
) -> tuple[str, str]:
pool = _pool(database)
async with pool.acquire() as conn:
@@ -432,6 +497,7 @@ async def deploy_ovf_from_library(
"hardware_version": "VMX_19",
"host": host,
"datastore": datastore,
"resource_pool": resource_pool or "resgroup-22",
"networks": ["network-41"],
"identity": {"name": name},
"deployed_from_library_item": item_id,
@@ -496,15 +562,18 @@ async def put_datastore_file(
)
async def seed_platform_extras(database: Database) -> None:
async def seed_platform_extras(database: Database, *, scale: int = 1) -> None:
"""Idempotent demo libraries/tags/files/sessions when platform tables exist.
Always ensures stable lab IDs so probes and clients hit non-empty GETs:
``lib-local-1``, ``item-ubuntu``, ``cat-lab-1``, ``tag-lab-1``, ``session-lab-1``, …
``scale`` (14) adds proportional extra libraries/items/files for larger profiles.
"""
from app.vsphere.domain import tasks as task_store
scale = max(1, min(int(scale or 1), 8))
# Ensure lab libraries/items even when older random-id rows already exist.
lib = await create_library(
database,
@@ -543,6 +612,23 @@ async def seed_platform_extras(database: Database) -> None:
item_id="item-golden",
)
# Proportional extras for large/big tiers.
for index in range(2, scale + 1):
extra_lib = await create_library(
database,
name=f"Team Library {index}",
description=f"Scaled lab library {index}",
library_id=f"lib-local-{index}",
)
await create_library_item(
database,
library_id=extra_lib,
name=f"template-{index:02d}",
item_type="ovf",
description=f"Scaled template {index}",
item_id=f"item-lab-{index}",
)
env = await tagging.create_category(
database,
name="Environment",
@@ -592,6 +678,25 @@ async def seed_platform_extras(database: Database) -> None:
await tagging.attach_tag(database, prod, "VirtualMachine", "vm-102")
await tagging.attach_tag(database, "tag-lab-1", "VirtualMachine", "vm-101")
# Proportional categories/tags for larger tiers (extras_scale).
for index in range(2, scale + 1):
cat_id = f"cat-scale-{index}"
tag_id = f"tag-scale-{index}"
await tagging.create_category(
database,
name=f"Scale {index}",
description=f"Scaled category {index}",
associable_types=["VirtualMachine", "HostSystem"],
category_id=cat_id,
)
await tagging.create_tag(
database,
category_id=cat_id,
name=f"tier-{index}",
tag_id=tag_id,
)
await tagging.attach_tag(database, tag_id, "VirtualMachine", "vm-101")
await put_datastore_file(
database, "datastore-31", "[datastore1] ISO/ubuntu.iso", size=900000000
)
@@ -602,6 +707,14 @@ async def seed_platform_extras(database: Database) -> None:
await put_datastore_file(
database, "datastore-31", "[datastore1] web-01/web-01.vmdk", size=42949672960
)
for index in range(2, scale + 1):
ds = f"datastore-{30 + min(index, 8)}"
await put_datastore_file(
database,
ds,
f"[ds-{index:02d}] ISO/lab-media-{index}.iso",
size=50_000_000 * index,
)
item_id = _LAB_ITEM_ID
pool = _pool(database)
+47
View File
@@ -38,6 +38,9 @@ async def create_folder(
async def create_datacenter(database: Database, *, name: str, folder: str = "group-d1") -> str:
import secrets
parent_obj = await inventory.get_object(database, folder)
if parent_obj is None:
raise not_found(f"Parent {folder} not found")
moid = f"datacenter-{secrets.randbelow(900) + 100}"
host_folder = f"group-h{secrets.randbelow(90) + 10}"
vm_folder = f"group-v{secrets.randbelow(90) + 10}"
@@ -83,6 +86,9 @@ async def create_cluster(
) -> str:
import secrets
parent_obj = await inventory.get_object(database, folder)
if parent_obj is None:
raise not_found(f"Parent {folder} not found")
moid = f"domain-c{secrets.randbelow(900) + 100}"
rp = f"resgroup-{secrets.randbelow(900) + 100}"
await inventory.upsert_object(
@@ -162,6 +168,9 @@ async def delete_managed(database: Database, moid: str) -> None:
obj = await inventory.get_object(database, moid)
if obj is None:
raise not_found(f"Object {moid} not found")
# Protect the lab seed spine so surface/matrix probes cannot empty the inventory dump.
if moid in _SEED_PROTECTED_MOIDS or _is_seed_host_or_named_vm(moid, obj):
raise invalid_argument(f"Cannot delete protected seed object {moid}")
children = [
child for child in await inventory.list_objects(database) if child.parent_moid == moid
]
@@ -170,6 +179,44 @@ async def delete_managed(database: Database, moid: str) -> None:
await inventory.delete_object(database, moid)
# Stable MOIDs from profiles._topology / small named VMs (all seed sizes).
_SEED_PROTECTED_MOIDS = frozenset(
{
"group-d1",
"datacenter-21",
"group-h23",
"group-v23",
"group-s23",
"group-n23",
"domain-c21",
"resgroup-22",
"group-v100",
"group-v101",
"group-v102",
"network-41",
"dvs-51",
*(f"datastore-{30 + n}" for n in range(1, 9)),
*(f"dvportgroup-{40 + n}" for n in range(2, 9)),
*(f"group-v{103 + n}" for n in range(0, 8)),
"vm-101",
"vm-102",
"vm-103",
"vm-104",
"vm-105",
}
)
def _is_seed_host_or_named_vm(moid: str, obj: Any) -> bool:
if obj.type == "HostSystem" and moid.startswith("host-"):
# Seed hosts are host-11..host-N; probe hosts use other patterns if any.
try:
return 11 <= int(moid.split("-", 1)[1]) <= 40
except ValueError:
return False
return False
async def set_host_maintenance(database: Database, host: str, enabled: bool) -> dict[str, Any]:
obj = await inventory.get_object(database, host)
if obj is None or obj.type != "HostSystem":
+23 -9
View File
@@ -72,6 +72,15 @@ async def list_tasks(database: Database) -> list[dict[str, Any]]:
return [_row(row) for row in rows]
def _localizable(message: str, *, message_id: str = "com.vmware.cis.task.description") -> dict[str, Any]:
return {
"id": message_id,
"default_message": message,
"args": [],
"localized": message,
}
def _row(row: Any) -> dict[str, Any]:
result = row["result"]
error = row["error"]
@@ -80,20 +89,25 @@ def _row(row: Any) -> dict[str, Any]:
if isinstance(error, str):
error = json.loads(error)
status = row["status"]
state = {
"PENDING": "PENDING",
"RUNNING": "RUNNING",
"SUCCEEDED": "SUCCEEDED",
"FAILED": "FAILED",
}.get(status, status)
completed = 100 if status in {"SUCCEEDED", "FAILED"} else 50
description_text = row["description"] or row["operation"] or "task"
# Cis Task Info wire shape (Automation) plus lab-friendly aliases used by cookbooks.
return {
"task": row["id"],
"description": row["description"],
"description": _localizable(description_text),
"status": status,
"state": state,
"state": status,
"service": row["service"],
"operation": row["operation"],
"progress": 100 if status in {"SUCCEEDED", "FAILED"} else 50,
"cancelable": False,
"progress": {
"total": 100,
"completed": completed,
"message": _localizable(
f"{completed}%",
message_id="com.vmware.cis.task.progress",
),
},
"result": result,
"error": error,
"start_time": row["created_at"].isoformat() if row["created_at"] else None,
+16 -4
View File
@@ -394,19 +394,31 @@ async def revert_snapshot(database: Database, vm: str, snapshot: str) -> str:
async def update_hardware_cpu(database: Database, vm: str, count: int) -> None:
obj = await require_vm(database, vm)
if obj.props.get("power_state") == "POWERED_ON":
raise invalid_argument("CPU count change requires powered-off VM in this simulator")
cpu = dict(obj.props.get("cpu") or {})
if obj.props.get("power_state") == "POWERED_ON" and not cpu.get("hot_add_enabled"):
raise invalid_argument(
"Virtual machine must be powered off to reconfigure CPU count "
"when CPU hot-add is disabled"
)
props = dict(obj.props)
props["cpu_count"] = count
cpu["count"] = count
props["cpu"] = cpu
await inventory.update_props(database, vm, props)
async def update_hardware_memory(database: Database, vm: str, size_mib: int) -> None:
obj = await require_vm(database, vm)
if obj.props.get("power_state") == "POWERED_ON":
raise invalid_argument("Memory change requires powered-off VM in this simulator")
memory = dict(obj.props.get("memory") or {})
if obj.props.get("power_state") == "POWERED_ON" and not memory.get("hot_add_enabled"):
raise invalid_argument(
"Virtual machine must be powered off to reconfigure memory "
"when memory hot-add is disabled"
)
props = dict(obj.props)
props["memory_size_mib"] = size_mib
memory["size_MiB"] = size_mib
props["memory"] = memory
await inventory.update_props(database, vm, props)