252 lines
10 KiB
Python
252 lines
10 KiB
Python
"""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,
|
|
("POST", "/api/cis/tasks"): 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,
|
|
("GET", "/api/content/library/{library_id}"): 8,
|
|
("GET", "/api/content/local-library"): 8,
|
|
("POST", "/api/content/local-library"): 8,
|
|
("GET", "/api/content/local-library/{library_id}"): 8,
|
|
("DELETE", "/api/content/local-library/{library_id}"): 8,
|
|
("GET", "/api/content/library/item"): 8,
|
|
("POST", "/api/content/library/item"): 8,
|
|
("GET", "/api/content/library/item/{library_item_id}"): 8,
|
|
("DELETE", "/api/content/library/item/{library_item_id}"): 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)
|