Initial commit: VMware vSphere API simulator scaffold.

Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API
contracts, docs, client examples, and the unit/integration/compatibility
test suite for local client and tooling labs without a real vCenter.
This commit is contained in:
2026-07-18 04:42:11 +03:00
commit f8d3cbdd59
422 changed files with 361335 additions and 0 deletions
+253
View File
@@ -0,0 +1,253 @@
#!/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())