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
+398
View File
@@ -0,0 +1,398 @@
#!/usr/bin/env python3
"""Hybrid pulumi-tests suite: pulumi-vsphere + full REST matrix + CRUD + SOAP WSDL ops."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import traceback
import xml.etree.ElementTree as ET
from datetime import UTC, datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / "lib"))
from assert_nonempty import assert_nonempty # noqa: E402
from report_html import write_report # noqa: E402
SMOKE = os.environ.get("TEST_SMOKE", "").lower() in {"1", "true", "yes"}
REPORT_DIR = Path(os.environ.get("REPORT_DIR", "/reports"))
JUNIT_PATH = Path(os.environ.get("REPORT_PATH", str(REPORT_DIR / "pulumi-junit.xml")))
HTML_PATH = Path(os.environ.get("HTML_REPORT_PATH", str(REPORT_DIR / "pulumi-report.html")))
JSON_PATH = Path(os.environ.get("JSON_REPORT_PATH", str(REPORT_DIR / "pulumi-summary.json")))
PROGRAMS = ROOT / "programs"
# Ensure VSPHERE_BASE for HTTP probes when only VSPHERE_SERVER is set (compose).
if not os.environ.get("VSPHERE_BASE") and os.environ.get("VSPHERE_SERVER"):
server = os.environ["VSPHERE_SERVER"]
if not server.startswith("http://") and not server.startswith("https://"):
os.environ["VSPHERE_BASE"] = f"https://{server}"
PULUMI_CASES = [
{
"id": "PU-INV",
"title": "Inventory data sources (dc/cluster/ds/net/vm/host/folder/pool)",
"dir": "inventory",
"smoke": True,
"required": [
"datacenter_id",
"datacenter_name",
"datastore_id",
"datastore_name",
"cluster_id",
"cluster_name",
"resource_pool_id",
"resource_pool_name",
"cluster_resource_pool_id",
"network_id",
"network_name",
"vm_id",
"vm_name",
"host_id",
"host_name",
"folder_id",
"folder_path",
],
},
{
"id": "PU-FOLDER",
"title": "Folder create/destroy",
"dir": "folders",
"smoke": False,
"required": ["folder_id", "folder_path", "datacenter_id"],
},
{
"id": "PU-VM",
"title": "VirtualMachine create/destroy",
"dir": "vm_lifecycle",
"smoke": False,
"required": [
"lab_vm_id",
"lab_vm_name",
"resource_pool_id",
"datastore_id",
"network_id",
],
},
{
"id": "PU-TAG",
"title": "TagCategory + Tag create/destroy",
"dir": "tags",
"smoke": False,
"required": ["category_id", "category_name", "tag_id", "tag_name"],
},
]
def _ensure_program_deps() -> None:
"""Install per-program requirements once (image usually already has them)."""
seen: set[str] = set()
for case in PULUMI_CASES:
req = PROGRAMS / case["dir"] / "requirements.txt"
key = str(req.resolve()) if req.exists() else ""
if not key or key in seen:
continue
seen.add(key)
subprocess.run(
[sys.executable, "-m", "pip", "install", "-q", "-r", str(req)],
check=False,
capture_output=True,
)
def _run_pulumi_case(case: dict) -> dict:
work_dir = PROGRAMS / case["dir"]
if not (work_dir / "__main__.py").is_file():
return {
"id": case["id"],
"title": case["title"],
"status": "failed",
"error": f"missing program {work_dir}",
"outputs": {},
}
try:
from pulumi import automation as auto
except Exception as exc: # noqa: BLE001
return {
"id": case["id"],
"title": case["title"],
"status": "failed",
"error": f"pulumi automation unavailable: {exc}",
"outputs": {},
}
os.environ.setdefault("PULUMI_CONFIG_PASSPHRASE", "lab")
os.environ.setdefault("PULUMI_BACKEND_URL", "file:///tmp/pulumi-state")
Path("/tmp/pulumi-state").mkdir(parents=True, exist_ok=True)
stack_name = f"{case['id'].lower()}-{os.environ.get('TEST_RUN_ID', 'lab')[:8]}"
try:
stack = auto.create_or_select_stack(stack_name=stack_name, work_dir=str(work_dir))
try:
result = stack.up(on_output=lambda *_: None)
outputs = {k: v.value for k, v in (result.outputs or {}).items()}
errors = assert_nonempty(outputs, required=case["required"])
if errors:
return {
"id": case["id"],
"title": case["title"],
"status": "failed",
"error": "; ".join(errors),
"outputs": outputs,
}
return {
"id": case["id"],
"title": case["title"],
"status": "passed",
"error": "",
"outputs": outputs,
}
finally:
try:
stack.destroy(on_output=lambda *_: None)
except Exception:
pass
try:
stack.workspace.remove_stack(stack_name)
except Exception:
pass
except Exception as exc: # noqa: BLE001
return {
"id": case["id"],
"title": case["title"],
"status": "failed",
"error": str(exc)[-3000:],
"outputs": {},
}
def _run_rest_matrix() -> dict:
from rest_matrix import run_rest_matrix
majors = [9] if SMOKE else [6, 7, 8, 9]
try:
summary = run_rest_matrix(majors=majors)
except Exception as exc: # noqa: BLE001
return {
"id": "PU-REST",
"title": f"REST full matrix majors={majors}",
"status": "failed",
"error": f"{exc}\n{traceback.format_exc()[-1500:]}",
"outputs": {},
"rest": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
}
status = "passed" if summary.get("ok") else "failed"
err = ""
if not summary.get("ok"):
sample = summary.get("failures") or []
err = f"REST matrix failures={summary.get('failed')} total={summary.get('total')}; "
err += "; ".join(f"{f.get('verb')} {f.get('path')}{f.get('status')}" for f in sample[:8])
return {
"id": "PU-REST",
"title": f"REST IMPLEMENTED×majors {majors} (deep+stub response check)",
"status": status,
"error": err,
"outputs": {
"total": summary.get("total"),
"failed": summary.get("failed"),
"by_verb": summary.get("by_verb"),
"majors": [m.get("major") for m in summary.get("majors") or []],
},
"rest": summary,
}
def _run_rest_crud() -> dict:
from rest_crud import run_rest_crud
try:
summary = run_rest_crud()
except Exception as exc: # noqa: BLE001
return {
"id": "PU-CRUD",
"title": "Deep REST CRUD (session/folder/tag/library/vm)",
"status": "failed",
"error": f"{exc}\n{traceback.format_exc()[-1500:]}",
"outputs": {},
"crud": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
}
status = "passed" if summary.get("ok") else "failed"
err = ""
if not summary.get("ok"):
err = "; ".join(
f"{f.get('flow')}/{f.get('step')}: {f.get('error')}"
for f in (summary.get("failures") or [])
)
return {
"id": "PU-CRUD",
"title": "Deep REST CRUD (session/folder/tag/library/vm)",
"status": status,
"error": err,
"outputs": {
"total": summary.get("total"),
"failed": summary.get("failed"),
"flows": [f.get("flow") for f in summary.get("flows") or []],
},
"crud": summary,
}
def _run_soap_ops() -> dict:
from soap_ops import run_soap_ops
try:
summary = run_soap_ops()
except Exception as exc: # noqa: BLE001
return {
"id": "PU-SOAP",
"title": "SOAP WSDL ops (all /sdk operations)",
"status": "failed",
"error": f"{exc}\n{traceback.format_exc()[-1500:]}",
"outputs": {},
"soap": {"total": 0, "failed": 1, "failures": [{"error": str(exc)}]},
}
status = "passed" if summary.get("ok") else "failed"
err = ""
if not summary.get("ok"):
err = "; ".join(
f"{f.get('op')}: {f.get('error')}" for f in (summary.get("failures") or [])[:12]
)
return {
"id": "PU-SOAP",
"title": f"SOAP WSDL ops ({summary.get('wsdl_ops', '?')} operations)",
"status": status,
"error": err,
"outputs": {
"total": summary.get("total"),
"failed": summary.get("failed"),
"wsdl_ops": summary.get("wsdl_ops"),
},
"soap": summary,
}
def main() -> int:
REPORT_DIR.mkdir(parents=True, exist_ok=True)
_ensure_program_deps()
pulumi_cases = [c for c in PULUMI_CASES if c["smoke"]] if SMOKE else PULUMI_CASES
print(
f"pulumi hybrid suite pulumi_cases={len(pulumi_cases)} smoke={SMOKE} "
f"server={os.environ.get('VSPHERE_SERVER', 'api-gateway')} "
f"base={os.environ.get('VSPHERE_BASE', '')}",
flush=True,
)
results: list[dict] = []
failed = 0
for case in pulumi_cases:
print(f"== {case['id']}: {case['title']} ==", flush=True)
result = _run_pulumi_case(case)
results.append(result)
print(f"{case['id']}: {result['status']}", flush=True)
if result["status"] == "failed":
failed += 1
print(result["error"][:800], flush=True)
# REST matrix (smoke = major 9 only; full = 69)
print("== PU-REST: REST matrix ==", flush=True)
rest_result = _run_rest_matrix()
results.append(rest_result)
print(f"PU-REST: {rest_result['status']}", flush=True)
if rest_result["status"] == "failed":
failed += 1
print(rest_result["error"][:800], flush=True)
rest_summary = rest_result.get("rest") or {}
crud_summary: dict = {}
soap_summary: dict = {}
if not SMOKE:
print("== PU-CRUD: deep REST CRUD ==", flush=True)
crud_result = _run_rest_crud()
results.append(crud_result)
crud_summary = crud_result.get("crud") or {}
print(f"PU-CRUD: {crud_result['status']}", flush=True)
if crud_result["status"] == "failed":
failed += 1
print(crud_result["error"][:800], flush=True)
print("== PU-SOAP: WSDL ops ==", flush=True)
soap_result = _run_soap_ops()
results.append(soap_result)
soap_summary = soap_result.get("soap") or {}
print(f"PU-SOAP: {soap_result['status']}", flush=True)
if soap_result["status"] == "failed":
failed += 1
print(soap_result["error"][:800], flush=True)
summary = {
"generated_at": datetime.now(UTC).isoformat(),
"vsphere_server": os.environ.get("VSPHERE_SERVER", "api-gateway"),
"vsphere_base": os.environ.get("VSPHERE_BASE", ""),
"smoke": SMOKE,
"provider": "pulumi-vsphere+rest+soap",
"cases": results,
"total_failed": failed,
"rest": {
"total": rest_summary.get("total", 0),
"failed": rest_summary.get("failed", 0),
"by_verb": rest_summary.get("by_verb"),
"majors": rest_summary.get("majors"),
"failures": rest_summary.get("failures"),
},
"crud": {
"total": crud_summary.get("total", 0),
"failed": crud_summary.get("failed", 0),
"flows": crud_summary.get("flows"),
"failures": crud_summary.get("failures"),
},
"soap": {
"total": soap_summary.get("total", 0),
"failed": soap_summary.get("failed", 0),
"ops": soap_summary.get("ops"),
"failures": soap_summary.get("failures"),
},
}
JSON_PATH.write_text(json.dumps(summary, indent=2, default=str), encoding="utf-8")
write_report(summary, HTML_PATH)
suite = ET.Element(
"testsuite",
name="pulumi-hybrid",
tests=str(len(results)),
failures=str(failed),
)
for result in results:
node = ET.SubElement(
suite,
"testcase",
classname="pulumi-hybrid",
name=f"{result['id']} {result['title']}",
)
if result["status"] == "failed":
ET.SubElement(node, "failure", message=result["error"][:500])
elif result["status"] == "skipped":
ET.SubElement(node, "skipped", message=result.get("error") or "skipped")
ET.ElementTree(suite).write(JUNIT_PATH, encoding="utf-8", xml_declaration=True)
print(f"Wrote {HTML_PATH}", flush=True)
print(f"Wrote {JSON_PATH}", flush=True)
print(f"Wrote {JUNIT_PATH}", flush=True)
print(
f"SUMMARY failed={failed} total={len(results)} "
f"rest.failed={summary['rest']['failed']} "
f"crud.failed={summary['crud']['failed']} "
f"soap.failed={summary['soap']['failed']}",
flush=True,
)
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())