Files
inecs e8b08526d1 Keep API not-found and 401 responses usable behind Ingress and in the Web UI.
Return native JSON with the missing id in the message, clear stale sessions on
401, and document ingress-nginx annotations so branded HTML 404/405 pages do
not rewrite simulator bodies.
2026-07-22 06:52:35 +03:00

221 lines
6.4 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)
_TYPE_RESOURCE: dict[str, str] = {
"HostSystem": "host",
"VirtualMachine": "vm",
"Datastore": "datastore",
"Datacenter": "datacenter",
"Folder": "folder",
"ClusterComputeResource": "cluster",
"ResourcePool": "resource_pool",
"Network": "network",
}
async def require_object(
database: Database,
moid: str,
*,
type_name: str | None = None,
resource: str | None = None,
) -> ManagedObject:
"""Return the managed object or raise a native not-found with the bad id."""
from app.vsphere.errors import no_such
obj = await get_object(database, moid)
label = resource or (type_name and _TYPE_RESOURCE.get(type_name)) or "object"
if obj is None or (type_name is not None and obj.type != type_name):
raise no_such(label, moid)
return obj
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 {}),
)