#!/usr/bin/env python3 """Generate REST universe stubs from Broadcom vSphere Automation operations index. Source: contracts/vsphere/broadcom-9.1-operations-index.txt (scraped from https://developer.broadcom.com/xapis/vsphere-automation-api/latest/operation-index/) Output: app/vsphere/rest/universe.json """ from __future__ import annotations import json import re import sys from collections import Counter, defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[1] INDEX = ROOT / "contracts" / "vsphere" / "broadcom-9.1-operations-index.txt" OUT = ROOT / "app" / "vsphere" / "rest" / "universe.json" # Title-Case service tokens that already imply a hyphenated REST segment. _SERVICE_ALIAS: dict[tuple[str, ...], tuple[str, ...]] = { ("Cis", "Session"): ("session",), ("Content", "LocalLibrary"): ("content", "local-library"), ("Content", "SubscribedLibrary"): ("content", "subscribed-library"), ("Vcenter", "VM"): ("vcenter", "vm"), ("Vcenter", "ResourcePool"): ("vcenter", "resource-pool"), ("Vcenter", "Authorization", "Privileges"): ("vcenter", "privilege"), ("Appliance", "Timesync"): ("appliance", "timesync"), } # Segments that carry a resource id when used as a parent, or as a collection leaf. _ID_NAMES: dict[str, str] = { "vm": "vm", "host": "host", "hosts": "host", "cluster": "cluster", "clusters": "cluster", "datacenter": "datacenter", "datacenters": "datacenter", "folder": "folder", "folders": "folder", "datastore": "datastore", "datastores": "datastore", "network": "network", "networks": "network", "resource-pool": "resource_pool", "library": "library_id", "libraries": "library_id", "local-library": "library_id", "subscribed-library": "library_id", "item": "item_id", "items": "item_id", "category": "category_id", "categories": "category_id", "tag": "tag_id", "tags": "tag_id", "policy": "policy", "policies": "policy", "snapshot": "snapshot", "snapshots": "snapshot", "disk": "disk", "disks": "disk", "ethernet": "nic", "cdrom": "cdrom", "cdroms": "cdrom", "serial": "port", "parallel": "port", "floppy": "floppy", "nvme": "adapter", "sata": "adapter", "scsi": "adapter", "provider": "provider", "providers": "provider", "task": "task", "tasks": "task", "permission": "permission_id", "permissions": "permission_id", "role": "role", "roles": "role", "zone": "zone", "zones": "zone", "project": "project", "projects": "project", "domain": "domain", "domains": "domain", "service": "service", "services": "service", "supervisor": "supervisor", "supervisors": "supervisor", "namespace": "namespace", "namespaces": "namespace", "depot": "depot", "depots": "depot", "component": "component", "components": "component", "image": "image", "images": "image", "draft": "draft", "drafts": "draft", "connection": "connection", "connections": "connection", "vpc": "vpc", "vpcs": "vpc", "subnet": "subnet", "subnets": "subnet", "download-session": "download_session_id", "update-session": "update_session_id", "subscription": "subscription_id", "subscriptions": "subscription_id", "usage": "usage_id", "usages": "usage_id", "library-items": "item_id", "versions": "version", "check-outs": "vm", "trusted-root-chains": "chain", "nodes": "node", "profiles": "profile", "interfaces": "interface", "cores": "core", "commit": "commit", "commits": "commit", } _LEAF_ID_ACTIONS = { "get", "delete", "update", "set", "remove", "forceddelete", "forcedDelete", "forceDelete", } def _to_kebab(token: str) -> str: token = token.replace("_", "-") s = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", token) s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", s) return s.lower() def _parse_ops(text: str) -> list[tuple[str, tuple[str, ...], str]]: ops: list[tuple[str, tuple[str, ...], str]] = [] for raw in text.splitlines(): line = raw.strip() match = re.match(r"^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$", line) if not match: continue method, rest = match.group(1), match.group(2) parts = rest.split() if len(parts) < 2: continue action = parts[-1] service = tuple(parts[:-1]) ops.append((method, service, action)) return ops def _segments(service: tuple[str, ...]) -> list[str]: if service in _SERVICE_ALIAS: return list(_SERVICE_ALIAS[service]) # Prefer Vm over VM when both appear in aliases above. return [_to_kebab(part) for part in service] def _action_base(action: str) -> str: return action.split("$", 1)[0] def _build_path(service: tuple[str, ...], action: str, actions_for_service: set[str]) -> str: action_base = _action_base(action) segs = _segments(service) out: list[str] = [] for index, seg in enumerate(segs): out.append(seg) id_name = _ID_NAMES.get(seg) if not id_name: continue is_last = index == len(segs) - 1 if not is_last: out.append("{" + id_name + "}") continue leaf_actions = {_action_base(a).lower() for a in actions_for_service} collection = bool(leaf_actions & {"list", "create", "add"}) if collection and action_base.lower() in {a.lower() for a in _LEAF_ID_ACTIONS}: out.append("{" + id_name + "}") return "/api/" + "/".join(out) def generate() -> dict: text = INDEX.read_text(encoding="utf-8") ops = _parse_ops(text) by_service: dict[tuple[str, ...], set[str]] = defaultdict(set) for _method, service, action in ops: by_service[service].add(action) routes: dict[str, dict[str, str]] = {} # key: "VERB PATH" for method, service, action in ops: path = _build_path(service, action, by_service[service]) key = f"{method} {path}" # Prefer keeping first-seen; all map to implemented stub. routes.setdefault( key, { "verb": method, "path": path, "service": " ".join(service), "sample_action": action, "status": "stub", }, ) methods = sorted(routes.values(), key=lambda item: (item["path"], item["verb"])) verb_counts = Counter(item["verb"] for item in methods) payload = { "source": "https://developer.broadcom.com/xapis/vsphere-automation-api/latest/operation-index/", "source_label": "vSphere Automation API 9.1 (Latest) operations index", "source_file": str(INDEX.relative_to(ROOT)), "broadcom_operations": len(ops), "broadcom_by_verb": dict(Counter(m for m, _s, _a in ops)), "unique_routes": len(methods), "unique_by_verb": dict(verb_counts), "methods": methods, } OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") return payload def main() -> int: if not INDEX.is_file(): print(f"missing index: {INDEX}", file=sys.stderr) return 1 payload = generate() print( json.dumps( { "out": str(OUT.relative_to(ROOT)), "broadcom_operations": payload["broadcom_operations"], "unique_routes": payload["unique_routes"], "unique_by_verb": payload["unique_by_verb"], }, indent=2, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())