#!/usr/bin/env python3 """Run Python / Ansible-uri / Terraform-data / Pulumi-style cookbooks against the simulator. Uses only ``requests`` so it works inside the Compose ``dev`` image. Terraform/Ansible CLIs are optional — when present they are invoked too. """ from __future__ import annotations import json import os import shutil import subprocess import sys import tempfile import urllib3 from pathlib import Path import requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) BASE = os.environ.get("VSPHERE_BASE", "https://localhost").rstrip("/") USER = os.environ.get("VSPHERE_USER", "administrator@vsphere.local") PASSWORD = os.environ.get("VSPHERE_PASSWORD", "VMware1!") ROOT = Path(__file__).resolve().parents[1] def _session() -> dict[str, str]: response = requests.post(f"{BASE}/api/session", auth=(USER, PASSWORD), verify=False, timeout=60) response.raise_for_status() return {"vmware-api-session-id": response.json()} def run_python_lifecycle(headers: dict[str, str]) -> dict[str, str]: created = requests.post( f"{BASE}/api/vcenter/vm", headers=headers, json={ "name": "cookbook-py-01", "guest_OS": "OTHER_GUEST_64", "placement": { "folder": "group-v23", "host": "host-11", "datastore": "datastore-31", "resource_pool": "resgroup-22", }, "cpu": {"count": 1}, "memory": {"size_MiB": 512}, }, verify=False, timeout=60, ) created.raise_for_status() vm = created.json() power = requests.post( f"{BASE}/api/vcenter/vm/{vm}/power", params={"action": "start"}, headers=headers, verify=False, timeout=60, ) power.raise_for_status() assert power.json().get("task") # Platform surfaces previously deferred providers = requests.get( f"{BASE}/api/vcenter/identity/providers", headers=headers, verify=False, timeout=60 ) providers.raise_for_status() assert any(p.get("type_id") in {"Oidc", "Saml", "LocalOS"} for p in providers.json()) nsx = requests.get( f"{BASE}/api/vcenter/namespace-management/nsx-tier0-gateway", headers=headers, verify=False, timeout=60, ) nsx.raise_for_status() assert nsx.json() nfc = requests.post( f"{BASE}/sdk", data=""" <_this type="Folder">group-v23 nfc-import-lab """, headers={ **headers, "Content-Type": "text/xml", "Cookie": f'vmware_soap_session="{headers["vmware-api-session-id"]}"', }, verify=False, timeout=60, ) nfc.raise_for_status() assert "task-" in nfc.text and "ImportVApp_TaskResponse" in nfc.text requests.post( f"{BASE}/api/vcenter/vm/{vm}/power", params={"action": "stop"}, headers=headers, verify=False, timeout=60, ).raise_for_status() requests.delete( f"{BASE}/api/vcenter/vm/{vm}", headers=headers, verify=False, timeout=60 ).raise_for_status() return {"python": "ok", "vm": str(vm)} def run_ansible_uri(headers: dict[str, str]) -> dict[str, str]: """Mirror examples/ansible/vsphere_playbook.yml using the same REST calls.""" created = requests.post( f"{BASE}/api/vcenter/vm", headers=headers, json={ "name": "cookbook-ansible-01", "guest_OS": "OTHER_GUEST_64", "placement": { "folder": "group-v23", "host": "host-11", "datastore": "datastore-31", "resource_pool": "resgroup-22", }, "cpu": {"count": 1}, "memory": {"size_MiB": 512}, }, verify=False, timeout=60, ) created.raise_for_status() vm = created.json() for action in ("start", "stop"): requests.post( f"{BASE}/api/vcenter/vm/{vm}/power", params={"action": action}, headers=headers, verify=False, timeout=60, ).raise_for_status() requests.put( f"{BASE}/api/vcenter/vm/{vm}/guest/filesystem", params={"path": "/tmp/ansible-marker"}, headers=headers, json={"content": "ansible-ok"}, verify=False, timeout=60, ).raise_for_status() requests.delete( f"{BASE}/api/vcenter/vm/{vm}", headers=headers, verify=False, timeout=60 ).raise_for_status() result = {"ansible_uri": "ok", "vm": str(vm)} playbook = ROOT / "examples" / "ansible" / "vsphere_playbook.yml" if shutil.which("ansible-playbook") and playbook.is_file(): # Prefer simulator HTTP inside compose if BASE is internal. env_base = BASE.replace("https://localhost", "https://localhost") proc = subprocess.run( [ "ansible-playbook", "-i", str(ROOT / "examples" / "ansible" / "inventory.ini"), str(playbook), "-e", f"vsphere_base={env_base}", "-e", "vm_name=cookbook-ansible-cli-01", ], check=False, capture_output=True, text=True, timeout=180, ) result["ansible_cli"] = "ok" if proc.returncode == 0 else f"failed:{proc.returncode}" if proc.returncode != 0: result["ansible_cli_stderr"] = (proc.stderr or proc.stdout)[-500:] else: result["ansible_cli"] = "skipped" return result def run_pulumi_style(headers: dict[str, str]) -> dict[str, str]: """Mirror examples/pulumi/__main__.py REST ComponentResource flow.""" created = requests.post( f"{BASE}/api/vcenter/vm", headers=headers, json={ "name": "cookbook-pulumi-01", "guest_OS": "OTHER_GUEST_64", "placement": { "folder": "group-v23", "host": "host-11", "datastore": "datastore-31", "resource_pool": "resgroup-22", }, "cpu": {"count": 1}, "memory": {"size_MiB": 512}, }, verify=False, timeout=60, ) created.raise_for_status() vm = created.json() power = requests.post( f"{BASE}/api/vcenter/vm/{vm}/power", params={"action": "start"}, headers=headers, verify=False, timeout=60, ) power.raise_for_status() detail = requests.get(f"{BASE}/api/vcenter/vm/{vm}", headers=headers, verify=False, timeout=60) detail.raise_for_status() requests.post( f"{BASE}/api/vcenter/vm/{vm}/power", params={"action": "stop"}, headers=headers, verify=False, timeout=60, ).raise_for_status() requests.delete( f"{BASE}/api/vcenter/vm/{vm}", headers=headers, verify=False, timeout=60 ).raise_for_status() result = {"pulumi_style": "ok", "vm": str(vm), "name": detail.json().get("name")} if shutil.which("pulumi"): result["pulumi_cli"] = "available" else: result["pulumi_cli"] = "skipped" return result def run_terraform_style(headers: dict[str, str]) -> dict[str, str]: """Validate the inventory lookups Terraform data sources need (SOAP+REST).""" # REST inventory used by many TF plans as complementary checks for path in ( "/api/vcenter/datacenter", "/api/vcenter/cluster", "/api/vcenter/datastore", "/api/vcenter/network", "/api/vcenter/vm?names=web-01", ): response = requests.get(f"{BASE}{path}", headers=headers, verify=False, timeout=60) response.raise_for_status() assert response.json(), path # SOAP FindByInventoryPath + CreateVM (resource path) sid = headers["vmware-api-session-id"] soap_headers = { **headers, "Content-Type": "text/xml", "Cookie": f'vmware_soap_session="{sid}"', } find = requests.post( f"{BASE}/sdk", data=""" <_this type="SearchIndex">SearchIndex /Datacenters/Datacenter/vm/web-01 """, headers=soap_headers, verify=False, timeout=60, ) find.raise_for_status() assert "VirtualMachine" in find.text create = requests.post( f"{BASE}/sdk", data=""" <_this type="Folder">group-v23 cookbook-tf-01 otherGuest64 1 512 [datastore1] resgroup-22 host-11 """, headers=soap_headers, verify=False, timeout=60, ) create.raise_for_status() assert "task-" in create.text result = {"terraform_style": "ok"} tf_dir = ROOT / "examples" / "terraform" / "vsphere" tf_bin = shutil.which("terraform") or ( str(ROOT / ".tools" / "terraform") if (ROOT / ".tools" / "terraform").is_file() else None ) if tf_bin and tf_dir.is_dir(): server = BASE.replace("https://", "").replace("http://", "") # Prefer the checked-out example (keeps .terraform providers) when writable. work = tf_dir if (tf_dir / ".terraform").is_dir() else None tmp_ctx = None if work is None: tmp_ctx = tempfile.TemporaryDirectory() work = Path(tmp_ctx.name) for name in ("main.tf", "variables.tf"): (work / name).write_text( (tf_dir / name).read_text(encoding="utf-8"), encoding="utf-8" ) env = { **os.environ, "TF_VAR_vsphere_server": server, "TF_VAR_vsphere_user": USER, "TF_VAR_vsphere_password": PASSWORD, "TF_VAR_create_lab_vm": "false", } try: if not (work / ".terraform").is_dir(): init = subprocess.run( [tf_bin, "init", "-input=false", "-no-color"], cwd=work, env=env, capture_output=True, text=True, timeout=180, check=False, ) if init.returncode != 0: result["terraform_cli"] = f"init_failed:{(init.stderr or '')[-300:]}" return result plan = subprocess.run( [tf_bin, "plan", "-input=false", "-no-color", "-detailed-exitcode"], cwd=work, env=env, capture_output=True, text=True, timeout=180, check=False, ) # 0 = no changes, 2 = changes present — both OK for data sources result["terraform_cli"] = ( "ok" if plan.returncode in {0, 2} else f"plan_failed:{plan.returncode}" ) if plan.returncode not in {0, 2}: result["terraform_cli_stderr"] = ((plan.stderr or "") + (plan.stdout or ""))[-500:] finally: if tmp_ctx is not None: tmp_ctx.cleanup() else: result["terraform_cli"] = "skipped" return result def main() -> int: headers = _session() report: dict[str, object] = {"base": BASE} failed = False for name, fn in ( ("python", run_python_lifecycle), ("ansible", run_ansible_uri), ("pulumi", run_pulumi_style), ("terraform", run_terraform_style), ): try: report[name] = fn(headers) except Exception as error: # noqa: BLE001 report[name] = {"error": str(error)} failed = True print(json.dumps(report, indent=2)) return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main())