Prepare 0.1.0 for lab release: durable handlers, HTTP Compose, CI, and pulumi-tests.
- Harden DB-backed handlers and seed profiles; align client wire shapes for cluster resources, QEMU config, and node SSL fields - Serve plain HTTP on Compose :8006; keep TLS optional (--profile tls) and terminate HTTPS at Kubernetes Ingress - Add pulumi-tests (full contract surface majors 6–9 + BPG lifecycle) and make pulumi-tests - Ship bilingual docs, CHANGELOG, SECURITY, CONTRIBUTING, and GitHub Actions (make ci + Compose/Helm validation)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Minimal Proxmox API client for Pulumi programs and surface probes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class Pve:
|
||||
def __init__(self, *, authenticate: bool = True) -> None:
|
||||
root = os.environ.get("API_URL", "http://simulator:8006").rstrip("/")
|
||||
self.root = root
|
||||
self.base = root + "/api2/json"
|
||||
self.node = os.environ.get("PVE_NODE", "pve01")
|
||||
self.storage = os.environ.get("PVE_STORAGE", "local-lvm")
|
||||
self.bridge = os.environ.get("PVE_BRIDGE", "vmbr0")
|
||||
self._c = httpx.Client(base_url=self.base, timeout=120.0)
|
||||
self._h: dict[str, str] = {}
|
||||
if authenticate:
|
||||
self.login()
|
||||
|
||||
def close(self) -> None:
|
||||
self._c.close()
|
||||
|
||||
def __enter__(self) -> Pve:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
|
||||
def login(
|
||||
self,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
r = self._c.post(
|
||||
"/access/ticket",
|
||||
data={
|
||||
"username": username or os.environ.get("API_USER", "root@pam"),
|
||||
"password": password or os.environ.get("API_PASSWORD", "secret"),
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()["data"]
|
||||
self._h = {
|
||||
"Cookie": f"PVEAuthCookie={data['ticket']}",
|
||||
"CSRFPreventionToken": data["CSRFPreventionToken"],
|
||||
}
|
||||
return data
|
||||
|
||||
def clear_auth(self) -> None:
|
||||
self._h = {}
|
||||
|
||||
def req(self, method: str, path: str, **kw: Any) -> Any:
|
||||
r = self._c.request(method, path, headers=self._h, **kw)
|
||||
if r.status_code >= 400:
|
||||
raise RuntimeError(f"{method} {path} -> {r.status_code} {r.text}")
|
||||
return r.json().get("data")
|
||||
|
||||
def wait(self, upid: str) -> None:
|
||||
enc = urllib.parse.quote(upid, safe="")
|
||||
for _ in range(600):
|
||||
st = self.req("GET", f"/nodes/{self.node}/tasks/{enc}/status")
|
||||
if st.get("status") == "stopped":
|
||||
if st.get("exitstatus") != "OK":
|
||||
raise RuntimeError(st)
|
||||
return
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(upid)
|
||||
|
||||
def create_vm(self, vmid: int, name: str) -> None:
|
||||
upid = self.req(
|
||||
"POST",
|
||||
f"/nodes/{self.node}/qemu",
|
||||
data={
|
||||
"vmid": vmid,
|
||||
"name": name,
|
||||
"cores": 1,
|
||||
"memory": 512,
|
||||
"scsi0": f"{self.storage}:vm-{vmid}-disk-0,size=8G",
|
||||
"net0": f"virtio,bridge={self.bridge}",
|
||||
},
|
||||
)
|
||||
self.wait(upid)
|
||||
|
||||
def delete_vm(self, vmid: int) -> None:
|
||||
try:
|
||||
upid = self.req("POST", f"/nodes/{self.node}/qemu/{vmid}/status/stop")
|
||||
if isinstance(upid, str):
|
||||
self.wait(upid)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
upid = self.req("DELETE", f"/nodes/{self.node}/qemu/{vmid}")
|
||||
if isinstance(upid, str):
|
||||
self.wait(upid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_lxc(self, vmid: int, name: str) -> None:
|
||||
upid = self.req(
|
||||
"POST",
|
||||
f"/nodes/{self.node}/lxc",
|
||||
data={
|
||||
"vmid": vmid,
|
||||
"hostname": name,
|
||||
"ostemplate": "local:vztmpl/example.tar.zst",
|
||||
"rootfs": f"{self.storage}:8",
|
||||
"memory": 512,
|
||||
"cores": 1,
|
||||
"net0": f"name=eth0,bridge={self.bridge},ip=dhcp",
|
||||
},
|
||||
)
|
||||
self.wait(upid)
|
||||
|
||||
def delete_lxc(self, vmid: int) -> None:
|
||||
try:
|
||||
upid = self.req("POST", f"/nodes/{self.node}/lxc/{vmid}/status/stop")
|
||||
if isinstance(upid, str):
|
||||
self.wait(upid)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
upid = self.req("DELETE", f"/nodes/{self.node}/lxc/{vmid}")
|
||||
if isinstance(upid, str):
|
||||
self.wait(upid)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user