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,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 = 6–9)
|
||||
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())
|
||||
Reference in New Issue
Block a user