Files
openstack-api-simulator/app/handlers/cluster.py
T
inecs 6033967e6a Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm
packaging, API contract packs, and pytest/Pulumi coverage labs.
2026-07-18 04:26:48 +03:00

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)