"""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"