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:
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail if any registered GET returns 501, stub marker, or empty JSON body."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from base64 import b64encode
|
||||
|
||||
from app.vsphere.rest.coverage import IMPLEMENTED
|
||||
|
||||
BASE = os.getenv("VSPHERE_BASE", "https://localhost")
|
||||
USER = os.getenv("VSPHERE_USER", "administrator@vsphere.local")
|
||||
PASSWORD = os.getenv("VSPHERE_PASSWORD", "VMware1!")
|
||||
|
||||
_SUBS = {
|
||||
"{vm}": "vm-101",
|
||||
"{host}": "host-11",
|
||||
"{datastore}": "datastore-31",
|
||||
"{task}": "task-1",
|
||||
"{snapshot}": "snapshot-missing",
|
||||
"{category_id}": "cat-lab-1",
|
||||
"{tag_id}": "tag-lab-1",
|
||||
"{item_id}": "item-ubuntu",
|
||||
"{library_id}": "lib-local-1",
|
||||
"{folder}": "group-v23",
|
||||
"{datacenter}": "datacenter-21",
|
||||
"{cluster}": "domain-c21",
|
||||
"{resource_pool}": "resgroup-22",
|
||||
"{permission_id}": "1",
|
||||
"{policy}": "policy-default",
|
||||
"{disk}": "2000",
|
||||
"{nic}": "4000",
|
||||
"{cdrom}": "3000",
|
||||
"{adapter}": "1000",
|
||||
"{network}": "network-41",
|
||||
"{supervisor}": "supervisor-1",
|
||||
"{commit}": "commit-lab-1",
|
||||
"{domain}": "lab.local",
|
||||
"{interface}": "nic0",
|
||||
"{service}": "vpxd",
|
||||
"{session_id}": "session-lab-1",
|
||||
"{download_session_id}": "session-lab-1",
|
||||
"{update_session_id}": "session-lab-1",
|
||||
"{provider}": "vsphere.local",
|
||||
"{role}": "ReadOnly",
|
||||
"{chain}": "chain-1",
|
||||
"{floppy}": "8000",
|
||||
"{port}": "9000",
|
||||
}
|
||||
|
||||
|
||||
def _ctx() -> ssl.SSLContext | None:
|
||||
if not BASE.startswith("https://"):
|
||||
return None
|
||||
return ssl._create_unverified_context() # noqa: S323
|
||||
|
||||
|
||||
def _concrete(path: str) -> str:
|
||||
out = path
|
||||
for key, value in _SUBS.items():
|
||||
out = out.replace(key, value)
|
||||
return re.sub(r"\{([A-Za-z0-9_]+)\}", r"lab-\1", out)
|
||||
|
||||
|
||||
def _request(method: str, path: str, *, headers: dict[str, str]) -> tuple[int, str]:
|
||||
req = urllib.request.Request(f"{BASE}{path}", method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=_ctx(), timeout=60) as resp: # noqa: S310
|
||||
return int(resp.status), resp.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as error:
|
||||
return int(error.code), error.read().decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
basic = b64encode(f"{USER}:{PASSWORD}".encode()).decode()
|
||||
code, body = _request("POST", "/api/session", headers={"Authorization": f"Basic {basic}"})
|
||||
if code not in {200, 201}:
|
||||
print(json.dumps({"error": f"session failed {code}", "body": body[:200]}))
|
||||
return 1
|
||||
session = json.loads(body)
|
||||
headers = {"vmware-api-session-id": session, "Accept": "application/json"}
|
||||
failures: list[dict[str, object]] = []
|
||||
ok = 0
|
||||
checked = 0
|
||||
for (verb, path), _status in sorted(IMPLEMENTED.items()):
|
||||
if verb != "GET" or path in {"/api/session", "/rest/com/vmware/cis/session"}:
|
||||
continue
|
||||
checked += 1
|
||||
concrete = _concrete(path)
|
||||
if path == "/api/content/library/item":
|
||||
concrete = f"{concrete}?library_id=lib-local-1"
|
||||
status, raw = _request("GET", concrete, headers=headers)
|
||||
if status == 501:
|
||||
failures.append({"path": path, "status": status, "reason": "version gate 501"})
|
||||
continue
|
||||
if status >= 500:
|
||||
failures.append(
|
||||
{"path": path, "status": status, "reason": "server error", "body": raw[:160]}
|
||||
)
|
||||
continue
|
||||
if status not in {200, 201}:
|
||||
# Missing probe ids may 404 — still require a JSON error body.
|
||||
if not raw.strip():
|
||||
failures.append({"path": path, "status": status, "reason": "empty error body"})
|
||||
continue
|
||||
if not raw.strip():
|
||||
failures.append({"path": path, "status": status, "reason": "empty body"})
|
||||
continue
|
||||
if '"stub": true' in raw or '"stub":true' in raw:
|
||||
failures.append({"path": path, "status": status, "reason": "stub marker"})
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
failures.append(
|
||||
{"path": path, "status": status, "reason": "non-json", "body": raw[:160]}
|
||||
)
|
||||
continue
|
||||
if payload in ([], {}, None, "") or (
|
||||
isinstance(payload, (list, dict)) and len(payload) == 0
|
||||
):
|
||||
failures.append(
|
||||
{"path": path, "status": status, "reason": "empty json", "body": raw[:160]}
|
||||
)
|
||||
continue
|
||||
if isinstance(payload, dict):
|
||||
for key in ("data", "value", "messages", "items", "results"):
|
||||
if key in payload and payload[key] in ([], None, {}):
|
||||
failures.append(
|
||||
{
|
||||
"path": path,
|
||||
"status": status,
|
||||
"reason": f"empty nested {key}",
|
||||
"body": raw[:160],
|
||||
}
|
||||
)
|
||||
break
|
||||
else:
|
||||
ok += 1
|
||||
continue
|
||||
ok += 1
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"checked": checked,
|
||||
"ok": ok,
|
||||
"failure_count": len(failures),
|
||||
"failures": failures[:80],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user