f8d3cbdd59
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.
191 lines
5.6 KiB
Python
191 lines
5.6 KiB
Python
"""Persistent vSphere managed-object inventory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from app.db.pool import Database
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ManagedObject:
|
|
moid: str
|
|
type: str
|
|
name: str
|
|
parent_moid: str | None
|
|
props: dict[str, Any]
|
|
|
|
|
|
def _pool(database: Database) -> Any:
|
|
return database.pool # type: ignore[attr-defined]
|
|
|
|
|
|
async def count_objects(database: Database) -> int:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
return int(await conn.fetchval("SELECT COUNT(*) FROM vsphere_objects") or 0)
|
|
|
|
|
|
async def list_objects(
|
|
database: Database,
|
|
*,
|
|
type_name: str | None = None,
|
|
) -> list[ManagedObject]:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
if type_name is None:
|
|
rows = await conn.fetch(
|
|
"SELECT moid, type, name, parent_moid, props FROM vsphere_objects ORDER BY type, name"
|
|
)
|
|
else:
|
|
rows = await conn.fetch(
|
|
"""
|
|
SELECT moid, type, name, parent_moid, props FROM vsphere_objects
|
|
WHERE type = $1 ORDER BY name
|
|
""",
|
|
type_name,
|
|
)
|
|
return [_row(row) for row in rows]
|
|
|
|
|
|
async def get_object(database: Database, moid: str) -> ManagedObject | None:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"SELECT moid, type, name, parent_moid, props FROM vsphere_objects WHERE moid = $1",
|
|
moid,
|
|
)
|
|
return None if row is None else _row(row)
|
|
|
|
|
|
async def upsert_object(
|
|
database: Database,
|
|
*,
|
|
moid: str,
|
|
type_name: str,
|
|
name: str,
|
|
parent_moid: str | None,
|
|
props: dict[str, Any],
|
|
) -> None:
|
|
await upsert_objects_batch(
|
|
database,
|
|
[
|
|
{
|
|
"moid": moid,
|
|
"type": type_name,
|
|
"name": name,
|
|
"parent_moid": parent_moid,
|
|
"props": props,
|
|
}
|
|
],
|
|
)
|
|
|
|
|
|
async def upsert_objects_batch(database: Database, rows: list[dict[str, Any]]) -> None:
|
|
if not rows:
|
|
return
|
|
pool = _pool(database)
|
|
payload = [
|
|
(
|
|
str(row["moid"]),
|
|
str(row["type"]),
|
|
str(row["name"]),
|
|
None if row.get("parent_moid") is None else str(row["parent_moid"]),
|
|
json.dumps(row.get("props") or {}),
|
|
)
|
|
for row in rows
|
|
]
|
|
async with pool.acquire() as conn:
|
|
await conn.executemany(
|
|
"""
|
|
INSERT INTO vsphere_objects (moid, type, name, parent_moid, props)
|
|
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
ON CONFLICT (moid) DO UPDATE SET
|
|
type = EXCLUDED.type,
|
|
name = EXCLUDED.name,
|
|
parent_moid = EXCLUDED.parent_moid,
|
|
props = EXCLUDED.props,
|
|
updated_at = now()
|
|
""",
|
|
payload,
|
|
)
|
|
|
|
|
|
async def count_by_type(database: Database) -> dict[str, int]:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch(
|
|
"SELECT type, COUNT(*)::int AS count FROM vsphere_objects GROUP BY type ORDER BY type"
|
|
)
|
|
return {str(row["type"]): int(row["count"]) for row in rows}
|
|
|
|
|
|
async def update_props(database: Database, moid: str, props: dict[str, Any]) -> ManagedObject:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
UPDATE vsphere_objects
|
|
SET props = $2::jsonb, updated_at = now()
|
|
WHERE moid = $1
|
|
RETURNING moid, type, name, parent_moid, props
|
|
""",
|
|
moid,
|
|
json.dumps(props),
|
|
)
|
|
if row is None:
|
|
raise KeyError(moid)
|
|
return _row(row)
|
|
|
|
|
|
async def delete_object(database: Database, moid: str) -> bool:
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
result = await conn.execute("DELETE FROM vsphere_objects WHERE moid = $1", moid)
|
|
return result.endswith("1")
|
|
|
|
|
|
async def next_moid(database: Database, prefix: str) -> str:
|
|
"""Allocate the next MoID under an advisory lock (safe under concurrent create).
|
|
|
|
Inserts a reservation row before releasing the lock so two creators cannot
|
|
compute the same next id between allocate and upsert.
|
|
"""
|
|
pool = _pool(database)
|
|
async with pool.acquire() as conn:
|
|
async with conn.transaction():
|
|
await conn.execute("SELECT pg_advisory_xact_lock(hashtext($1))", f"moid:{prefix}")
|
|
rows = await conn.fetch(
|
|
"SELECT moid FROM vsphere_objects WHERE moid LIKE $1",
|
|
f"{prefix}-%",
|
|
)
|
|
numbers: list[int] = []
|
|
for row in rows:
|
|
suffix = str(row["moid"]).removeprefix(f"{prefix}-")
|
|
if suffix.isdigit():
|
|
numbers.append(int(suffix))
|
|
moid = f"{prefix}-{max(numbers, default=100) + 1}"
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO vsphere_objects (moid, type, name, parent_moid, props)
|
|
VALUES ($1, 'MoIdReservation', $1, NULL, '{}'::jsonb)
|
|
""",
|
|
moid,
|
|
)
|
|
return moid
|
|
|
|
|
|
def _row(row: Any) -> ManagedObject:
|
|
props = row["props"]
|
|
if isinstance(props, str):
|
|
props = json.loads(props)
|
|
return ManagedObject(
|
|
moid=str(row["moid"]),
|
|
type=str(row["type"]),
|
|
name=str(row["name"]),
|
|
parent_moid=None if row["parent_moid"] is None else str(row["parent_moid"]),
|
|
props=dict(props or {}),
|
|
)
|