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:
@@ -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())
|
||||
@@ -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())
|
||||
@@ -0,0 +1,2 @@
|
||||
proxmoxer>=2.3,<3
|
||||
requests>=2.31
|
||||
Reference in New Issue
Block a user