0773f721ea
- Scale small/large/big seeds (3×50 / 10×1000 / 20×2000) with proportional backups, snapshots, HA, replication, Ceph capacity, and OSD totals (10 / 100 / 500) plus matching node disks and crush/pg metadata - Enrich handler responses for apt, certificates, qemu/lxc status, storage, SDN, metrics export, and related cluster/node dumps - Flatten nested body_example fields into PARAMS and sync the request body via dotted paths (oVirt-style) - Restyle DATA controls as size cards with full-width Reset to minimal / Refresh stats; unload reloads the minimal cluster
167 lines
5.6 KiB
Python
167 lines
5.6 KiB
Python
"""Persistent LXC semantic handler tests."""
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
|
|
from app.api.errors import ApiError
|
|
from app.api.registry import HandlerRegistry
|
|
from app.handlers.lxc import register_lxc_handlers
|
|
|
|
|
|
class LxcPool:
|
|
def __init__(self) -> None:
|
|
self.resource_exists = False
|
|
self.missing = False
|
|
self.running = False
|
|
self.commands: list[str] = []
|
|
self.resource_id = uuid.uuid4()
|
|
|
|
async def fetchval(self, sql: str, *args: object) -> bool | int:
|
|
del args
|
|
if "pg_backend_pid" in sql:
|
|
return 123
|
|
if "extract(epoch" in sql:
|
|
return 1_700_000_000
|
|
if "FROM nodes" in sql:
|
|
return True
|
|
if "FROM resources" in sql:
|
|
return self.resource_exists
|
|
if "FROM snapshots" in sql:
|
|
return False
|
|
raise AssertionError(sql)
|
|
|
|
async def fetch(self, sql: str, *args: object) -> list[dict[str, object]]:
|
|
del args
|
|
if "FROM resources" in sql and "kind='lxc'" in sql:
|
|
return [
|
|
{
|
|
"vmid": 200,
|
|
"state": '{"status":"stopped","name":"service"}',
|
|
"config": '{"hostname":"service","memory":512,"cores":1}',
|
|
}
|
|
]
|
|
assert "FROM snapshots" in sql
|
|
return [
|
|
{
|
|
"name": "baseline",
|
|
"parent_name": None,
|
|
"description": "stable",
|
|
"created_at": datetime(2026, 1, 1, tzinfo=UTC),
|
|
}
|
|
]
|
|
|
|
async def fetchrow(self, sql: str, *args: object) -> dict[str, object] | None:
|
|
del args
|
|
if self.missing:
|
|
return None
|
|
if "SELECT r.id, r.version" in sql:
|
|
return {
|
|
"id": self.resource_id,
|
|
"version": 1,
|
|
"state": '{"name":"old","status":"stopped"}',
|
|
"config": '{"name":"old"}',
|
|
}
|
|
if "SELECT r.id, r.state, c.config" in sql:
|
|
return {"id": self.resource_id, "state": '{"status":"stopped"}', "config": "{}"}
|
|
if "SELECT r.id, r.state FROM resources" in sql:
|
|
status = "running" if self.running else "stopped"
|
|
return {"id": self.resource_id, "state": f'{{"status":"{status}"}}'}
|
|
if "SELECT s.* FROM snapshots" in sql:
|
|
return {
|
|
"id": uuid.uuid4(),
|
|
"name": "baseline",
|
|
"parent_name": None,
|
|
"description": "stable",
|
|
"created_at": datetime(2026, 1, 1, tzinfo=UTC),
|
|
"state": "{}",
|
|
}
|
|
raise AssertionError(sql)
|
|
|
|
async def execute(self, sql: str, *args: object) -> str:
|
|
del sql, args
|
|
self.commands.append("execute")
|
|
return "UPDATE 1"
|
|
|
|
|
|
class FakeDatabase:
|
|
def __init__(self, pool: LxcPool) -> None:
|
|
self.pool = pool
|
|
|
|
|
|
class FakeTaskRepository:
|
|
def __init__(self, pool: LxcPool) -> None:
|
|
self.pool = pool
|
|
self.created: list[dict[str, Any]] = []
|
|
|
|
async def create(self, **kwargs: Any) -> Any:
|
|
self.created.append(kwargs)
|
|
return type(
|
|
"Task", (), {"upid": "UPID:pve1:00000001:00000001:1700000000:pctcreate:201:root@pam:"}
|
|
)()
|
|
|
|
|
|
def _request(pool: LxcPool) -> Request:
|
|
app = type("App", (), {"state": type("State", (), {"database": FakeDatabase(pool)})()})()
|
|
request = Request({"type": "http", "headers": [], "method": "POST", "path": "/"})
|
|
request.scope["app"] = app
|
|
request.state.principal = "root@pam"
|
|
return request
|
|
|
|
|
|
@pytest.fixture
|
|
def registry() -> HandlerRegistry:
|
|
handler_registry = HandlerRegistry()
|
|
register_lxc_handlers(handler_registry)
|
|
return handler_registry
|
|
|
|
|
|
async def test_lxc_list_returns_seeded_containers(registry: HandlerRegistry) -> None:
|
|
pool = LxcPool()
|
|
handler = registry.get("/nodes/{node}/lxc", "GET")
|
|
assert handler is not None
|
|
result = await handler(_request(pool), {"values": {"node": "pve1"}})
|
|
assert len(result) == 1
|
|
assert result[0]["vmid"] == 200
|
|
assert result[0]["status"] == "stopped"
|
|
assert result[0]["name"] == "service"
|
|
assert result[0]["cpus"] == 1
|
|
assert "maxmem" in result[0]
|
|
|
|
|
|
async def test_lxc_create_rejects_duplicate_vmid(registry: HandlerRegistry) -> None:
|
|
pool = LxcPool()
|
|
pool.resource_exists = True
|
|
handler = registry.get("/nodes/{node}/lxc", "POST")
|
|
assert handler is not None
|
|
with pytest.raises(ApiError, match="VMID already exists"):
|
|
await handler(
|
|
_request(pool),
|
|
{"values": {"node": "pve1", "vmid": 201, "hostname": "app"}},
|
|
)
|
|
|
|
|
|
async def test_lxc_delete_requires_stopped_container(registry: HandlerRegistry) -> None:
|
|
pool = LxcPool()
|
|
pool.running = True
|
|
handler = registry.get("/nodes/{node}/lxc/{vmid}", "DELETE")
|
|
assert handler is not None
|
|
with pytest.raises(ApiError, match="cannot delete a running container"):
|
|
await handler(_request(pool), {"values": {"node": "pve1", "vmid": "200"}})
|
|
|
|
|
|
async def test_lxc_start_creates_task(
|
|
monkeypatch: pytest.MonkeyPatch, registry: HandlerRegistry
|
|
) -> None:
|
|
pool = LxcPool()
|
|
repository = FakeTaskRepository(pool)
|
|
monkeypatch.setattr("app.handlers.lxc.TaskRepository", lambda _pool: repository)
|
|
handler = registry.get("/nodes/{node}/lxc/{vmid}/status/start", "POST")
|
|
assert handler is not None
|
|
upid = await handler(_request(pool), {"values": {"node": "pve1", "vmid": "200"}})
|
|
assert upid.startswith("UPID:")
|
|
assert repository.created[0]["task_type"] == "lxc-start"
|