Files
proxmox-api-simulator/pulumi-tests/pulumi/pvelib/api.py
T
Sergey Antropoff baa1f58ad0 Fix CI/pulumi blockers and document test suites with latest PASS results.
Stabilize node status, cluster-wide Ceph OSD ids, and QEMU cpu utilization so
status/list no longer 500 on model strings; align pulumi defaults to pve1;
add bilingual testing docs with the 2026-07-18 ci-all + pulumi-tests pass.
2026-07-18 09:38:28 +03:00

133 lines
4.1 KiB
Python

"""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", "pve1")
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