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
This commit is contained in:
Sergey Antropoff
2026-07-18 08:46:11 +03:00
parent 48df10b17e
commit 0773f721ea
77 changed files with 4870 additions and 855 deletions
+217 -5
View File
@@ -6,8 +6,10 @@ import json
from typing import Any, cast
from uuid import uuid4
import pytest
from fastapi import FastAPI, Request
from app.api.errors import ApiError
from app.api.registry import HandlerRegistry
from app.db.pool import AsyncpgDatabase
from app.handlers.acme import register_acme_handlers
@@ -34,6 +36,8 @@ class MetaPool:
async def fetchval(self, query: str, *arguments: object) -> Any:
if "EXISTS(SELECT 1 FROM nodes" in query:
return str(arguments[0]) in self.nodes
if "SELECT name FROM nodes ORDER BY name LIMIT 1" in query:
return sorted(self.nodes)[0] if self.nodes else None
raise AssertionError(query)
async def execute(self, query: str, *arguments: object) -> str:
@@ -52,6 +56,16 @@ class MetaPool:
raise AssertionError(query)
class FakeTaskRepository:
def __init__(self, pool: MetaPool) -> 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"]})()
async def call(
registry: HandlerRegistry, path: str, verb: str, http: Request, inputs: dict[str, Any]
) -> Any:
@@ -63,7 +77,7 @@ async def call(
def request(pool: MetaPool) -> Request:
app = FastAPI()
app.state.database = cast(AsyncpgDatabase, type("DB", (), {"pool": pool})())
return Request(
http = Request(
{
"type": "http",
"app": app,
@@ -76,15 +90,19 @@ def request(pool: MetaPool) -> Request:
"scheme": "http",
}
)
http.state.principal = "root@pam"
return http
async def test_mapping_acme_config_persist() -> None:
async def test_mapping_acme_config_persist(monkeypatch: pytest.MonkeyPatch) -> None:
registry = HandlerRegistry()
register_mapping_handlers(registry)
register_acme_handlers(registry)
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
repository = FakeTaskRepository(pool)
monkeypatch.setattr("app.handlers.cluster_config.TaskRepository", lambda _pool: repository)
await call(
registry,
@@ -119,20 +137,214 @@ async def test_mapping_acme_config_persist() -> None:
http,
{"values": {"name": "default"}, "provided": frozenset()},
)
assert account["name"] == "default"
assert account["account"]["name"] == "default"
assert account["directory"]
assert "eab-hmac-key" not in account
assert "eab-hmac-key" not in account["account"]
await call(
upid = await call(
registry,
"/cluster/config",
"POST",
http,
{"values": {"clustername": "lab"}, "provided": frozenset()},
{"values": {"clustername": "lab", "link0": "10.0.0.1"}, "provided": frozenset()},
)
assert isinstance(upid, str)
assert upid.startswith("UPID:pve1:")
assert ":clustercreate:" in upid
assert repository.created[0]["task_type"] == "cluster-create"
assert pool.metadata["cluster_config"]["clustername"] == "lab"
assert pool.cluster_name == "lab"
assert pool.metadata["cluster_config"]["corosync_conf"]
assert pool.metadata["cluster_config"]["config_digest"]
totem = await call(
registry, "/cluster/config/totem", "GET", http, {"values": {}, "provided": frozenset()}
)
assert totem["cluster_name"] == "lab"
assert uuid4()
async def test_cluster_config_index_uses_name_links() -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
index = await call(
registry, "/cluster/config", "GET", request(pool), {"values": {}, "provided": frozenset()}
)
assert index == [
{"name": "nodes"},
{"name": "totem"},
{"name": "join"},
{"name": "qdevice"},
{"name": "apiversion"},
]
async def test_cluster_join_info_shape_and_stable_digest(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
monkeypatch.setattr(
"app.handlers.cluster_config.TaskRepository",
lambda _pool: FakeTaskRepository(pool),
)
await call(
registry,
"/cluster/config",
"POST",
http,
{"values": {"clustername": "lab", "link0": "10.0.0.1"}, "provided": frozenset()},
)
first = await call(
registry,
"/cluster/config/join",
"GET",
http,
{"values": {}, "provided": frozenset()},
)
second = await call(
registry,
"/cluster/config/join",
"GET",
http,
{"values": {"node": "pve1"}, "provided": frozenset()},
)
assert set(first) == {"nodelist", "preferred_node", "totem", "config_digest"}
assert first["config_digest"] == second["config_digest"]
assert first["preferred_node"] == "pve1"
defaulted = await call(
registry,
"/cluster/config/join",
"GET",
http,
{
"values": {"node": "current connected node"},
"provided": frozenset(),
},
)
assert defaulted["preferred_node"] == "pve1"
node = first["nodelist"][0]
assert node["name"] == "pve1"
assert node["pve_addr"]
assert node["ring0_addr"]
assert node["pve_fp"].count(":") == 31
assert "password" not in json.dumps(first)
async def test_cluster_join_returns_upid(monkeypatch: pytest.MonkeyPatch) -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
repository = FakeTaskRepository(pool)
monkeypatch.setattr("app.handlers.cluster_config.TaskRepository", lambda _pool: repository)
fingerprint = ":".join(["AB"] * 32)
upid = await call(
registry,
"/cluster/config/join",
"POST",
http,
{
"values": {
"hostname": "10.0.0.1",
"fingerprint": fingerprint,
"password": "secret",
},
"provided": frozenset(),
},
)
assert upid.startswith("UPID:")
assert ":clusterjoin:" in upid
assert repository.created[0]["task_type"] == "cluster-join"
join = pool.metadata["cluster_config"]["join_info"]["10.0.0.1"]
assert join["password_set"] is True
assert "password" not in join
async def test_cluster_join_requires_contract_params() -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
with pytest.raises(ApiError, match="hostname"):
await call(
registry,
"/cluster/config/join",
"POST",
http,
{"values": {"password": "x", "fingerprint": "y"}, "provided": frozenset()},
)
async def test_cluster_create_rejects_existing_corosync(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
monkeypatch.setattr(
"app.handlers.cluster_config.TaskRepository",
lambda _pool: FakeTaskRepository(pool),
)
await call(
registry,
"/cluster/config",
"POST",
http,
{"values": {"clustername": "lab", "link0": "10.0.0.1"}, "provided": frozenset()},
)
with pytest.raises(ApiError, match="cluster config already exists"):
await call(
registry,
"/cluster/config",
"POST",
http,
{"values": {"clustername": "other"}, "provided": frozenset()},
)
async def test_cluster_addnode_returns_corosync_object(
monkeypatch: pytest.MonkeyPatch,
) -> None:
registry = HandlerRegistry()
register_cluster_config_handlers(registry)
pool = MetaPool()
http = request(pool)
monkeypatch.setattr(
"app.handlers.cluster_config.TaskRepository",
lambda _pool: FakeTaskRepository(pool),
)
await call(
registry,
"/cluster/config",
"POST",
http,
{"values": {"clustername": "lab", "link0": "10.0.0.1"}, "provided": frozenset()},
)
result = await call(
registry,
"/cluster/config/nodes/{node}",
"POST",
http,
{
"values": {"node": "pve2", "new_node_ip": "10.0.0.2", "votes": 1},
"provided": frozenset(),
},
)
assert set(result) == {"corosync_authkey", "corosync_conf", "warnings"}
assert "nodelist" in result["corosync_conf"]
assert "pve2" in result["corosync_conf"]
assert result["warnings"] == []
assert "pve2" in pool.nodes
nodes = await call(
registry,
"/cluster/config/nodes",
"GET",
http,
{"values": {}, "provided": frozenset()},
)
assert {item["node"] for item in nodes} == {"pve1", "pve2"}