"""Native vSphere API catalog for the Web UI console. Parameter / request-body metadata comes from the official Automation OpenAPI (``app/vsphere/rest/param_index.json``, generated by ``scripts/generate_vsphere_param_index.py``). Nested ``body_example`` values are flattened into dotted PARAM leaves (``placement.host``, ``cpu.count``, …). Path-parameter examples still use lab seed identifiers so Send works against the seeded inventory. """ from __future__ import annotations import json import re from functools import lru_cache from pathlib import Path from typing import Any from app.vsphere.contracts.matrix import ( VERSIONS, catalog_entries_for_major, is_implemented_for_major, load_bundle, ) from app.vsphere.rest.param_fields import body_fields_from_example, set_by_path _PATH_PARAM = re.compile(r"\{([^{}/]+)\}") _PARAM_INDEX_PATH = Path(__file__).resolve().parents[1] / "rest" / "param_index.json" _PATH_EXAMPLES: dict[str, str] = { "vm": "vm-101", "host": "host-11", "datastore": "datastore-31", "task": "task-1", "snapshot": "snapshot-1", "category_id": "urn:vmomi:InventoryServiceCategory:demo:GLOBAL", "tag_id": "urn:vmomi:InventoryServiceTag:demo:GLOBAL", "item_id": "item-demo", "folder": "group-v23", "datacenter": "datacenter-21", "cluster": "domain-c21", "resource_pool": "resgroup-22", "permission_id": "1", "policy": "policy-default", "library_id": "library-demo", } @lru_cache(maxsize=1) def _param_index() -> dict[str, Any]: if not _PARAM_INDEX_PATH.is_file(): return {"methods": {}} payload = json.loads(_PARAM_INDEX_PATH.read_text(encoding="utf-8")) methods = payload.get("methods") return methods if isinstance(methods, dict) else {} def list_vsphere_majors(*, runtime_version: str | None) -> dict[str, Any]: return { "runtime_version": runtime_version or VERSIONS[9]["version"], "plane": "vsphere-rest", "majors": [ { "major": major, "series": meta["series"], "latest_version": meta["version"], "artifact_url": f"stub://vmware/vsphere-{meta['version']}/api-contract", "bundled": True, } for major, meta in VERSIONS.items() ], } def vsphere_catalog_payload(major: int) -> dict[str, Any]: meta = VERSIONS.get(major) or VERSIONS[9] bundle = load_bundle(major) entries = catalog_entries_for_major(major) grouped: dict[str, dict[str, dict[str, Any]]] = {} for entry in entries: path = entry["path"] parts = [p for p in path.split("/") if p] tag = "/".join(parts[:3]) if len(parts) >= 3 else path by_path = grouped.setdefault(tag, {}) path_entry = by_path.setdefault(path, {"path": path, "methods": []}) path_entry["methods"].append( { "verb": entry["verb"], "name": f"{entry['verb'].lower()}_{parts[-1] if parts else 'root'}", "description": f"{entry['status']} {entry['verb']} {path}", "protected": True, "implemented": entry["status"] in {"implemented", "stub"}, } ) categories = [ { "tag": tag, "paths": sorted(by_path.values(), key=lambda item: item["path"]), } for tag, by_path in sorted(grouped.items()) ] return { "major": major, "series": meta["series"], "source_version": meta["version"], "latest_version": meta["version"], "artifact_url": f"stub://vmware/vsphere-{meta['version']}/api-contract", "bundled": True, "path_count": sum(len(cat["paths"]) for cat in categories), "method_count": len(entries), "categories": categories, "plane": "vsphere-rest", "contract_kind": bundle.get("kind", "stub-openapi-matrix"), } def _field( name: str, *, type_name: str = "string", optional: bool = False, example: Any = None, description: str | None = None, enum: list[str] | None = None, ) -> dict[str, Any]: return { "name": name, "type": type_name, "description": description, "optional": optional, "enum": enum or [], "example": example if example is not None else name, } def _path_fields(path: str) -> list[dict[str, Any]]: fields = [] for name in _PATH_PARAM.findall(path): fields.append( _field( name, optional=False, example=_PATH_EXAMPLES.get(name, name), description=f"Path parameter {{{name}}}", ) ) return fields def _normalize_index_fields(raw_fields: Any) -> list[dict[str, Any]]: if not isinstance(raw_fields, list): return [] fields: list[dict[str, Any]] = [] for item in raw_fields: if not isinstance(item, dict) or not item.get("name"): continue enum = item.get("enum") if isinstance(item.get("enum"), list) else [] fields.append( _field( str(item["name"]), type_name=str(item.get("type") or "string"), optional=bool(item.get("optional", True)), example=item.get("example"), description=item.get("description") if isinstance(item.get("description"), str) else None, enum=[str(value) for value in enum], ) ) return fields def _lookup_param_entry(verb: str, path: str) -> dict[str, Any] | None: methods = _param_index() key = f"{verb.upper()} {path}" entry = methods.get(key) return entry if isinstance(entry, dict) else None def vsphere_method_payload( *, major: int, path: str, verb: str, runtime_version: str | None, ) -> dict[str, Any]: meta = VERSIONS.get(major) or VERSIONS[9] upper = verb.upper() path_fields = _path_fields(path) entry = _lookup_param_entry(upper, path) query_fields: list[dict[str, Any]] = [] body_fields: list[dict[str, Any]] = [] body_example: dict[str, Any] = {} if entry is not None: # Prefer OpenAPI path examples when present, but keep lab seed IDs. indexed_path = _normalize_index_fields(entry.get("path_fields")) if indexed_path: by_name = {field["name"]: field for field in indexed_path} merged_path: list[dict[str, Any]] = [] for field in path_fields: indexed = by_name.get(str(field["name"])) if indexed is None: merged_path.append(field) continue merged = dict(indexed) # Lab seed identifiers beat generic OpenAPI "example" strings. if field["name"] in _PATH_EXAMPLES: merged["example"] = _PATH_EXAMPLES[str(field["name"])] merged_path.append(merged) path_fields = merged_path query_fields = _normalize_index_fields(entry.get("query_fields")) body_fields = _normalize_index_fields(entry.get("body_fields")) raw_example = entry.get("body_example") if isinstance(raw_example, dict): body_example = raw_example # Prefer leaf paths flattened from nested body_example (placement.host, …). nested_fields = body_fields_from_example(body_example) if nested_fields: body_fields = nested_fields elif not body_example and body_fields: # Build a nested example from dotted / JSON-string body fields. built: dict[str, Any] = {} for field in body_fields: if field.get("optional"): continue example = field.get("example") name = str(field["name"]) if isinstance(example, str) and example[:1] in {"{", "["}: try: example = json.loads(example) except json.JSONDecodeError: pass if "." in name: set_by_path(built, name, example) else: built[name] = example body_example = built nested_fields = body_fields_from_example(body_example) if nested_fields: body_fields = nested_fields # Params drawer shows query + body together; keep query_fields distinct for URL build. params_fields = [*body_fields, *query_fields] resolved = path for field in path_fields: resolved = resolved.replace(f"{{{field['name']}}}", str(field["example"])) return { "major": major, "path": path, "verb": upper, "name": path.strip("/").replace("/", "_"), "description": f"{upper} {path}", "resolved_path": resolved, "path_fields": path_fields, "query_fields": query_fields, "body_fields": params_fields, "indexed_fields": [], "body_example": body_example, "implemented": is_implemented_for_major(upper, path, major), "runtime_version": runtime_version or meta["version"], "source_version": meta["version"], "param_source": "openapi" if entry is not None else "none", }