Expand lab seed tiers, OpenAPI PARAMS, and console DATA/Params UX.
This commit is contained in:
+133
-20
@@ -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`` (1–4) 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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user