"""Native vSphere API catalog (replaces Proxmox stub catalog in the console).""" from __future__ import annotations import re from typing import Any from app.vsphere.contracts.matrix import ( VERSIONS, catalog_entries_for_major, is_implemented_for_major, load_bundle, ) _PATH_PARAM = re.compile(r"\{([^{}/]+)\}") _PATH_EXAMPLES: dict[str, str] = { "vm": "vm-111", "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", } # Common query/body fields for lab Params drawer (not a full OpenAPI schema). _QUERY_FIELDS: dict[tuple[str, str], list[dict[str, Any]]] = { ("GET", "/api/vcenter/vm"): [ { "name": "names", "type": "array", "optional": True, "example": "app-0011", "description": "Filter by VM name", }, { "name": "hosts", "type": "array", "optional": True, "example": "host-11", "description": "Filter by host", }, { "name": "power_states", "type": "array", "optional": True, "example": "POWERED_ON", "description": "Filter by power state", }, ], ("POST", "/api/vcenter/vm/{vm}/power"): [ { "name": "action", "type": "string", "optional": False, "example": "start", "description": "start|stop|reset|suspend", "enum": ["start", "stop", "reset", "suspend"], }, ], ("POST", "/api/vcenter/folder/{folder}"): [ { "name": "action", "type": "string", "optional": False, "example": "rename", "description": "rename|move", }, ], ("POST", "/api/vcenter/host/{host}/maintenance"): [ { "name": "action", "type": "string", "optional": False, "example": "enter", "description": "enter|exit", }, ], } _BODY_FIELDS: dict[tuple[str, str], list[dict[str, Any]]] = { ("POST", "/api/vcenter/vm"): [ {"name": "name", "type": "string", "optional": False, "example": "lab-vm"}, { "name": "placement", "type": "object", "optional": True, "example": '{"folder":"group-v23","host":"host-11","datastore":"datastore-31"}', }, {"name": "cpu_count", "type": "integer", "optional": True, "example": "2"}, {"name": "memory_size_MiB", "type": "integer", "optional": True, "example": "2048"}, ], ("POST", "/api/vcenter/datacenter"): [ {"name": "name", "type": "string", "optional": False, "example": "Datacenter-2"}, {"name": "folder", "type": "string", "optional": True, "example": "group-d1"}, ], ("POST", "/api/vcenter/cluster"): [ {"name": "name", "type": "string", "optional": False, "example": "Cluster-2"}, {"name": "folder", "type": "string", "optional": True, "example": "group-h23"}, ], ("POST", "/api/vcenter/folder"): [ {"name": "name", "type": "string", "optional": False, "example": "workloads"}, {"name": "parent", "type": "string", "optional": True, "example": "group-v23"}, {"name": "type", "type": "string", "optional": True, "example": "VIRTUAL_MACHINE"}, ], ("POST", "/api/cis/tagging/category"): [ { "name": "create_spec", "type": "object", "optional": False, "example": '{"name":"env","description":"lab","cardinality":"MULTIPLE","associable_types":[]}', }, ], ("POST", "/api/cis/tagging/tag"): [ { "name": "create_spec", "type": "object", "optional": False, "example": '{"name":"prod","category_id":"…"}', }, ], ("POST", "/api/content/local-library"): [ { "name": "create_spec", "type": "object", "optional": False, "example": '{"name":"Templates"}', }, ], } 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 _body_example_from_fields(fields: list[dict[str, Any]]) -> dict[str, Any]: body: dict[str, Any] = {} for field in fields: if field.get("optional"): continue example = field.get("example") if isinstance(example, str) and example.startswith("{"): try: import json body[field["name"]] = json.loads(example) continue except Exception: body[field["name"]] = example continue body[field["name"]] = example return body 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) key = (upper, path) query_or_body = _QUERY_FIELDS.get(key, []) body_fields = list(_BODY_FIELDS.get(key, [])) # Query-style action fields appear as body_fields in the Params UI (same editor). for item in query_or_body: body_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"), enum=list(item.get("enum") or []), ) ) # Generic POST with {path params} but no body schema → offer empty object note via name. if upper in {"POST", "PATCH", "PUT"} and not body_fields and "{" not in path: body_fields.append( _field( "name", optional=True, example="example", description="Primary name field when required by create APIs", ) ) 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, "body_fields": body_fields, "indexed_fields": [], "body_example": _body_example_from_fields(body_fields), "implemented": is_implemented_for_major(upper, path, major), "runtime_version": runtime_version or meta["version"], "source_version": meta["version"], }