Add a stateful Proxmox API console and broad handler coverage beyond the

initial QEMU slice, backed by imported contracts for majors 6–9.
- Implement durable handlers for access/auth, cluster, LXC, storage, HA,
  firewall, Ceph, SDN, ACME, notifications, pools, mapping, and node ops
- Serve an interactive Web UI with catalog browsing, demo seed controls,
  and OpenAPI/help surfaces
- Bundle PVE 6.4-15, 7.4-16, and 8.4.5 contract revisions alongside 9.2.3
- Support in-memory runtime contract Apply (POST /ui/api/contract/apply)
  so /version and /api2 routes follow the selected major until restart
- Expand seed profiles (including demo-cluster), migrations 007–008, TLS
  gateway config, Compose/Makefile tooling, and compatibility evidence
- Tighten .gitignore for macOS, hidden directories (.*/), and local secrets
This commit is contained in:
Sergey Antropoff
2026-07-16 01:08:01 +03:00
parent 003ee5d634
commit 777926487b
189 changed files with 241501 additions and 944 deletions
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""proxmoxer cookbook against the local HTTPS gateway."""
from __future__ import annotations
import os
import sys
import time
from proxmoxer import ProxmoxAPI
def wait_task(proxmox: ProxmoxAPI, node: str, upid: str, timeout: float = 120.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
status = proxmox.nodes(node).tasks(upid).status.get()
if status.get("status") == "stopped":
exitstatus = status.get("exitstatus", "")
if exitstatus not in ("OK", "ok", None, ""):
# Proxmox uses exitstatus "OK" on success; accept empty for lab.
if str(exitstatus).upper() != "OK":
raise RuntimeError(f"task failed: {status}")
return
time.sleep(0.5)
raise TimeoutError(upid)
def main() -> int:
host = os.environ.get("PVE_HOST", "localhost")
port = int(os.environ.get("PVE_PORT", "8007"))
user = os.environ.get("PVE_USER", "root@pam")
node = os.environ.get("PVE_NODE", "pve01")
vmid = int(os.environ.get("PVE_VMID", "110"))
token_name = os.environ.get("PVE_TOKEN_NAME")
token_value = os.environ.get("PVE_TOKEN_VALUE")
if token_name and token_value:
proxmox = ProxmoxAPI(
host,
user=user,
token_name=token_name,
token_value=token_value,
port=port,
verify_ssl=False,
)
else:
proxmox = ProxmoxAPI(
host,
user=user,
password=os.environ.get("PVE_PASSWORD", "secret"),
port=port,
verify_ssl=False,
)
print("version:", proxmox.version.get())
print("nodes:", proxmox.nodes.get())
print("qemu:", proxmox.nodes(node).qemu.get())
upid = proxmox.nodes(node).qemu.post(
vmid=vmid,
name=f"cookbook-{vmid}",
cores=1,
memory=512,
)
print("create:", upid)
wait_task(proxmox, node, upid)
upid = proxmox.nodes(node).qemu(vmid).status.start.post()
print("start:", upid)
wait_task(proxmox, node, upid)
print("status:", proxmox.nodes(node).qemu(vmid).status.current.get())
upid = proxmox.nodes(node).qemu(vmid).status.stop.post()
print("stop:", upid)
wait_task(proxmox, node, upid)
upid = proxmox.nodes(node).qemu(vmid).delete()
print("delete:", upid)
wait_task(proxmox, node, upid)
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Raw requests cookbook against HTTP :8006."""
from __future__ import annotations
import os
import sys
import time
from typing import Any
import requests
BASE = os.environ.get("PVE_BASE", "http://localhost:8006/api2/json")
NODE = os.environ.get("PVE_NODE", "pve01")
VMID = int(os.environ.get("PVE_VMID", "111"))
TOKEN = os.environ.get(
"PVE_API_TOKEN",
"root@pam!automation=automation-secret",
)
def api(
method: str,
path: str,
*,
headers: dict[str, str] | None = None,
data: dict[str, Any] | None = None,
) -> Any:
response = requests.request(
method,
f"{BASE}{path}",
headers=headers,
data=data,
timeout=60,
)
response.raise_for_status()
body = response.json()
return body.get("data", body)
def wait_task(headers: dict[str, str], upid: str, timeout: float = 120.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
status = api("GET", f"/nodes/{NODE}/tasks/{upid}/status", headers=headers)
if status.get("status") == "stopped":
return
time.sleep(0.5)
raise TimeoutError(upid)
def with_token() -> dict[str, str]:
return {"Authorization": f"PVEAPIToken={TOKEN}"}
def with_ticket() -> dict[str, str]:
data = api(
"POST",
"/access/ticket",
data={
"username": os.environ.get("PVE_USER", "root@pam"),
"password": os.environ.get("PVE_PASSWORD", "secret"),
},
)
return {
"Cookie": f"PVEAuthCookie={data['ticket']}",
"CSRFPreventionToken": data["CSRFPreventionToken"],
}
def cookbook(headers: dict[str, str], label: str) -> None:
print(label, "version:", api("GET", "/version", headers=headers))
print(label, "qemu:", api("GET", f"/nodes/{NODE}/qemu", headers=headers))
upid = api(
"POST",
f"/nodes/{NODE}/qemu",
headers=headers,
data={"vmid": VMID, "name": f"req-{VMID}", "cores": 1, "memory": 512},
)
wait_task(headers, upid)
upid = api("POST", f"/nodes/{NODE}/qemu/{VMID}/status/start", headers=headers)
wait_task(headers, upid)
print(
label, "status:", api("GET", f"/nodes/{NODE}/qemu/{VMID}/status/current", headers=headers)
)
upid = api("POST", f"/nodes/{NODE}/qemu/{VMID}/status/stop", headers=headers)
wait_task(headers, upid)
upid = api("DELETE", f"/nodes/{NODE}/qemu/{VMID}", headers=headers)
wait_task(headers, upid)
print(label, "ok")
def main() -> int:
cookbook(with_token(), "token")
# second VMID for ticket path
global VMID
VMID = int(os.environ.get("PVE_VMID_TICKET", "112"))
cookbook(with_ticket(), "ticket")
return 0
if __name__ == "__main__":
sys.exit(main())
+2
View File
@@ -0,0 +1,2 @@
proxmoxer>=2.3,<3
requests>=2.31