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:
2026-07-18 04:42:11 +03:00
commit f8d3cbdd59
422 changed files with 361335 additions and 0 deletions
+632
View File
@@ -0,0 +1,632 @@
"""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
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
]
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 [
{
"id": row["id"],
"library_id": row["library_id"],
"name": row["name"],
"type": row["type"],
"description": row["description"],
}
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",
) -> 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,
"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) -> 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``, …
"""
from app.vsphere.domain import tasks as task_store
# 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",
)
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")
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
)
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",
)