#!/usr/bin/env python3 """Probe every implemented REST path from the coverage registry.""" from __future__ import annotations import json import os import ssl import sys import urllib.error import urllib.request from base64 import b64encode from app.vsphere.rest.coverage import catalog_entries BASE = os.getenv("VSPHERE_BASE", "https://localhost") USER = "administrator@vsphere.local" PASSWORD = "VMware1!" def _ctx() -> ssl.SSLContext | None: if not BASE.startswith("https://"): return None return ssl._create_unverified_context() # noqa: S323 def _concrete(path: str) -> str: import re subs = { "{vm}": "vm-101", "{host}": "host-11", "{datastore}": "datastore-31", "{task}": "task-missing", "{snapshot}": "snapshot-missing", "{category_id}": "missing", "{tag_id}": "missing", "{item_id}": "missing", "{library_id}": "lib-missing", "{folder}": "group-v23", "{datacenter}": "datacenter-21", "{cluster}": "domain-c21", # Disposable id — seed resgroup-22 is protected from DELETE. "{resource_pool}": "resgroup-missing", "{permission_id}": "1", "{policy}": "policy-default", "{disk}": "2000", "{nic}": "4000", "{cdrom}": "3000", "{adapter}": "1000", "{network}": "network-41", } out = path for key, value in subs.items(): out = out.replace(key, value) return re.sub(r"\{([A-Za-z0-9_]+)\}", r"probe-\1", out) def _request(method: str, path: str, *, headers: dict[str, str], data: bytes | None = None) -> int: concrete = _concrete(path) req = urllib.request.Request(f"{BASE}{concrete}", data=data, method=method, headers=headers) try: with urllib.request.urlopen(req, context=_ctx()) as resp: # noqa: S310 return int(resp.status) except urllib.error.HTTPError as error: return int(error.code) def main() -> int: basic = b64encode(f"{USER}:{PASSWORD}".encode()).decode() status = _request("POST", "/api/session", headers={"Authorization": f"Basic {basic}"}) if status not in {200, 201}: print(f"session failed: {status}", file=sys.stderr) return 1 # Re-login to capture body req = urllib.request.Request( f"{BASE}/api/session", method="POST", headers={"Authorization": f"Basic {basic}"}, ) with urllib.request.urlopen(req, context=_ctx()) as resp: # noqa: S310 session = json.loads(resp.read().decode()) headers = {"vmware-api-session-id": session, "Content-Type": "application/json"} failures: list[str] = [] probed = 0 for entry in catalog_entries(): verb = entry["verb"] path = entry["path"] if verb == "DELETE" and path in {"/api/session", "/rest/com/vmware/cis/session"}: continue if ( "{" in path and verb in {"POST", "PATCH", "DELETE"} and "missing" in (path.replace("{vm}", "vm-101")) ): # skip destructive ops on missing ids except GET pass data = b"{}" if verb in {"POST", "PUT", "PATCH"} else None if path.endswith("/power") and verb == "POST": code = _request(verb, path + "?action=start", headers=headers) elif "tag-association" in path and verb == "POST": data = json.dumps( { "action": "list-attached-tags", "tag_id": "x", "object_id": {"type": "VirtualMachine", "id": "vm-101"}, } ).encode() code = _request(verb, path, headers=headers, data=data) else: code = _request(verb, path, headers=headers, data=data) probed += 1 # Accept success, not-found for missing substitutions, or validation errors. if code >= 500: failures.append(f"{verb} {path} -> {code}") continue if verb == "GET" and 200 <= code < 300: # Surface probe reads body via a second request-sized check only for markers. # Re-fetch is avoided: empty GET bodies for session are OK. pass # Re-auth in case any probe request invalidated the session cookie. req = urllib.request.Request( f"{BASE}/api/session", method="POST", headers={"Authorization": f"Basic {basic}"}, ) with urllib.request.urlopen(req, context=_ctx()) as resp: # noqa: S310 session = json.loads(resp.read().decode()) headers = {"vmware-api-session-id": session, "Content-Type": "application/json"} # Spot-check critical inventory payloads are non-empty / non-stub. spot = [ "/api/vcenter/vm", "/api/vcenter/host", "/api/content/library", "/api/cis/tagging/category", "/api/esx/settings/clusters/domain-c21/software", "/api/vcenter/namespace-management/supervisors/supervisor-1/summary", "/api/appliance/access/ssh", "/api/appliance/services", "/api/vcenter/vm/vm-101/hardware/cdrom", ] for path in spot: url = f"{BASE}{_concrete(path)}" req = urllib.request.Request(url, method="GET", headers=headers) try: with urllib.request.urlopen(req, context=_ctx()) as resp: # noqa: S310 body = resp.read().decode("utf-8", errors="replace") code = int(resp.status) except urllib.error.HTTPError as error: body = error.read().decode("utf-8", errors="replace") code = int(error.code) if code >= 400: failures.append(f"GET {path} spot -> {code}") continue if '"stub": true' in body or '"stub":true' in body: failures.append(f"GET {path} spot -> stub marker") continue try: parsed = json.loads(body) except json.JSONDecodeError: failures.append(f"GET {path} spot -> non-json") continue if parsed in ([], {}, None): failures.append(f"GET {path} spot -> empty") print(json.dumps({"probed": probed, "failures": failures}, indent=2)) return 1 if failures else 0 if __name__ == "__main__": raise SystemExit(main())