172 lines
6.1 KiB
Python
172 lines
6.1 KiB
Python
#!/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())
|