"""Per-major vSphere REST availability matrix (stub OpenAPI stand-in).""" from __future__ import annotations import json import re from functools import lru_cache from pathlib import Path from typing import Any from app.vsphere.rest.coverage import ACTIVE_STATUSES, IMPLEMENTED # Integer majors mirror the console hot-swap ids (6-9). VERSIONS: dict[int, dict[str, str]] = { 6: {"series": "vSphere 7.0", "version": "7.0.0"}, 7: {"series": "vSphere 7.0 U3", "version": "7.0.3"}, 8: {"series": "vSphere 8.0", "version": "8.0.0"}, 9: {"series": "vSphere 8.0 U2", "version": "8.0.2"}, } # Minimum major at which a registered path is considered implemented. # Anything absent defaults to major 9 (latest) so new paths stay opt-in until catalogued. _DEFAULT_FLOOR = 9 PATH_FLOOR: dict[tuple[str, str], int] = { # Always-on core (7.0+) ("POST", "/api/session"): 6, ("DELETE", "/api/session"): 6, ("GET", "/api/session"): 6, ("POST", "/rest/com/vmware/cis/session"): 6, ("GET", "/rest/com/vmware/cis/session"): 6, ("DELETE", "/rest/com/vmware/cis/session"): 6, ("GET", "/api/appliance/system/version"): 6, ("GET", "/api/vcenter/vm"): 6, ("POST", "/api/vcenter/vm"): 6, ("GET", "/api/vcenter/vm/{vm}"): 6, ("DELETE", "/api/vcenter/vm/{vm}"): 6, ("POST", "/api/vcenter/vm/{vm}/power"): 6, ("GET", "/api/vcenter/vm/{vm}/guest/identity"): 6, ("GET", "/api/vcenter/host"): 6, ("GET", "/api/vcenter/host/{host}"): 6, ("GET", "/api/vcenter/datastore"): 6, ("GET", "/api/vcenter/datastore/{datastore}"): 6, ("GET", "/api/vcenter/network"): 6, ("GET", "/api/vcenter/datacenter"): 6, ("GET", "/api/vcenter/cluster"): 6, ("GET", "/api/vcenter/folder"): 6, ("GET", "/api/vcenter/resource-pool"): 6, # 7.0 U3 depth ("GET", "/api/cis/tasks"): 7, ("GET", "/api/cis/tasks/{task}"): 7, ("GET", "/api/vcenter/vm/{vm}/tools"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware/cpu"): 7, ("PATCH", "/api/vcenter/vm/{vm}/hardware/cpu"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware/memory"): 7, ("PATCH", "/api/vcenter/vm/{vm}/hardware/memory"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware/disk"): 7, ("POST", "/api/vcenter/vm/{vm}/hardware/disk"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware/ethernet"): 7, ("POST", "/api/vcenter/vm/{vm}/hardware/ethernet"): 7, ("GET", "/api/vcenter/vm/{vm}/hardware/boot"): 7, ("GET", "/api/vcenter/vm/{vm}/snapshots"): 7, ("POST", "/api/vcenter/vm/{vm}/snapshots"): 7, ("DELETE", "/api/vcenter/vm/{vm}/snapshots/{snapshot}"): 7, ("POST", "/api/vcenter/vm/{vm}/snapshots/{snapshot}"): 7, ("POST", "/api/vcenter/vm/{vm}/clone"): 7, ("POST", "/api/vcenter/vm/{vm}/relocate"): 7, ("POST", "/api/vcenter/host/{host}/maintenance"): 7, ("GET", "/api/vcenter/datastore/{datastore}/files"): 7, ("POST", "/api/vcenter/datastore/{datastore}/files"): 7, ("POST", "/api/vcenter/datacenter"): 7, ("DELETE", "/api/vcenter/datacenter/{datacenter}"): 7, ("POST", "/api/vcenter/cluster"): 7, ("DELETE", "/api/vcenter/cluster/{cluster}"): 7, ("POST", "/api/vcenter/folder"): 7, ("POST", "/api/vcenter/folder/{folder}"): 7, ("DELETE", "/api/vcenter/folder/{folder}"): 7, ("POST", "/api/vcenter/resource-pool"): 7, ("DELETE", "/api/vcenter/resource-pool/{resource_pool}"): 7, ("GET", "/api/cis/tagging/category"): 7, ("POST", "/api/cis/tagging/category"): 7, ("GET", "/api/cis/tagging/category/{category_id}"): 7, ("DELETE", "/api/cis/tagging/category/{category_id}"): 7, ("GET", "/api/cis/tagging/tag"): 7, ("POST", "/api/cis/tagging/tag"): 7, ("GET", "/api/cis/tagging/tag/{tag_id}"): 7, ("DELETE", "/api/cis/tagging/tag/{tag_id}"): 7, ("POST", "/api/cis/tagging/tag-association"): 7, # 8.0 platform services ("GET", "/api/appliance/health/system"): 8, ("GET", "/api/appliance/networking"): 8, ("GET", "/api/appliance/timesync"): 8, ("GET", "/api/vcenter/network/dvs"): 8, ("POST", "/api/vcenter/network/dvs"): 8, ("POST", "/api/vcenter/network/dvpg"): 8, ("GET", "/api/content/library"): 8, ("POST", "/api/content/local-library"): 8, ("GET", "/api/content/library/item"): 8, ("POST", "/api/content/library/item"): 8, ("POST", "/api/vcenter/ovf/library-item/{item_id}"): 8, ("GET", "/api/vcenter/storage/policies"): 8, ("GET", "/api/vcenter/storage/policies/{policy}/vm"): 8, ("GET", "/api/vcenter/privilege"): 8, ("GET", "/api/vcenter/authorization/roles"): 8, ("GET", "/api/vcenter/authorization/permissions"): 8, ("POST", "/api/vcenter/authorization/permissions"): 8, ("DELETE", "/api/vcenter/authorization/permissions/{permission_id}"): 8, ("GET", "/api/vcenter/identity/providers"): 8, ("GET", "/api/vcenter/certificate-management/vcenter/tls"): 9, ("GET", "/api/vcenter/vm/{vm}/guest/networking"): 7, ("GET", "/api/vcenter/vm/{vm}/guest/power"): 7, ("POST", "/api/vcenter/vm/{vm}/guest/power"): 7, ("POST", "/api/vcenter/vm/{vm}/tools"): 7, ("POST", "/api/vcenter/vm/{vm}/console/tickets"): 8, ("POST", "/api/vcenter/vm/{vm}/guest/customization"): 8, ("POST", "/api/vcenter/vm/{vm}"): 7, ("GET", "/api/vcenter/host/{host}/storage/storage-device"): 8, ("GET", "/api/vcenter/host/{host}/networking"): 8, ("GET", "/api/vcenter/folder/{folder}/children"): 7, ("GET", "/api/vapi/metadata/metamodel/service"): 8, ("GET", "/api/vapi/metadata/authentication/component"): 8, ("GET", "/api/vcenter/activity-history"): 8, ("GET", "/rest/vcenter/vm"): 6, ("GET", "/rest/vcenter/vm/{vm}"): 6, ("POST", "/rest/vcenter/vm/{vm}/power"): 6, ("GET", "/rest/vcenter/host"): 6, ("GET", "/rest/vcenter/datastore"): 6, ("GET", "/rest/vcenter/network"): 6, ("GET", "/rest/vcenter/datacenter"): 6, ("GET", "/rest/vcenter/cluster"): 6, ("GET", "/rest/appliance/system/version"): 6, } def floor_for(verb: str, path: str) -> int: return PATH_FLOOR.get((verb.upper(), path), _DEFAULT_FLOOR) def methods_for_major(major: int) -> dict[tuple[str, str], str]: active = major if major in VERSIONS else 9 return { key: status for key, status in IMPLEMENTED.items() if floor_for(key[0], key[1]) <= active } def catalog_entries_for_major(major: int) -> list[dict[str, str]]: return [ {"verb": verb, "path": path, "status": status} for (verb, path), status in sorted(methods_for_major(major).items()) ] def is_implemented_for_major(verb: str, path: str, major: int) -> bool: return methods_for_major(major).get((verb.upper(), path)) in ACTIVE_STATUSES @lru_cache(maxsize=1) def _compiled_routes() -> list[tuple[str, re.Pattern[str], str, int, int]]: compiled: list[tuple[str, re.Pattern[str], str, int, int]] = [] for verb, path in IMPLEMENTED: pattern = "^" + re.sub(r"\{[^/]+\}", r"[^/]+", path) + "$" parts = [part for part in path.split("/") if part] static = sum(1 for part in parts if not (part.startswith("{") and part.endswith("}"))) compiled.append((verb, re.compile(pattern), path, static, len(path))) # Prefer more literal segments so /library/item beats /library/{library_id}. compiled.sort(key=lambda item: (item[0], -item[3], -item[4], item[2])) return compiled def resolve_template(verb: str, request_path: str) -> str | None: method = verb.upper() for route_verb, pattern, template, _static, _length in _compiled_routes(): if route_verb == method and pattern.match(request_path): return template return None def available_for_request(verb: str, request_path: str, major: int) -> bool | None: """True = allowed, False = known but wrong version, None = not in registry. Lab policy: every registered Automation API method is always served with real (DB-backed) handlers/stubs regardless of the hot-swapped catalog major. Catalog browse still uses ``methods_for_major`` / PATH_FLOOR for history. """ del major # major retained for call-site compatibility; gating is catalog-only template = resolve_template(verb, request_path) if template is None: return None return True def bundle_payload(major: int) -> dict[str, Any]: meta = VERSIONS.get(major) or VERSIONS[9] entries = catalog_entries_for_major(major) return { "product": "vmware-api-simulator", "plane": "vsphere-rest", "major": major, "series": meta["series"], "version": meta["version"], "kind": "stub-openapi-matrix", "notes": ( "Stub contract derived from app/vsphere/rest/coverage.py + PATH_FLOOR. " "Not a Broadcom OpenAPI dump; used for catalog browse and hot-swap gating." ), "method_count": len(entries), "methods": entries, } def bundles_root() -> Path: return Path(__file__).resolve().parents[3] / "contracts" / "vsphere" def write_bundles(root: Path | None = None) -> list[Path]: base = root or bundles_root() written: list[Path] = [] for major in sorted(VERSIONS): meta = VERSIONS[major] directory = base / meta["version"] directory.mkdir(parents=True, exist_ok=True) payload = bundle_payload(major) path = directory / "manifest.json" path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") written.append(path) (base / "README.md").write_text( "# vSphere stub contracts\n\n" "Versioned JSON matrices generated from `app/vsphere/contracts/matrix.py`.\n" "Hot-swap (`POST /ui/api/contract/apply?major=N`) switches the catalog major " "for UI browse/evidence. Runtime always serves the full registered surface " "(no HTTP 501 version gate on known paths).\n", encoding="utf-8", ) return written def load_bundle(major: int) -> dict[str, Any]: meta = VERSIONS.get(major) or VERSIONS[9] path = bundles_root() / meta["version"] / "manifest.json" if path.is_file(): return json.loads(path.read_text(encoding="utf-8")) return bundle_payload(major)