Initial working version

This commit is contained in:
Philip Henning 2026-03-08 18:32:25 +01:00
parent 34a0627e76
commit b6886cb34a
61 changed files with 4475 additions and 6 deletions

View file

@ -0,0 +1,193 @@
from __future__ import annotations
from urllib.parse import parse_qs
import httpx
import pytest
from pve_vm_setup.errors import ProxmoxConnectError
from pve_vm_setup.models.workflow import VmConfig
from pve_vm_setup.services.proxmox import ProxmoxApiClient
from pve_vm_setup.settings import AppSettings
def build_settings() -> AppSettings:
return AppSettings.from_env(
{
"PROXMOX_URL": "https://proxmox.example.invalid:8006",
"PROXMOX_USER": "root",
"PROXMOX_PASSWORD": "secret",
"PROXMOX_REALM": "pam",
},
load_dotenv_file=False,
)
def test_client_uses_api_base_when_loading_realms() -> None:
recorded_urls: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
recorded_urls.append(str(request.url))
return httpx.Response(200, json={"data": [{"realm": "pam", "comment": "Linux PAM"}]})
client = ProxmoxApiClient(build_settings(), transport=httpx.MockTransport(handler))
try:
realms = client.load_realms()
finally:
client.close()
assert realms[0].name == "pam"
assert recorded_urls == ["https://proxmox.example.invalid:8006/api2/json/access/domains"]
def test_client_maps_connect_errors() -> None:
def handler(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("boom", request=request)
client = ProxmoxApiClient(build_settings(), transport=httpx.MockTransport(handler))
try:
with pytest.raises(ProxmoxConnectError):
client.load_realms()
finally:
client.close()
def test_client_attaches_serial_device_without_switching_display_to_serial() -> None:
requests: list[tuple[str, str, bytes]] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append((request.method, request.url.path, request.content))
path = request.url.path
if path.endswith("/nodes/fake-node-01/qemu") and request.method == "POST":
return httpx.Response(200, json={"data": "UPID:create"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:create/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
if path.endswith("/nodes/fake-node-01/qemu/123/config") and request.method == "PUT":
return httpx.Response(200, json={"data": "UPID:serial"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:serial/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
raise AssertionError(f"Unexpected request: {request.method} {request.url}")
client = ProxmoxApiClient(build_settings(), transport=httpx.MockTransport(handler))
client._ticket = "ticket"
client._csrf_token = "csrf"
client._client.cookies.set("PVEAuthCookie", "ticket")
config = VmConfig()
config.general.node = "fake-node-01"
config.general.vmid = 123
config.general.name = "demo"
config.general.ha_enabled = False
config.os.storage = "cephfs"
config.os.iso = "cephfs:iso/nixos.iso"
try:
client.create_vm(config)
finally:
client.close()
serial_request = next(
content
for method, path, content in requests
if method == "PUT" and path.endswith("/nodes/fake-node-01/qemu/123/config")
)
payload = parse_qs(serial_request.decode())
assert payload["serial0"] == ["socket"]
assert "vga" not in payload
def test_client_starts_vm_after_create_when_requested() -> None:
requests: list[tuple[str, str, bytes]] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append((request.method, request.url.path, request.content))
path = request.url.path
if path.endswith("/nodes/fake-node-01/qemu") and request.method == "POST":
return httpx.Response(200, json={"data": "UPID:create"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:create/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
if path.endswith("/nodes/fake-node-01/qemu/123/config") and request.method == "PUT":
return httpx.Response(200, json={"data": "UPID:serial"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:serial/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
if path.endswith("/nodes/fake-node-01/qemu/123/status/start") and request.method == "POST":
return httpx.Response(200, json={"data": "UPID:start"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:start/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
raise AssertionError(f"Unexpected request: {request.method} {request.url}")
client = ProxmoxApiClient(build_settings(), transport=httpx.MockTransport(handler))
client._ticket = "ticket"
client._csrf_token = "csrf"
client._client.cookies.set("PVEAuthCookie", "ticket")
config = VmConfig()
config.general.node = "fake-node-01"
config.general.vmid = 123
config.general.name = "demo"
config.general.ha_enabled = False
config.os.storage = "cephfs"
config.os.iso = "cephfs:iso/nixos.iso"
try:
client.create_vm(config, start_after_create=True)
finally:
client.close()
assert any(
method == "POST" and path.endswith("/nodes/fake-node-01/qemu/123/status/start")
for method, path, _ in requests
)
def test_client_registers_ha_without_start_when_auto_start_disabled() -> None:
requests: list[tuple[str, str, bytes]] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append((request.method, request.url.path, request.content))
path = request.url.path
if path.endswith("/nodes/fake-node-01/qemu") and request.method == "POST":
return httpx.Response(200, json={"data": "UPID:create"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:create/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
if path.endswith("/nodes/fake-node-01/qemu/123/config") and request.method == "PUT":
return httpx.Response(200, json={"data": "UPID:serial"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:serial/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
if path.endswith("/cluster/ha/resources") and request.method == "POST":
return httpx.Response(200, json={"data": "UPID:ha"})
if path.endswith("/nodes/fake-node-01/tasks/UPID:ha/status"):
return httpx.Response(200, json={"data": {"status": "stopped", "exitstatus": "OK"}})
raise AssertionError(f"Unexpected request: {request.method} {request.url}")
client = ProxmoxApiClient(build_settings(), transport=httpx.MockTransport(handler))
client._ticket = "ticket"
client._csrf_token = "csrf"
client._client.cookies.set("PVEAuthCookie", "ticket")
config = VmConfig()
config.general.node = "fake-node-01"
config.general.vmid = 123
config.general.name = "demo"
config.general.ha_enabled = True
config.os.storage = "cephfs"
config.os.iso = "cephfs:iso/nixos.iso"
try:
client.create_vm(config, start_after_create=False)
finally:
client.close()
ha_request = next(
content
for method, path, content in requests
if method == "POST" and path.endswith("/cluster/ha/resources")
)
payload = parse_qs(ha_request.decode())
assert payload["state"] == ["stopped"]
assert not any(
method == "POST" and path.endswith("/nodes/fake-node-01/qemu/123/status/start")
for method, path, _ in requests
)