"""Generic surface-complete CRUD backed by ov_api_objects for undeclared collections.""" from __future__ import annotations import json from typing import Any from uuid import uuid4 from asyncpg import Connection from fastapi import Request, Response from app.ovirt.errors import OVirtError from app.ovirt.repr import generic_entity from app.ovirt.serialize import respond, unwrap_entity # Map collection path segment → (element singular, default status) _COLLECTIONS: dict[str, tuple[str, str]] = { "affinitylabels": ("affinity_label", "ok"), "bookmarks": ("bookmark", "ok"), "clusterlevels": ("cluster_level", "ok"), "domains": ("domain", "ok"), "externalhostproviders": ("external_host_provider", "ok"), "groups": ("group", "ok"), "icons": ("icon", "ok"), "instancetypes": ("instance_type", "ok"), "katelloerrata": ("katello_erratum", "ok"), "macpools": ("mac_pool", "ok"), "networkfilters": ("network_filter", "ok"), "openstackimageproviders": ("openstack_image_provider", "ok"), "openstacknetworkproviders": ("openstack_network_provider", "ok"), "openstackvolumeproviders": ("openstack_volume_provider", "ok"), "operatingsystems": ("operating_system", "ok"), "permissions": ("permission", "ok"), "roles": ("role", "ok"), "schedulingpolicies": ("scheduling_policy", "ok"), "schedulingpolicyunits": ("scheduling_policy_unit", "ok"), "tags": ("tag", "ok"), "vmpools": ("vm_pool", "ok"), "imagetransfers": ("image_transfer", "ok"), "options": ("engine_option", "ok"), "networklabels": ("network_label", "ok"), "cpuprofiles": ("cpu_profile", "ok"), "diskprofiles": ("disk_profile", "ok"), "diskattachments": ("disk_attachment", "ok"), "qoss": ("qos", "ok"), "iscsibonds": ("iscsi_bond", "ok"), "glustervolumes": ("gluster_volume", "ok"), "files": ("file", "ok"), "images": ("image", "ok"), "permits": ("permit", "ok"), "filters": ("filter", "ok"), "weights": ("weight", "ok"), "balances": ("balance", "ok"), "enabledfeatures": ("cluster_enabled_feature", "ok"), "cdroms": ("cdrom", "ok"), "graphicsconsoles": ("graphics_console", "ok"), "reporteddevices": ("reported_device", "ok"), "sessions": ("session", "ok"), "applications": ("application", "ok"), "watchdogs": ("watchdog", "ok"), "hostdevices": ("host_device", "ok"), "numanodes": ("numa_node", "ok"), "mediateddevices": ("vm_mediated_device", "ok"), "statistics": ("statistic", "ok"), "hooks": ("hook", "ok"), "devices": ("host_device", "ok"), "sshpublickeys": ("ssh_public_key", "ok"), "networkfilterparameters": ("network_filter_parameter", "ok"), "storage": ("host_storage", "ok"), } def _meta(collection: str) -> tuple[str, str]: return _COLLECTIONS.get(collection, (collection.rstrip("s") or "object", "ok")) async def handle_generic( request: Request, conn: Connection, method: str, parts: list[str], payload: dict[str, Any], ) -> Response: from app.ovirt.settings import OPT_DEFAULT_API_OBJECT_STATUS, option_value collection = parts[0] element, _catalog_status = _meta(collection) default_status = await option_value(conn, OPT_DEFAULT_API_OBJECT_STATUS) if len(parts) == 1: if method == "GET": rows = await conn.fetch( """SELECT * FROM ov_api_objects WHERE collection=$1 AND parent_id IS NULL ORDER BY name""", collection, ) items = [generic_entity(collection, element, r) for r in rows] return respond(request, element=element, collection=collection, data=items) if method == "POST": body = unwrap_entity(payload, element) oid = uuid4() name = str(body.get("name") or f"{element}-{oid.hex[:8]}") await conn.execute( """INSERT INTO ov_api_objects(id, collection, name, status, data) VALUES($1,$2,$3,$4,$5::jsonb)""", oid, collection, name, str(body.get("status") or default_status), json.dumps(body), ) row = await conn.fetchrow("SELECT * FROM ov_api_objects WHERE id=$1", oid) return respond( request, element=element, data=generic_entity(collection, element, row), status_code=201 ) if len(parts) == 2: oid = parts[1] row = await conn.fetchrow( """SELECT * FROM ov_api_objects WHERE id=$1::uuid AND collection=$2 AND parent_id IS NULL""", oid, collection, ) if method == "GET": if row is None: raise OVirtError("NotFound", f"{element} not found", status_code=404) return respond(request, element=element, data=generic_entity(collection, element, row)) if method == "PUT": if row is None: raise OVirtError("NotFound", f"{element} not found", status_code=404) body = unwrap_entity(payload, element) data = dict(json.loads(row["data"]) if isinstance(row["data"], str) else row["data"] or {}) data.update(body) await conn.execute( """UPDATE ov_api_objects SET name=COALESCE($2,name), data=$3::jsonb, updated_at=now() WHERE id=$1::uuid""", oid, body.get("name"), json.dumps(data), ) row = await conn.fetchrow("SELECT * FROM ov_api_objects WHERE id=$1::uuid", oid) return respond(request, element=element, data=generic_entity(collection, element, row)) if method == "DELETE": await conn.execute( """DELETE FROM ov_api_objects WHERE id=$1::uuid AND collection=$2 AND parent_id IS NULL""", oid, collection, ) return Response(status_code=200) if len(parts) == 3 and method == "POST": from app.ovirt.jobs import respond_action return await respond_action( request, conn, description=f"{collection} {parts[2]}" ) if len(parts) >= 3: return await handle_subcollection( request, conn, method, collection, parts[1], parts[2], parts[3:], payload ) raise OVirtError("NotFound", f"No handler for /{'/'.join(parts)}", status_code=404) _PARENT_OBJECT_TYPE: dict[str, str] = { "vms": "vm", "hosts": "host", "disks": "disk", "datacenters": "data_center", "clusters": "cluster", "networks": "network", "storagedomains": "storage_domain", "templates": "template", "users": "user", "groups": "group", "vnicprofiles": "vnic_profile", "vmpools": "vm_pool", } async def handle_subcollection( request: Request, conn: Connection, method: str, parent_collection: str, parent_id: str, sub: str, rest: list[str], payload: dict[str, Any], ) -> Response: from app.ovirt.settings import OPT_DEFAULT_API_OBJECT_STATUS, option_value element, _catalog_status = _meta(sub) default_status = await option_value(conn, OPT_DEFAULT_API_OBJECT_STATUS) collection_key = sub if sub == "permissions" and method == "GET": object_type = _PARENT_OBJECT_TYPE.get(parent_collection, parent_collection.rstrip("s")) def _perm_item(r: Any) -> dict[str, Any]: return { "id": str(r["id"]), "href": f"/ovirt-engine/api/{parent_collection}/{parent_id}/permissions/{r['id']}", "role": {"id": str(r["role_id"]), "name": r["role_name"]}, "object": {"type": r["object_type"], "id": str(parent_id)}, **( {"user": {"id": str(r["user_id"]), "name": r["user_name"]}} if r["user_id"] is not None else {} ), } if not rest: rows = await conn.fetch( """SELECT p.*, r.name AS role_name, u.name AS user_name FROM ov_permissions p JOIN ov_roles r ON r.id = p.role_id LEFT JOIN ov_users u ON u.id = p.user_id WHERE p.object_type=$1 AND p.object_id=$2::uuid ORDER BY r.name""", object_type, parent_id, ) return respond( request, element="permission", collection="permissions", data=[_perm_item(r) for r in rows], ) if len(rest) == 1: r = await conn.fetchrow( """SELECT p.*, r.name AS role_name, u.name AS user_name FROM ov_permissions p JOIN ov_roles r ON r.id = p.role_id LEFT JOIN ov_users u ON u.id = p.user_id WHERE p.id=$1::uuid AND p.object_type=$2 AND p.object_id=$3::uuid""", rest[0], object_type, parent_id, ) if r is None: raise OVirtError("NotFound", "permission not found", status_code=404) return respond(request, element="permission", data=_perm_item(r)) if sub == "tags" and method == "GET": object_type = _PARENT_OBJECT_TYPE.get(parent_collection, parent_collection.rstrip("s")) if not rest: rows = await conn.fetch( """SELECT t.* FROM ov_tag_assignments a JOIN ov_tags t ON t.id = a.tag_id WHERE a.object_type=$1 AND a.object_id=$2::uuid ORDER BY t.name""", object_type, parent_id, ) items = [ { "id": str(r["id"]), "href": f"/ovirt-engine/api/tags/{r['id']}", "name": r["name"], "description": r["description"] or "", } for r in rows ] return respond(request, element="tag", collection="tags", data=items) if len(rest) == 1: r = await conn.fetchrow( """SELECT t.* FROM ov_tag_assignments a JOIN ov_tags t ON t.id = a.tag_id WHERE a.object_type=$1 AND a.object_id=$2::uuid AND t.id=$3::uuid""", object_type, parent_id, rest[0], ) if r is None: raise OVirtError("NotFound", "tag not found", status_code=404) return respond( request, element="tag", data={ "id": str(r["id"]), "href": f"/ovirt-engine/api/tags/{r['id']}", "name": r["name"], "description": r["description"] or "", }, ) if not rest: if method == "GET": rows = await conn.fetch( """SELECT * FROM ov_api_objects WHERE collection=$1 AND parent_collection=$2 AND parent_id=$3::uuid ORDER BY name""", collection_key, parent_collection, parent_id, ) items = [ generic_entity(f"{parent_collection}/{parent_id}/{sub}", element, r) for r in rows ] # Fix hrefs for item, row in zip(items, rows, strict=False): item["href"] = f"/ovirt-engine/api/{parent_collection}/{parent_id}/{sub}/{row['id']}" return respond(request, element=element, collection=sub, data=items) if method == "POST": body = unwrap_entity(payload, element) oid = uuid4() name = str(body.get("name") or f"{element}-{oid.hex[:8]}") await conn.execute( """INSERT INTO ov_api_objects(id, collection, name, status, parent_collection, parent_id, data) VALUES($1,$2,$3,$4,$5,$6::uuid,$7::jsonb)""", oid, collection_key, name, str(body.get("status") or default_status), parent_collection, parent_id, json.dumps(body), ) row = await conn.fetchrow("SELECT * FROM ov_api_objects WHERE id=$1", oid) data = generic_entity(f"{parent_collection}/{parent_id}/{sub}", element, row) data["href"] = f"/ovirt-engine/api/{parent_collection}/{parent_id}/{sub}/{oid}" return respond(request, element=element, data=data, status_code=201) if len(rest) == 1: oid = rest[0] if method == "GET": row = await conn.fetchrow( """SELECT * FROM ov_api_objects WHERE id=$1::uuid AND collection=$2 AND parent_id=$3::uuid""", oid, collection_key, parent_id, ) if row is None: raise OVirtError("NotFound", f"{element} not found", status_code=404) data = generic_entity(sub, element, row) data["href"] = f"/ovirt-engine/api/{parent_collection}/{parent_id}/{sub}/{oid}" return respond(request, element=element, data=data) if method == "DELETE": await conn.execute( "DELETE FROM ov_api_objects WHERE id=$1::uuid AND parent_id=$2::uuid", oid, parent_id, ) return Response(status_code=200) if method == "PUT": body = unwrap_entity(payload, element) await conn.execute( "UPDATE ov_api_objects SET data=$2::jsonb, updated_at=now() WHERE id=$1::uuid", oid, json.dumps(body), ) row = await conn.fetchrow("SELECT * FROM ov_api_objects WHERE id=$1::uuid", oid) return respond(request, element=element, data=generic_entity(sub, element, row)) if len(rest) == 2 and method == "POST": from app.ovirt.jobs import respond_action return await respond_action( request, conn, description=f"{parent_collection}/{sub} {rest[1]}" ) raise OVirtError( "NotFound", f"No handler for /{parent_collection}/{parent_id}/{sub}/{'/'.join(rest)}", status_code=404, ) def remount_schema_services(app: Any, series: str) -> int: """Hot-swap contract pack: reload ops and re-register OpenAPI routes.""" from app.ovirt.contract_loader import ensure_loaded from app.ovirt.registry import clear_ovirt_contract_routes, register_ovirt_contract_routes from app.ovirt.routes import engine rt = ensure_loaded(series) summary = rt.reload(series) clear_ovirt_contract_routes(app) registered = 0 if rt.pack is not None: registered = register_ovirt_contract_routes(app, rt.pack) # Re-attach catch-all after specific contract routes (match order matters). app.include_router(engine.router) app.state.ovirt_series = series app.state.ovirt_schema_ops = registered or summary.get("operation_count", 0) app.state.runtime_version = f"ovirt-{series}" return int(app.state.ovirt_schema_ops)