baa1f58ad0
Stabilize node status, cluster-wide Ceph OSD ids, and QEMU cpu utilization so status/list no longer 500 on model strings; align pulumi defaults to pve1; add bilingual testing docs with the 2026-07-18 ci-all + pulumi-tests pass.
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""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)
|
|
|
|
list_jobs = registry.get("/cluster/backup", "GET")
|
|
create_job = registry.get("/cluster/backup", "POST")
|
|
backup_info = registry.get("/cluster/backup-info", "GET")
|
|
not_backed_up = registry.get("/cluster/backup-info/not-backed-up", "GET")
|
|
included_volumes = registry.get("/cluster/backup/{id}/included_volumes", "GET")
|
|
delete_job = registry.get("/cluster/backup/{id}", "DELETE")
|
|
assert list_jobs is not None
|
|
assert create_job is not None
|
|
assert backup_info is not None
|
|
assert not_backed_up is not None
|
|
assert included_volumes is not None
|
|
assert delete_job is not None
|
|
|
|
listed = await list_jobs(http, {"values": {}})
|
|
assert listed[0]["id"] == "backup-daily"
|
|
assert listed[0]["schedule"] == "0 2 * * *"
|
|
|
|
await create_job(
|
|
http,
|
|
{
|
|
"values": {
|
|
"id": "backup-weekly",
|
|
"schedule": "@weekly",
|
|
"storage": "local",
|
|
"vmid": "200",
|
|
}
|
|
},
|
|
)
|
|
assert "backup-weekly" in pool.metadata["backup_jobs"]
|
|
|
|
info = await backup_info(http, {"values": {}})
|
|
assert info == [{"subdir": "not-backed-up"}]
|
|
|
|
missing = await not_backed_up(http, {"values": {}})
|
|
# daily covers 100, weekly covers 200 → none missing
|
|
assert missing == []
|
|
|
|
included = await included_volumes(http, {"values": {"id": "backup-daily"}})
|
|
assert included["children"][0]["id"] == 100
|
|
assert included["children"][0]["children"][0]["id"] == "scsi0"
|
|
|
|
await delete_job(http, {"values": {"id": "backup-weekly"}})
|
|
assert "backup-weekly" not in pool.metadata["backup_jobs"]
|
|
missing_after = await not_backed_up(http, {"values": {}})
|
|
assert missing_after[0]["vmid"] == 200
|
|
assert missing_after[0]["type"] == "lxc"
|