6033967e6a
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
218 lines
8.6 KiB
Python
218 lines
8.6 KiB
Python
"""Cluster-level semantic handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from fastapi import Request
|
|
|
|
from app.api.errors import ApiError
|
|
from app.api.registry import HandlerRegistry
|
|
from app.handlers.common import (
|
|
cluster_metadata,
|
|
database,
|
|
save_cluster_metadata,
|
|
state,
|
|
subdirs,
|
|
values,
|
|
)
|
|
from app.simulation.seed import CLUSTER_ID
|
|
|
|
|
|
def _replication_jobs(metadata: dict[str, Any]) -> list[dict[str, Any]]:
|
|
jobs = metadata.get("replication", [])
|
|
if not isinstance(jobs, list):
|
|
return []
|
|
return [dict(item) for item in jobs if isinstance(item, dict)]
|
|
|
|
|
|
def register_cluster_handlers(registry: HandlerRegistry) -> None:
|
|
async def cluster_index(_request: Request, _inputs: dict[str, Any]) -> list[dict[str, str]]:
|
|
return subdirs(
|
|
"acme",
|
|
"backup",
|
|
"backup-info",
|
|
"config",
|
|
"ha",
|
|
"log",
|
|
"mapping",
|
|
"nextid",
|
|
"notifications",
|
|
"options",
|
|
"replication",
|
|
"sdn",
|
|
"status",
|
|
"tasks",
|
|
)
|
|
|
|
async def cluster_status(_request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
rows = await database(_request).pool.fetch(
|
|
"""SELECT id, name, status FROM nodes ORDER BY name"""
|
|
)
|
|
result: list[dict[str, Any]] = []
|
|
for index, row in enumerate(rows):
|
|
online = str(row["status"]) == "online"
|
|
result.append(
|
|
{
|
|
"id": str(row["id"]),
|
|
"name": str(row["name"]),
|
|
"nodeid": index,
|
|
"online": 1 if online else 0,
|
|
"local": 1 if index == 0 else 0,
|
|
"ip": f"10.32.{index // 254 + 1}.{index % 254 + 10}",
|
|
"level": "c",
|
|
"type": "node",
|
|
"quorate": 1,
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def cluster_nextid(request: Request, inputs: dict[str, Any]) -> int:
|
|
requested = values(inputs).get("vmid")
|
|
if requested is not None:
|
|
candidate = int(requested)
|
|
taken = await database(request).pool.fetchval(
|
|
"""SELECT EXISTS(
|
|
SELECT 1 FROM resources WHERE kind IN ('qemu', 'lxc') AND external_id=$1
|
|
)""",
|
|
str(candidate),
|
|
)
|
|
if not taken:
|
|
return candidate
|
|
raise ApiError(400, f"VMID {candidate} already exists")
|
|
maximum = await database(request).pool.fetchval(
|
|
"""SELECT COALESCE(MAX(external_id::integer), 99)
|
|
FROM resources WHERE kind IN ('qemu', 'lxc')"""
|
|
)
|
|
return int(maximum) + 1
|
|
|
|
async def cluster_options_get(_request: Request, _inputs: dict[str, Any]) -> dict[str, Any]:
|
|
row = await database(_request).pool.fetchrow(
|
|
"SELECT metadata FROM clusters WHERE id=$1",
|
|
CLUSTER_ID,
|
|
)
|
|
metadata = state(row["metadata"]) if row is not None else {}
|
|
options = metadata.get("options", {})
|
|
if not isinstance(options, dict):
|
|
options = {}
|
|
return {
|
|
"keyboard": options.get("keyboard", "en-us"),
|
|
"email_from": options.get("email_from", "root@localhost"),
|
|
"http_proxy": options.get("http_proxy", ""),
|
|
"description": options.get("description", "Proxmox API emulator cluster"),
|
|
**options,
|
|
}
|
|
|
|
async def cluster_options_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
current = await cluster_options_get(request, inputs)
|
|
provided = values(inputs)
|
|
updated = {**current, **{key: value for key, value in provided.items() if key != "node"}}
|
|
await database(request).pool.execute(
|
|
"""UPDATE clusters SET metadata = jsonb_set(
|
|
COALESCE(metadata, '{}'::jsonb), '{options}', $2::jsonb, true
|
|
), updated_at=now() WHERE id=$1""",
|
|
CLUSTER_ID,
|
|
json.dumps(updated, sort_keys=True),
|
|
)
|
|
return updated
|
|
|
|
async def cluster_log(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
limit = int(values(inputs).get("max") or 50)
|
|
rows = await database(request).pool.fetch(
|
|
"""SELECT tl.message, tl.sequence
|
|
FROM task_logs tl
|
|
ORDER BY tl.created_at DESC, tl.sequence DESC
|
|
LIMIT $1""",
|
|
limit,
|
|
)
|
|
return [{"n": int(row["sequence"]), "t": str(row["message"])} for row in reversed(rows)]
|
|
|
|
async def cluster_tasks(_request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
rows = await database(_request).pool.fetch(
|
|
"SELECT upid FROM tasks ORDER BY created_at DESC LIMIT 1000"
|
|
)
|
|
return [{"upid": str(row["upid"])} for row in rows]
|
|
|
|
async def replication_list(request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
metadata = await cluster_metadata(request)
|
|
return _replication_jobs(metadata)
|
|
|
|
async def replication_create(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
from app.handlers.common import require_value
|
|
|
|
payload = values(inputs)
|
|
guest = str(require_value(payload, "guest"))
|
|
target = str(require_value(payload, "target"))
|
|
job_id = str(payload.get("id") or f"repl-{guest.replace(':', '-')}")
|
|
metadata = await cluster_metadata(request)
|
|
jobs = _replication_jobs(metadata)
|
|
if any(str(item.get("id")) == job_id for item in jobs):
|
|
raise ApiError(409, "replication job already exists")
|
|
job = {
|
|
"id": job_id,
|
|
"guest": guest,
|
|
"target": target,
|
|
"type": str(payload.get("type") or "local"),
|
|
"schedule": str(payload.get("schedule") or "*/15"),
|
|
"rate": int(payload.get("rate") or 1),
|
|
"comment": str(payload.get("comment") or ""),
|
|
"enabled": int(payload.get("enabled", 1)),
|
|
}
|
|
jobs.append(job)
|
|
metadata["replication"] = jobs
|
|
await save_cluster_metadata(request, metadata)
|
|
return job
|
|
|
|
async def replication_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
job_id = str(values(inputs)["id"])
|
|
for job in _replication_jobs(await cluster_metadata(request)):
|
|
if str(job.get("id")) == job_id:
|
|
return job
|
|
raise ApiError(404, "replication job does not exist")
|
|
|
|
async def replication_update(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
job_id = str(values(inputs)["id"])
|
|
metadata = await cluster_metadata(request)
|
|
jobs = _replication_jobs(metadata)
|
|
for index, job in enumerate(jobs):
|
|
if str(job.get("id")) != job_id:
|
|
continue
|
|
payload = values(inputs)
|
|
updated = {
|
|
**job,
|
|
**{
|
|
key: value
|
|
for key, value in payload.items()
|
|
if key not in {"id", "delete", "digest"}
|
|
},
|
|
}
|
|
jobs[index] = updated
|
|
metadata["replication"] = jobs
|
|
await save_cluster_metadata(request, metadata)
|
|
return updated
|
|
raise ApiError(404, "replication job does not exist")
|
|
|
|
async def replication_delete(request: Request, inputs: dict[str, Any]) -> None:
|
|
job_id = str(values(inputs)["id"])
|
|
metadata = await cluster_metadata(request)
|
|
jobs = _replication_jobs(metadata)
|
|
remaining = [job for job in jobs if str(job.get("id")) != job_id]
|
|
if len(remaining) == len(jobs):
|
|
raise ApiError(404, "replication job does not exist")
|
|
metadata["replication"] = remaining
|
|
await save_cluster_metadata(request, metadata)
|
|
|
|
registry.register("/cluster", "GET", cluster_index)
|
|
registry.register("/cluster/status", "GET", cluster_status)
|
|
registry.register("/cluster/nextid", "GET", cluster_nextid)
|
|
registry.register("/cluster/options", "GET", cluster_options_get)
|
|
registry.register("/cluster/options", "PUT", cluster_options_put)
|
|
registry.register("/cluster/log", "GET", cluster_log)
|
|
registry.register("/cluster/tasks", "GET", cluster_tasks)
|
|
registry.register("/cluster/replication", "GET", replication_list)
|
|
registry.register("/cluster/replication", "POST", replication_create)
|
|
registry.register("/cluster/replication/{id}", "GET", replication_get)
|
|
registry.register("/cluster/replication/{id}", "PUT", replication_update)
|
|
registry.register("/cluster/replication/{id}", "DELETE", replication_delete)
|