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:
@@ -0,0 +1,126 @@
|
||||
"""Cluster backup job handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, cast
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from app.api.registry import HandlerRegistry
|
||||
from app.db.pool import AsyncpgDatabase
|
||||
from app.handlers.backup import register_backup_handlers
|
||||
|
||||
|
||||
class BackupPool:
|
||||
def __init__(self) -> None:
|
||||
self.metadata: dict[str, Any] = {
|
||||
"backup_jobs": {
|
||||
"backup-daily": {
|
||||
"id": "backup-daily",
|
||||
"schedule": "0 2 * * *",
|
||||
"storage": "local",
|
||||
"enabled": 1,
|
||||
"vmid": "100",
|
||||
}
|
||||
}
|
||||
}
|
||||
self.guests = [
|
||||
{
|
||||
"external_id": "100",
|
||||
"kind": "qemu",
|
||||
"state": '{"name":"demo"}',
|
||||
"qemu_config": '{"scsi0":"local-lvm:vm-100-disk-0,size=32G"}',
|
||||
"lxc_config": None,
|
||||
},
|
||||
{
|
||||
"external_id": "200",
|
||||
"kind": "lxc",
|
||||
"state": '{"name":"service"}',
|
||||
"qemu_config": None,
|
||||
"lxc_config": '{"rootfs":"local-lvm:vm-200-disk-0,size=8G"}',
|
||||
},
|
||||
]
|
||||
|
||||
async def fetchrow(self, query: str, *_arguments: object) -> dict[str, Any] | None:
|
||||
if "FROM clusters WHERE id" in query:
|
||||
return {"metadata": json.dumps(self.metadata)}
|
||||
raise AssertionError(query)
|
||||
|
||||
async def fetch(self, query: str, *arguments: object) -> list[dict[str, Any]]:
|
||||
if "FROM resources r" in query and "ANY($1" in query:
|
||||
selected = set(str(item) for item in cast(list[Any], arguments[0]))
|
||||
return [row for row in self.guests if row["external_id"] in selected]
|
||||
if "FROM resources r" in query:
|
||||
return list(self.guests)
|
||||
raise AssertionError(query)
|
||||
|
||||
async def execute(self, query: str, *arguments: object) -> str:
|
||||
if "UPDATE clusters SET metadata" in query:
|
||||
self.metadata = json.loads(str(arguments[1]))
|
||||
return "UPDATE 1"
|
||||
raise AssertionError(query)
|
||||
|
||||
|
||||
def _request(pool: BackupPool) -> Request:
|
||||
app = FastAPI()
|
||||
app.state.database = cast(AsyncpgDatabase, type("DB", (), {"pool": pool})())
|
||||
return Request(
|
||||
{
|
||||
"type": "http",
|
||||
"app": app,
|
||||
"method": "POST",
|
||||
"path": "/",
|
||||
"headers": [],
|
||||
"query_string": b"",
|
||||
"server": ("test", 80),
|
||||
"client": ("test", 123),
|
||||
"scheme": "http",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_backup_jobs_crud_and_shapes() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_backup_handlers(registry)
|
||||
pool = BackupPool()
|
||||
http = _request(pool)
|
||||
|
||||
listed = await registry.get("/cluster/backup", "GET")(http, {"values": {}})
|
||||
assert listed[0]["id"] == "backup-daily"
|
||||
assert listed[0]["schedule"] == "0 2 * * *"
|
||||
|
||||
await registry.get("/cluster/backup", "POST")(
|
||||
http,
|
||||
{
|
||||
"values": {
|
||||
"id": "backup-weekly",
|
||||
"schedule": "@weekly",
|
||||
"storage": "local",
|
||||
"vmid": "200",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert "backup-weekly" in pool.metadata["backup_jobs"]
|
||||
|
||||
info = await registry.get("/cluster/backup-info", "GET")(http, {"values": {}})
|
||||
assert info == [{"subdir": "not-backed-up"}]
|
||||
|
||||
missing = await registry.get("/cluster/backup-info/not-backed-up", "GET")(http, {"values": {}})
|
||||
assert missing == [] # 100 covered by daily; after weekly both covered? weekly adds 200
|
||||
# daily covers 100, weekly covers 200 → none missing
|
||||
assert missing == []
|
||||
|
||||
included = await registry.get("/cluster/backup/{id}/included_volumes", "GET")(
|
||||
http, {"values": {"id": "backup-daily"}}
|
||||
)
|
||||
assert included["children"][0]["id"] == 100
|
||||
assert included["children"][0]["children"][0]["id"] == "scsi0"
|
||||
|
||||
await registry.get("/cluster/backup/{id}", "DELETE")(http, {"values": {"id": "backup-weekly"}})
|
||||
assert "backup-weekly" not in pool.metadata["backup_jobs"]
|
||||
missing_after = await registry.get("/cluster/backup-info/not-backed-up", "GET")(
|
||||
http, {"values": {}}
|
||||
)
|
||||
assert missing_after[0]["vmid"] == 200
|
||||
assert missing_after[0]["type"] == "lxc"
|
||||
Reference in New Issue
Block a user