"""DB-backed per-path routes for Broadcom Automation API stubs without deep handlers. Each ``(verb, path)`` from the universe registry is registered with ``APIRouter.add_api_route`` (same idea as Proxmox ``register_contract_routes``), instead of a single ``/api/{full_path:path}`` catch-all. """ from __future__ import annotations import re from collections.abc import Awaitable, Callable from typing import Any from fastapi import APIRouter, Depends, Request, Response from fastapi.responses import JSONResponse from app.db.pool import Database from app.dependencies import get_database from app.vsphere import inventory from app.vsphere.domain import api_state, tagging from app.vsphere.domain import content as content_domain from app.vsphere.rest.coverage import IMPLEMENTED from app.vsphere.security.authz import require_read from app.vsphere.security.session import SessionInfo router = APIRouter(tags=["vSphere REST surface"]) _PARAM_RE = re.compile(r"\{([A-Za-z0-9_]+)\}") def _extract_params(template: str, concrete: str) -> dict[str, str]: pattern = "^" + _PARAM_RE.sub(r"([^/]+)", template) + "$" match = re.match(pattern, concrete) if not match: return {} names = _PARAM_RE.findall(template) return {name: match.group(index + 1) for index, name in enumerate(names)} async def _live_get(database: Database, template: str, concrete: str) -> Any | None: """Return inventory/platform-backed payloads when possible.""" params = _extract_params(template, concrete) if template == "/api/cis/tagging/category": return await tagging.list_categories(database) if template == "/api/cis/tagging/tag": return await tagging.list_tags(database) if template == "/api/content/library": return await content_domain.list_libraries(database) if template == "/api/content/library/item": libs = await content_domain.list_libraries(database) items: list[dict[str, Any]] = [] for lib in libs: items.extend(await content_domain.list_library_items(database, lib["id"])) return items if template == "/api/content/local-library": libs = await content_domain.list_libraries(database) return [lib for lib in libs if lib.get("type") == "LOCAL"] vm = params.get("vm") if vm and "/hardware/" in template: obj = await inventory.require_object(database, vm, type_name="VirtualMachine", resource="vm") props = obj.props or {} def _live_list(key: str) -> list[Any] | None: value = props.get(key) if isinstance(value, list) and value: return list(value) return None # Prefer non-empty inventory props; otherwise fall through to vsphere_api_state. if template.endswith("/hardware/cdrom"): return _live_list("cdroms") if template.endswith("/hardware/floppy"): return _live_list("floppies") if template.endswith("/hardware/serial"): return _live_list("serials") if template.endswith("/hardware/parallel"): return _live_list("parallels") if template.endswith("/hardware/adapter/scsi"): return _live_list("scsi_adapters") if template.endswith("/hardware/adapter/sata"): return _live_list("sata_adapters") if template.endswith("/hardware/adapter/nvme"): return _live_list("nvme_adapters") if template.endswith("/hardware/boot") and isinstance(props.get("boot"), dict): return props["boot"] if template.endswith("/hardware/boot/device"): return _live_list("boot_devices") if template.endswith("/hardware/disk"): return _live_list("disks") or list(props.get("disks") or []) if template.endswith("/hardware/ethernet"): return _live_list("nics") or list(props.get("nics") or []) if "/hardware/disk/" in template and template.endswith("}"): disk_id = params.get("disk") for disk in props.get("disks") or []: if str(disk.get("key") or disk.get("disk")) == str(disk_id): return disk if "/hardware/ethernet/" in template and template.endswith("}"): nic_id = params.get("nic") for nic in props.get("nics") or []: if str(nic.get("key") or nic.get("nic")) == str(nic_id): return nic if vm and "/guest/" in template: obj = await inventory.require_object(database, vm, type_name="VirtualMachine", resource="vm") props = obj.props or {} if template.endswith("/guest/local-filesystem"): if "guest_filesystems" in props: return props["guest_filesystems"] return None identity = props.get("identity") or {} if identity or props.get("guest_OS") or props.get("guest_ip"): return { "name": identity.get("name") or obj.name, "family": "LINUX" if "WIN" not in str(props.get("guest_OS", "")).upper() else "WINDOWS", "full_name": {"name": props.get("guest_OS") or obj.name}, "host_name": identity.get("name") or obj.name, "ip_address": props.get("guest_ip"), } return None host = params.get("host") if host and template.endswith("/networking"): obj = await inventory.require_object(database, host, type_name="HostSystem", resource="host") networking = (obj.props or {}).get("networking") return networking if networking is not None else None if host and "storage-device" in template: obj = await inventory.require_object(database, host, type_name="HostSystem", resource="host") devices = (obj.props or {}).get("storage_devices") return devices if devices is not None else None return None async def _mutate_vm_hardware( database: Database, template: str, concrete: str, verb: str, body: dict[str, Any], ) -> Any | None: params = _extract_params(template, concrete) vm = params.get("vm") if not vm or "/hardware/" not in template: return False obj = await inventory.require_object(database, vm, type_name="VirtualMachine", resource="vm") props = dict(obj.props or {}) if verb == "POST" and template.endswith("/hardware/cdrom"): items = list(props.get("cdroms") or []) key = str(3000 + len(items)) items.append( {"cdrom": key, "label": f"CD/DVD drive {len(items) + 1}", "state": "CONNECTED", **body} ) props["cdroms"] = items await inventory.upsert_object( database, moid=vm, type_name=obj.type, name=obj.name, parent_moid=obj.parent_moid, props=props, ) return key if verb in {"PUT", "PATCH"} and template.endswith("/hardware/boot"): props["boot"] = {**(props.get("boot") or {}), **body} await inventory.upsert_object( database, moid=vm, type_name=obj.type, name=obj.name, parent_moid=obj.parent_moid, props=props, ) return None if verb == "DELETE" and "/hardware/cdrom/" in template: cdrom = params.get("cdrom") props["cdroms"] = [ c for c in (props.get("cdroms") or []) if str(c.get("cdrom")) != str(cdrom) ] await inventory.upsert_object( database, moid=vm, type_name=obj.type, name=obj.name, parent_moid=obj.parent_moid, props=props, ) return None return False # not handled specialized; fall through to api_state async def _dispatch( template: str, verb: str, request: Request, database: Database, ) -> Response: concrete = request.url.path method = verb.upper() raw_body: Any = {} if method in {"POST", "PUT", "PATCH"}: try: raw_body = await request.json() except Exception: raw_body = {} if not isinstance(raw_body, dict): raw_body = {"value": raw_body} if method == "GET": live = await _live_get(database, template, concrete) if live is not None and not api_state.is_empty_payload(live): return JSONResponse(content=live, status_code=200) stored = await api_state.get_payload_or_seed(database, "GET", template) if stored is None or api_state.is_empty_payload(stored): return JSONResponse(content={"path": template, "status": "NOT_SEEDED"}, status_code=404) return JSONResponse(content=stored, status_code=200) hw = await _mutate_vm_hardware(database, template, concrete, method, raw_body) if hw is not False: if isinstance(hw, str): return JSONResponse(content=hw, status_code=201) return Response(status_code=204) if method in {"PUT", "PATCH"}: existing = await api_state.get_payload(database, "GET", template) if isinstance(existing, dict) and isinstance(raw_body, dict): merged = {**existing, **raw_body} elif isinstance(raw_body, dict) and raw_body: merged = raw_body elif existing is not None: merged = existing else: merged = {} await api_state.put_payload(database, "GET", template, merged) return JSONResponse(content=merged, status_code=200) if method == "DELETE": # Soft-delete: restore seed_payload from DB so lab GETs never go empty/404. await api_state.restore_seed_payload(database, "GET", template) if template.endswith("}"): parent = template.rsplit("/", 1)[0] if parent: await api_state.restore_seed_payload(database, "GET", parent) return Response(status_code=204) # POST create / action action = request.query_params.get("action") if action: await api_state.put_payload( database, "GET", template, { "last_action": action, "accepted": True, "path": template, **({} if not raw_body else {"spec": raw_body}), }, ) if action.endswith("Task") or "task" in action.lower(): from app.vsphere.domain import tasks as task_store task_id = await task_store.create_task( database, description=f"{action} {template}", service="com.vmware.vapi", operation=action, status="SUCCEEDED", result={"path": template, "action": action}, ) return JSONResponse(content=task_id, status_code=200) return JSONResponse(content={"status": "SUCCESS", "action": action}, status_code=200) new_id = await api_state.new_id(template.rstrip("/").rsplit("/", 1)[-1].strip("{}") or "id") collection_payload = await api_state.get_payload(database, "GET", template) created = {"id": new_id, "name": raw_body.get("name") or new_id, **raw_body} if isinstance(collection_payload, list): collection_payload = [*collection_payload, created] await api_state.put_payload(database, "GET", template, collection_payload) else: await api_state.put_payload( database, "GET", f"{template}/{{{template.rsplit('/', 1)[-1]}}}", created ) await api_state.put_payload(database, "GET", template, created) return JSONResponse(content=new_id, status_code=201) def _endpoint( template: str, verb: str, ) -> Callable[..., Awaitable[Response]]: async def dispatch( request: Request, database: Database = Depends(get_database), _: SessionInfo = Depends(require_read), ) -> Response: return await _dispatch(template, verb, request, database) dispatch.__name__ = f"vsphere_stub_{verb}_{template.replace('/', '_').strip('_')}" dispatch.__qualname__ = dispatch.__name__ return dispatch def register_stub_routes( target: APIRouter | None = None, *, implemented: dict[tuple[str, str], str] | None = None, ) -> int: """Register one FastAPI route per stub ``(verb, path)`` from the coverage registry. Deep ``CORE_IMPLEMENTED`` handlers are skipped so they keep winning on their dedicated routers. Returns the number of routes added. """ api = target if target is not None else router registry = implemented if implemented is not None else IMPLEMENTED added = 0 for (verb, path), status in sorted(registry.items(), key=lambda item: (item[0][1], item[0][0])): if status != "stub": continue api.add_api_route( path, _endpoint(path, verb), methods=[verb], name=f"vsphere-stub:{verb}:{path}", include_in_schema=True, openapi_extra={"x-vmware-implementation": "stub"}, ) added += 1 return added register_stub_routes()