Files
Sergey Antropoff 0773f721ea Align sized cluster seeds and GET dumps with PVE wire shapes; restyle DATA panel.
- 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
2026-07-18 08:46:11 +03:00

167 lines
5.8 KiB
Python

"""Node ops handlers persist network/disks/services into nodes.metadata."""
from __future__ import annotations
import json
from typing import Any, cast
import pytest
from fastapi import FastAPI, Request
from app.api.registry import HandlerRegistry
from app.db.pool import AsyncpgDatabase
from app.handlers.nodes import register_node_ops_handlers
class NodePool:
def __init__(self) -> None:
self.metadata: dict[str, Any] = {}
async def fetchrow(self, query: str, *arguments: object) -> dict[str, Any] | None:
del arguments
if "SELECT metadata FROM nodes" in query:
return {"metadata": json.dumps(self.metadata)}
raise AssertionError(query)
async def fetchval(self, query: str, *arguments: object) -> Any:
del arguments
if "EXISTS(SELECT 1 FROM nodes" in query:
return True
if "SELECT name FROM nodes ORDER BY name LIMIT 1" in query:
return "pve01"
raise AssertionError(query)
async def execute(self, query: str, *arguments: object) -> str:
if "UPDATE nodes SET metadata" in query:
self.metadata = json.loads(str(arguments[1]))
return "UPDATE 1"
raise AssertionError(query)
class FakeTaskRepository:
def __init__(self, pool: NodePool) -> 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": kwargs["upid"]})()
class FakeDatabase:
def __init__(self, pool: NodePool) -> None:
self.pool = pool
def request(pool: NodePool, *, method: str = "GET", path: str = "/") -> Request:
app = FastAPI()
app.state.database = cast(AsyncpgDatabase, FakeDatabase(pool))
result = Request(
{
"type": "http",
"app": app,
"method": method,
"path": path,
"headers": [],
"query_string": b"",
"server": ("test", 80),
"client": ("test", 123),
"scheme": "http",
}
)
result.state.principal = "root@pam"
return result
@pytest.fixture
def task_repo(monkeypatch: pytest.MonkeyPatch) -> FakeTaskRepository:
repository = FakeTaskRepository(NodePool())
monkeypatch.setattr("app.handlers.nodes.TaskRepository", lambda _pool: repository)
return repository
async def test_network_and_service_mutations_persist(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = HandlerRegistry()
register_node_ops_handlers(registry)
pool = NodePool()
repository = FakeTaskRepository(pool)
monkeypatch.setattr("app.handlers.nodes.TaskRepository", lambda _pool: repository)
create = registry.get("/nodes/{node}/network", "POST")
listing = registry.get("/nodes/{node}/network", "GET")
delete = registry.get("/nodes/{node}/network/{iface}", "DELETE")
reload_network = registry.get("/nodes/{node}/network", "PUT")
stop = registry.get("/nodes/{node}/services/{service}/stop", "POST")
state = registry.get("/nodes/{node}/services/{service}/state", "GET")
assert create and listing and delete and reload_network and stop and state
await create(
request(pool, method="POST", path="/api2/json/nodes/pve01/network"),
{"values": {"node": "pve01", "iface": "vmbr9", "type": "bridge"}, "provided": frozenset()},
)
items = await listing(
request(pool),
{"values": {"node": "pve01"}, "provided": frozenset()},
)
assert any(item["iface"] == "vmbr9" for item in items)
await delete(
request(pool, method="DELETE", path="/api2/json/nodes/pve01/network/vmbr9"),
{"values": {"node": "pve01", "iface": "vmbr9"}, "provided": frozenset()},
)
items = await listing(
request(pool),
{"values": {"node": "pve01"}, "provided": frozenset()},
)
assert all(item["iface"] != "vmbr9" for item in items)
reload_upid = await reload_network(
request(pool, method="PUT", path="/api2/json/nodes/pve01/network"),
{"values": {"node": "pve01"}, "provided": frozenset()},
)
assert isinstance(reload_upid, str) and reload_upid.startswith("UPID:")
stop_upid = await stop(
request(pool, method="POST", path="/api2/json/nodes/pve01/services/pveproxy/stop"),
{"values": {"node": "pve01", "service": "pveproxy"}, "provided": frozenset()},
)
assert isinstance(stop_upid, str) and stop_upid.startswith("UPID:")
service = await state(
request(pool),
{"values": {"node": "pve01", "service": "pveproxy"}, "provided": frozenset()},
)
assert service["state"] == "stopped"
assert "ops" in pool.metadata
async def test_disk_init_and_wipe_persist(monkeypatch: pytest.MonkeyPatch) -> None:
registry = HandlerRegistry()
register_node_ops_handlers(registry)
pool = NodePool()
repository = FakeTaskRepository(pool)
monkeypatch.setattr("app.handlers.nodes.TaskRepository", lambda _pool: repository)
initgpt = registry.get("/nodes/{node}/disks/initgpt", "POST")
wipe = registry.get("/nodes/{node}/disks/wipedisk", "PUT")
listing = registry.get("/nodes/{node}/disks/list", "GET")
assert initgpt and wipe and listing
init_upid = await initgpt(
request(pool, method="POST"),
{"values": {"node": "pve01", "disk": "/dev/sdb"}, "provided": frozenset()},
)
wipe_upid = await wipe(
request(pool, method="PUT"),
{"values": {"node": "pve01", "disk": "/dev/sdb"}, "provided": frozenset()},
)
assert init_upid.startswith("UPID:")
assert wipe_upid.startswith("UPID:")
disks = await listing(
request(pool),
{"values": {"node": "pve01"}, "provided": frozenset()},
)
target = next(item for item in disks if item["devpath"] == "/dev/sdb")
assert target["wiped"] == 1
assert target["gpt"] == 0