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.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user