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,92 @@
|
||||
"""Minimal PBM (Profile-Based Management) SOAP endpoint for Terraform/govmomi."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
router = APIRouter(tags=["vSphere PBM"])
|
||||
|
||||
NS_SOAP = "http://schemas.xmlsoap.org/soap/envelope/"
|
||||
|
||||
|
||||
@router.get("/pbm")
|
||||
@router.get("/pbm/")
|
||||
@router.get("/pbm/sdk")
|
||||
@router.get("/pbm/sdk/")
|
||||
async def pbm_get() -> PlainTextResponse:
|
||||
return PlainTextResponse("VMware PBM SDK simulator — POST SOAP to /pbm/sdk")
|
||||
|
||||
|
||||
@router.post("/pbm")
|
||||
@router.post("/pbm/")
|
||||
@router.post("/pbm/sdk")
|
||||
@router.post("/pbm/sdk/")
|
||||
async def pbm_post(request: Request) -> Response:
|
||||
body = (await request.body()).decode("utf-8", errors="replace")
|
||||
if "PbmRetrieveServiceContent" in body or "RetrieveContent" in body:
|
||||
xml = _wrap(
|
||||
"PbmRetrieveServiceContentResponse",
|
||||
"""<returnval>
|
||||
<about>
|
||||
<name>VMware vCenter Profile-Driven Storage Service</name>
|
||||
<version>2.0</version>
|
||||
</about>
|
||||
<sessionManager type="PbmSessionManager">SessionManager</sessionManager>
|
||||
<capabilityMetadataManager type="PbmCapabilityMetadataManager">CapabilityMetadataManager</capabilityMetadataManager>
|
||||
<profileManager type="PbmProfileProfileManager">ProfileManager</profileManager>
|
||||
<complianceManager type="PbmComplianceManager">ComplianceManager</complianceManager>
|
||||
<placementSolver type="PbmPlacementSolver">PlacementSolver</placementSolver>
|
||||
</returnval>""",
|
||||
)
|
||||
return Response(content=xml, media_type='text/xml; charset="utf-8"')
|
||||
if "PbmQueryProfile" in body or "PbmQueryDefaultRequirementProfile" in body:
|
||||
xml = _wrap(
|
||||
"PbmQueryProfileResponse",
|
||||
"""<returnval>
|
||||
<uniqueId>com.vmware.storage.default</uniqueId>
|
||||
<name>vSAN Default Storage Policy</name>
|
||||
</returnval>
|
||||
<returnval>
|
||||
<uniqueId>policy-thin</uniqueId>
|
||||
<name>Thin provision</name>
|
||||
</returnval>""",
|
||||
)
|
||||
return Response(content=xml, media_type='text/xml; charset="utf-8"')
|
||||
if "PbmQueryAssociatedProfile" in body:
|
||||
# Empty association list (no storage policy) — must use the correct response
|
||||
# tag so govmomi unmarshals []types.PbmProfileId instead of panicking.
|
||||
xml = _wrap("PbmQueryAssociatedProfileResponse", "")
|
||||
return Response(content=xml, media_type='text/xml; charset="utf-8"')
|
||||
if "PbmRetrieveContent" in body:
|
||||
xml = _wrap("PbmRetrieveContentResponse", "<returnval></returnval>")
|
||||
return Response(content=xml, media_type='text/xml; charset="utf-8"')
|
||||
# Unknown PBM op — empty success so clients continue.
|
||||
op = "PbmMethod"
|
||||
match = re.search(r"<(?:\w+:)?([A-Za-z0-9_]+)(?:\s|>)", body)
|
||||
if match:
|
||||
op = match.group(1)
|
||||
return Response(
|
||||
content=_wrap(f"{op}Response", "<returnval></returnval>"),
|
||||
media_type='text/xml; charset="utf-8"',
|
||||
)
|
||||
|
||||
|
||||
def _wrap(response_tag: str, inner: str) -> str:
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soapenv:Envelope xmlns:soapenv="{NS_SOAP}">
|
||||
<soapenv:Body>
|
||||
<{response_tag} xmlns="urn:pbm">
|
||||
{inner}
|
||||
</{response_tag}>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
"""
|
||||
|
||||
|
||||
# silence unused escape import warning by using it in fault helper
|
||||
def _fault(message: str) -> str:
|
||||
return f"<faultstring>{escape(message)}</faultstring>"
|
||||
Reference in New Issue
Block a user