"""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", """ VMware vCenter Profile-Driven Storage Service 2.0 SessionManager CapabilityMetadataManager ProfileManager ComplianceManager PlacementSolver """, ) return Response(content=xml, media_type='text/xml; charset="utf-8"') if "PbmQueryProfile" in body or "PbmQueryDefaultRequirementProfile" in body: xml = _wrap( "PbmQueryProfileResponse", """ com.vmware.storage.default vSAN Default Storage Policy policy-thin Thin provision """, ) 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", "") 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", ""), media_type='text/xml; charset="utf-8"', ) def _wrap(response_tag: str, inner: str) -> str: return f""" <{response_tag} xmlns="urn:pbm"> {inner} """ # silence unused escape import warning by using it in fault helper def _fault(message: str) -> str: return f"{escape(message)}"