746 lines
24 KiB
Python
746 lines
24 KiB
Python
"""Content library + datastore file metadata."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import secrets
|
||
from typing import Any
|
||
|
||
from asyncpg.exceptions import UniqueViolationError # type: ignore[import-untyped]
|
||
|
||
from app.db.pool import Database
|
||
from app.vsphere import inventory
|
||
from app.vsphere.domain import tagging
|
||
from app.vsphere.domain import tasks as task_store
|
||
from app.vsphere.errors import already_exists, invalid_argument, not_found
|
||
|
||
|
||
def _pool(database: Database) -> Any:
|
||
return database.pool # type: ignore[attr-defined]
|
||
|
||
|
||
async def create_library(
|
||
database: Database,
|
||
*,
|
||
name: str,
|
||
description: str = "",
|
||
library_id: str | None = None,
|
||
) -> str:
|
||
if not name.strip():
|
||
raise invalid_argument("name is required")
|
||
lib_id = library_id or f"lib-{secrets.token_hex(6)}"
|
||
pool = _pool(database)
|
||
try:
|
||
async with pool.acquire() as conn:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_libraries (id, name, description, type, props)
|
||
VALUES ($1, $2, $3, 'LOCAL', '{}'::jsonb)
|
||
ON CONFLICT (id) DO NOTHING
|
||
""",
|
||
lib_id,
|
||
name,
|
||
description,
|
||
)
|
||
# Id already present (seed ensure) — treat as success.
|
||
exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", lib_id)
|
||
if exists:
|
||
return lib_id
|
||
except UniqueViolationError as error:
|
||
# Name collision with a different id: keep requesting id with a unique name.
|
||
if library_id:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_libraries (id, name, description, type, props)
|
||
VALUES ($1, $2, $3, 'LOCAL', '{}'::jsonb)
|
||
ON CONFLICT (id) DO NOTHING
|
||
""",
|
||
lib_id,
|
||
f"{name} ({lib_id})",
|
||
description,
|
||
)
|
||
return lib_id
|
||
raise already_exists(f"Library {name} already exists") from error
|
||
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 [_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(
|
||
database: Database,
|
||
*,
|
||
library_id: str,
|
||
name: str,
|
||
item_type: str = "ovf",
|
||
description: str = "",
|
||
item_id: str | None = None,
|
||
) -> str:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", library_id)
|
||
if not exists:
|
||
raise not_found(f"Library {library_id} not found")
|
||
resolved_id = item_id or f"item-{secrets.token_hex(6)}"
|
||
try:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_library_items (id, library_id, name, type, description)
|
||
VALUES ($1, $2, $3, $4, $5)
|
||
ON CONFLICT (id) DO NOTHING
|
||
""",
|
||
resolved_id,
|
||
library_id,
|
||
name,
|
||
item_type,
|
||
description,
|
||
)
|
||
except UniqueViolationError:
|
||
# (library_id, name) taken — keep stable id with a unique name for seed.
|
||
if item_id:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_library_items (id, library_id, name, type, description)
|
||
VALUES ($1, $2, $3, $4, $5)
|
||
ON CONFLICT (id) DO NOTHING
|
||
""",
|
||
resolved_id,
|
||
library_id,
|
||
f"{name}-{resolved_id}",
|
||
item_type,
|
||
description,
|
||
)
|
||
else:
|
||
raise
|
||
return resolved_id
|
||
|
||
|
||
async def list_library_items(database: Database, library_id: str) -> list[dict[str, Any]]:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
exists = await conn.fetchval("SELECT 1 FROM vsphere_libraries WHERE id = $1", library_id)
|
||
if not exists:
|
||
raise not_found(f"Library {library_id} not found")
|
||
rows = await conn.fetch(
|
||
"SELECT * FROM vsphere_library_items WHERE library_id = $1 ORDER BY name",
|
||
library_id,
|
||
)
|
||
return [_library_item_info(row) for row in rows]
|
||
|
||
|
||
_LAB_SESSION_ID = "session-lab-1"
|
||
_LAB_ITEM_ID = "item-ubuntu"
|
||
|
||
|
||
async def clear_transfer_sessions(database: Database) -> None:
|
||
"""Drop durable transfer sessions (used by inventory reseed)."""
|
||
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
await conn.execute(
|
||
"""
|
||
DO $$ BEGIN
|
||
IF to_regclass('public.vsphere_transfer_sessions') IS NOT NULL THEN
|
||
DELETE FROM vsphere_transfer_sessions;
|
||
END IF;
|
||
END $$;
|
||
"""
|
||
)
|
||
|
||
|
||
def _decode_files(value: Any) -> dict[str, Any]:
|
||
import json
|
||
|
||
current = value
|
||
while isinstance(current, str):
|
||
current = json.loads(current)
|
||
if isinstance(current, dict):
|
||
return current
|
||
return {}
|
||
|
||
|
||
async def _upsert_session(
|
||
database: Database,
|
||
*,
|
||
session_id: str,
|
||
kind: str,
|
||
library_item_id: str,
|
||
state: str,
|
||
files: dict[str, Any],
|
||
) -> None:
|
||
import json
|
||
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_transfer_sessions (id, kind, library_item_id, state, files, updated_at)
|
||
VALUES ($1, $2, $3, $4, $5::jsonb, now())
|
||
ON CONFLICT (id, kind) DO UPDATE SET
|
||
library_item_id = EXCLUDED.library_item_id,
|
||
state = EXCLUDED.state,
|
||
files = EXCLUDED.files,
|
||
updated_at = now()
|
||
""",
|
||
session_id,
|
||
kind,
|
||
library_item_id,
|
||
state,
|
||
json.dumps(files),
|
||
)
|
||
|
||
|
||
async def _get_session_row(
|
||
database: Database, session_id: str, kind: str | None = None
|
||
) -> dict[str, Any] | None:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
if kind:
|
||
row = await conn.fetchrow(
|
||
"SELECT * FROM vsphere_transfer_sessions WHERE id = $1 AND kind = $2",
|
||
session_id,
|
||
kind,
|
||
)
|
||
else:
|
||
row = await conn.fetchrow(
|
||
"SELECT * FROM vsphere_transfer_sessions WHERE id = $1",
|
||
session_id,
|
||
)
|
||
if row is None:
|
||
return None
|
||
return {
|
||
"id": row["id"],
|
||
"kind": row["kind"],
|
||
"library_item_id": row["library_item_id"],
|
||
"state": row["state"],
|
||
"files": _decode_files(row["files"]),
|
||
}
|
||
|
||
|
||
async def create_update_session(
|
||
database: Database,
|
||
*,
|
||
library_item_id: str,
|
||
session_id: str | None = None,
|
||
) -> str:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
exists = await conn.fetchval(
|
||
"SELECT 1 FROM vsphere_library_items WHERE id = $1", library_item_id
|
||
)
|
||
if not exists:
|
||
raise not_found(f"Library item {library_item_id} not found")
|
||
resolved = session_id or f"update-session-{secrets.token_hex(6)}"
|
||
files = {
|
||
"upload.bin": {
|
||
"name": "upload.bin",
|
||
"source_type": "PUSH",
|
||
"size": 1024,
|
||
"status": "READY",
|
||
"upload_endpoint": {
|
||
"uri": f"/api/content/library/item/update-session/{resolved}/file/upload.bin",
|
||
},
|
||
}
|
||
}
|
||
await _upsert_session(
|
||
database,
|
||
session_id=resolved,
|
||
kind="update",
|
||
library_item_id=library_item_id,
|
||
state="ACTIVE",
|
||
files=files,
|
||
)
|
||
return resolved
|
||
|
||
|
||
async def add_update_session_file(
|
||
database: Database,
|
||
session_id: str,
|
||
*,
|
||
name: str,
|
||
source_type: str = "PUSH",
|
||
size: int = 0,
|
||
content: str = "",
|
||
) -> dict[str, Any]:
|
||
session = await _get_session_row(database, session_id, "update")
|
||
if session is None:
|
||
raise not_found(f"Update session {session_id} not found")
|
||
file_info = {
|
||
"name": name,
|
||
"source_type": source_type,
|
||
"size": size or len(content.encode("utf-8")),
|
||
"status": "READY",
|
||
"upload_endpoint": {
|
||
"uri": f"/api/content/library/item/update-session/{session_id}/file/{name}",
|
||
},
|
||
"content": content,
|
||
}
|
||
files = dict(session["files"])
|
||
files[name] = file_info
|
||
await _upsert_session(
|
||
database,
|
||
session_id=session_id,
|
||
kind="update",
|
||
library_item_id=session["library_item_id"],
|
||
state=session["state"],
|
||
files=files,
|
||
)
|
||
return {
|
||
"name": name,
|
||
"source_type": source_type,
|
||
"size": file_info["size"],
|
||
"status": "READY",
|
||
"upload_endpoint": file_info["upload_endpoint"],
|
||
}
|
||
|
||
|
||
async def complete_update_session(database: Database, session_id: str) -> None:
|
||
session = await _get_session_row(database, session_id, "update")
|
||
if session is None:
|
||
raise not_found(f"Update session {session_id} not found")
|
||
await _upsert_session(
|
||
database,
|
||
session_id=session_id,
|
||
kind="update",
|
||
library_item_id=session["library_item_id"],
|
||
state="DONE",
|
||
files=session["files"],
|
||
)
|
||
|
||
|
||
async def get_update_session(database: Database, session_id: str) -> dict[str, Any]:
|
||
session = await _get_session_row(database, session_id, "update")
|
||
if session is None:
|
||
raise not_found(f"Update session {session_id} not found")
|
||
return {
|
||
"id": session["id"],
|
||
"library_item_id": session["library_item_id"],
|
||
"state": session["state"],
|
||
"client_progress": 100 if session["state"] == "DONE" else 50,
|
||
}
|
||
|
||
|
||
async def create_download_session(
|
||
database: Database,
|
||
*,
|
||
library_item_id: str,
|
||
session_id: str | None = None,
|
||
) -> str:
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
exists = await conn.fetchval(
|
||
"SELECT 1 FROM vsphere_library_items WHERE id = $1", library_item_id
|
||
)
|
||
if not exists:
|
||
raise not_found(f"Library item {library_item_id} not found")
|
||
resolved = session_id or f"download-session-{secrets.token_hex(6)}"
|
||
files = {
|
||
"descriptor.ovf": {
|
||
"name": "descriptor.ovf",
|
||
"size": 256,
|
||
"status": "READY",
|
||
"download_endpoint": {
|
||
"uri": f"/api/content/library/item/download-session/{resolved}/file/descriptor.ovf",
|
||
},
|
||
}
|
||
}
|
||
await _upsert_session(
|
||
database,
|
||
session_id=resolved,
|
||
kind="download",
|
||
library_item_id=library_item_id,
|
||
state="ACTIVE",
|
||
files=files,
|
||
)
|
||
return resolved
|
||
|
||
|
||
async def get_download_session(database: Database, session_id: str) -> dict[str, Any]:
|
||
session = await _get_session_row(database, session_id, "download")
|
||
if session is None:
|
||
raise not_found(f"Download session {session_id} not found")
|
||
return {
|
||
"id": session["id"],
|
||
"library_item_id": session["library_item_id"],
|
||
"state": session["state"],
|
||
}
|
||
|
||
|
||
async def list_download_session_files(database: Database, session_id: str) -> list[dict[str, Any]]:
|
||
session = await _get_session_row(database, session_id, "download")
|
||
if session is None:
|
||
raise not_found(f"Download session {session_id} not found")
|
||
return [
|
||
{
|
||
"name": f["name"],
|
||
"size": f["size"],
|
||
"status": f["status"],
|
||
"download_endpoint": f["download_endpoint"],
|
||
}
|
||
for f in session["files"].values()
|
||
]
|
||
|
||
|
||
async def deploy_ovf_from_library(
|
||
database: Database,
|
||
*,
|
||
item_id: str,
|
||
name: str,
|
||
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:
|
||
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")
|
||
moid = await inventory.next_moid(database, "vm")
|
||
await inventory.upsert_object(
|
||
database,
|
||
moid=moid,
|
||
type_name="VirtualMachine",
|
||
name=name,
|
||
parent_moid=folder,
|
||
props={
|
||
"power_state": "POWERED_OFF",
|
||
"cpu_count": 2,
|
||
"memory_size_mib": 2048,
|
||
"guest_OS": "OTHER_GUEST_64",
|
||
"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,
|
||
"nics": [],
|
||
"disks": [
|
||
{
|
||
"key": "2000",
|
||
"value": {"label": "Hard disk 1", "capacity": 21474836480, "type": "SCSI"},
|
||
}
|
||
],
|
||
},
|
||
)
|
||
task_id = await task_store.create_task(
|
||
database,
|
||
description=f"Deploy OVF item {item_id} as {moid}",
|
||
service="com.vmware.vcenter.ovf",
|
||
operation="deploy",
|
||
result={"vm": moid},
|
||
)
|
||
return moid, task_id
|
||
|
||
|
||
async def list_datastore_files(database: Database, datastore: str) -> list[dict[str, Any]]:
|
||
obj = await inventory.get_object(database, datastore)
|
||
if obj is None or obj.type != "Datastore":
|
||
raise not_found(f"Datastore {datastore} not found")
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
rows = await conn.fetch(
|
||
"""
|
||
SELECT path, size, type FROM vsphere_datastore_files
|
||
WHERE datastore_moid = $1 ORDER BY path
|
||
""",
|
||
datastore,
|
||
)
|
||
return [{"path": row["path"], "size": row["size"], "type": row["type"]} for row in rows]
|
||
|
||
|
||
async def put_datastore_file(
|
||
database: Database,
|
||
datastore: str,
|
||
path: str,
|
||
*,
|
||
size: int = 0,
|
||
file_type: str = "FILE",
|
||
) -> None:
|
||
obj = await inventory.get_object(database, datastore)
|
||
if obj is None or obj.type != "Datastore":
|
||
raise not_found(f"Datastore {datastore} not found")
|
||
pool = _pool(database)
|
||
async with pool.acquire() as conn:
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO vsphere_datastore_files (datastore_moid, path, size, type)
|
||
VALUES ($1, $2, $3, $4)
|
||
ON CONFLICT (datastore_moid, path) DO UPDATE SET size = EXCLUDED.size, type = EXCLUDED.type
|
||
""",
|
||
datastore,
|
||
path,
|
||
size,
|
||
file_type,
|
||
)
|
||
|
||
|
||
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,
|
||
name="Local Content",
|
||
description="Lab library",
|
||
library_id="lib-local-1",
|
||
)
|
||
await create_library_item(
|
||
database,
|
||
library_id=lib,
|
||
name="ubuntu-22.04",
|
||
item_type="ovf",
|
||
description="Ubuntu template OVF",
|
||
item_id="item-ubuntu",
|
||
)
|
||
await create_library_item(
|
||
database,
|
||
library_id=lib,
|
||
name="centos-stream-9",
|
||
item_type="ovf",
|
||
description="CentOS Stream OVF",
|
||
item_id="item-centos",
|
||
)
|
||
pub = await create_library(
|
||
database,
|
||
name="Published Templates",
|
||
description="Published lab library",
|
||
library_id="lib-published-1",
|
||
)
|
||
await create_library_item(
|
||
database,
|
||
library_id=pub,
|
||
name="golden-image",
|
||
item_type="ovf",
|
||
description="Golden image OVF",
|
||
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",
|
||
description="Environment tags",
|
||
associable_types=["VirtualMachine", "HostSystem"],
|
||
category_id="urn:vmomi:InventoryServiceCategory:environment:GLOBAL",
|
||
)
|
||
owner = await tagging.create_category(
|
||
database,
|
||
name="Owner",
|
||
description="Team ownership",
|
||
associable_types=["VirtualMachine"],
|
||
category_id="urn:vmomi:InventoryServiceCategory:owner:GLOBAL",
|
||
)
|
||
prod = await tagging.create_tag(
|
||
database,
|
||
category_id=env,
|
||
name="prod",
|
||
tag_id="urn:vmomi:InventoryServiceTag:prod:GLOBAL",
|
||
)
|
||
await tagging.create_tag(
|
||
database,
|
||
category_id=env,
|
||
name="staging",
|
||
tag_id="urn:vmomi:InventoryServiceTag:staging:GLOBAL",
|
||
)
|
||
await tagging.create_tag(
|
||
database,
|
||
category_id=owner,
|
||
name="platform",
|
||
tag_id="urn:vmomi:InventoryServiceTag:platform:GLOBAL",
|
||
)
|
||
await tagging.create_category(
|
||
database,
|
||
name="Lab",
|
||
description="Probe alias category",
|
||
associable_types=["VirtualMachine"],
|
||
category_id="cat-lab-1",
|
||
)
|
||
await tagging.create_tag(
|
||
database,
|
||
category_id="cat-lab-1",
|
||
name="lab",
|
||
tag_id="tag-lab-1",
|
||
)
|
||
await tagging.attach_tag(database, prod, "VirtualMachine", "vm-101")
|
||
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
|
||
)
|
||
await put_datastore_file(
|
||
database, "datastore-31", "[datastore1] ISO/vmware-tools.iso", size=120000000
|
||
)
|
||
await put_datastore_file(database, "datastore-31", "[datastore1] web-01/web-01.vmx", size=4096)
|
||
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)
|
||
async with pool.acquire() as conn:
|
||
has_item = await conn.fetchval("SELECT 1 FROM vsphere_library_items WHERE id = $1", item_id)
|
||
if has_item:
|
||
await create_download_session(database, library_item_id=item_id, session_id=_LAB_SESSION_ID)
|
||
await create_update_session(database, library_item_id=item_id, session_id=_LAB_SESSION_ID)
|
||
|
||
# Lab snapshots so GET /snapshots is DB-backed (no create-on-read).
|
||
from app.vsphere.domain import vm_ops
|
||
|
||
for vm_moid in ("vm-101", "vm-102"):
|
||
snaps = await vm_ops.list_snapshots(database, vm_moid)
|
||
if not snaps:
|
||
await vm_ops.create_snapshot(
|
||
database, vm_moid, name="initial", description="Lab default snapshot"
|
||
)
|
||
|
||
await task_store.create_task(
|
||
database,
|
||
description="Lab inventory seed",
|
||
service="com.vmware.vcenter",
|
||
operation="seed",
|
||
status="SUCCEEDED",
|
||
result={"seeded": True, "profile": "lab"},
|
||
task_id="task-1",
|
||
)
|