0773f721ea
- Scale small/large/big seeds (3×50 / 10×1000 / 20×2000) with proportional backups, snapshots, HA, replication, Ceph capacity, and OSD totals (10 / 100 / 500) plus matching node disks and crush/pg metadata - Enrich handler responses for apt, certificates, qemu/lxc status, storage, SDN, metrics export, and related cluster/node dumps - Flatten nested body_example fields into PARAMS and sync the request body via dotted paths (oVirt-style) - Restyle DATA controls as size cards with full-width Reset to minimal / Refresh stats; unload reloads the minimal cluster
161 lines
6.2 KiB
Python
161 lines
6.2 KiB
Python
"""Resource pool semantic handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
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 database, require_value, state, values
|
|
from app.simulation.seed import CLUSTER_ID, stable_id
|
|
|
|
|
|
async def _pool_members(request: Request, pool_id: uuid.UUID) -> list[dict[str, Any]]:
|
|
rows = await database(request).pool.fetch(
|
|
"""SELECT r.external_id, r.kind, r.state, n.name AS node
|
|
FROM pool_members pm
|
|
JOIN resources r ON r.id = pm.resource_id
|
|
JOIN nodes n ON n.id = r.node_id
|
|
WHERE pm.pool_id=$1
|
|
ORDER BY r.external_id::integer""",
|
|
pool_id,
|
|
)
|
|
members: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
guest_state = state(row["state"])
|
|
kind = str(row["kind"])
|
|
vmid = int(row["external_id"])
|
|
members.append(
|
|
{
|
|
"id": f"{kind}/{vmid}",
|
|
"node": str(row["node"]),
|
|
"type": kind,
|
|
"vmid": vmid,
|
|
"name": str(guest_state.get("name") or f"{kind}-{vmid}"),
|
|
}
|
|
)
|
|
return members
|
|
|
|
|
|
def register_pool_handlers(registry: HandlerRegistry) -> None:
|
|
async def pool_list(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
payload = values(inputs)
|
|
filter_poolid = payload.get("poolid")
|
|
rows = await database(request).pool.fetch(
|
|
"""SELECT id, pool_id, comment, metadata FROM pools
|
|
WHERE ($1::text IS NULL OR pool_id=$1)
|
|
ORDER BY pool_id""",
|
|
str(filter_poolid) if filter_poolid is not None else None,
|
|
)
|
|
result: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
metadata = state(row["metadata"])
|
|
members = await _pool_members(request, row["id"])
|
|
if not members and isinstance(metadata.get("members"), list):
|
|
# Fallback for metadata-only membership before pool_members rows exist.
|
|
for item in metadata["members"]:
|
|
members.append(
|
|
{
|
|
"id": f"qemu/{item}",
|
|
"node": "",
|
|
"type": "qemu",
|
|
"vmid": int(item) if str(item).isdigit() else 0,
|
|
}
|
|
)
|
|
pool_item: dict[str, Any] = {
|
|
"poolid": str(row["pool_id"]),
|
|
"members": members,
|
|
}
|
|
if row["comment"] is not None:
|
|
pool_item["comment"] = str(row["comment"])
|
|
elif metadata.get("comment"):
|
|
pool_item["comment"] = str(metadata["comment"])
|
|
result.append(pool_item)
|
|
return result
|
|
|
|
async def pool_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
items = await pool_list(request, inputs)
|
|
if not items:
|
|
raise ApiError(404, "pool does not exist")
|
|
return items[0]
|
|
|
|
async def pool_create(request: Request, inputs: dict[str, Any]) -> None:
|
|
payload = values(inputs)
|
|
poolid = str(payload["poolid"])
|
|
exists = await database(request).pool.fetchval(
|
|
"SELECT EXISTS(SELECT 1 FROM pools WHERE pool_id=$1)",
|
|
poolid,
|
|
)
|
|
if exists:
|
|
raise ApiError(409, "pool already exists")
|
|
await database(request).pool.execute(
|
|
"""INSERT INTO pools(id, cluster_id, pool_id, comment, metadata)
|
|
VALUES($1, $2, $3, $4, $5::jsonb)""",
|
|
stable_id(f"pool:{poolid}"),
|
|
CLUSTER_ID,
|
|
poolid,
|
|
payload.get("comment"),
|
|
json.dumps({"members": []}, sort_keys=True),
|
|
)
|
|
|
|
async def pool_update(request: Request, inputs: dict[str, Any]) -> None:
|
|
payload = values(inputs)
|
|
poolid = str(payload["poolid"])
|
|
pool_row = await database(request).pool.fetchrow(
|
|
"SELECT id FROM pools WHERE pool_id=$1",
|
|
poolid,
|
|
)
|
|
if pool_row is None:
|
|
raise ApiError(404, "pool does not exist")
|
|
if payload.get("comment") is not None:
|
|
await database(request).pool.execute(
|
|
"UPDATE pools SET comment=$2 WHERE pool_id=$1",
|
|
poolid,
|
|
payload.get("comment"),
|
|
)
|
|
if "vms" in payload:
|
|
vmids = [item.strip() for item in str(payload["vms"]).split(",") if item.strip()]
|
|
for vmid in vmids:
|
|
resource = await database(request).pool.fetchrow(
|
|
"""SELECT id FROM resources
|
|
WHERE kind IN ('qemu', 'lxc') AND external_id=$1""",
|
|
vmid,
|
|
)
|
|
if resource is None:
|
|
continue
|
|
await database(request).pool.execute(
|
|
"""INSERT INTO pool_members(pool_id, resource_id)
|
|
VALUES($1, $2) ON CONFLICT DO NOTHING""",
|
|
pool_row["id"],
|
|
resource["id"],
|
|
)
|
|
if "delete" in payload:
|
|
vmids = [item.strip() for item in str(payload["delete"]).split(",") if item.strip()]
|
|
await database(request).pool.execute(
|
|
"""DELETE FROM pool_members pm USING resources r
|
|
WHERE pm.pool_id=$1 AND pm.resource_id=r.id AND r.external_id = ANY($2::text[])""",
|
|
pool_row["id"],
|
|
vmids,
|
|
)
|
|
|
|
async def pool_delete(request: Request, inputs: dict[str, Any]) -> None:
|
|
poolid = str(require_value(values(inputs), "poolid"))
|
|
status = await database(request).pool.execute(
|
|
"DELETE FROM pools WHERE pool_id=$1",
|
|
poolid,
|
|
)
|
|
if status != "DELETE 1":
|
|
raise ApiError(404, "pool does not exist")
|
|
|
|
registry.register("/pools", "GET", pool_list)
|
|
registry.register("/pools", "POST", pool_create)
|
|
registry.register("/pools", "PUT", pool_update)
|
|
registry.register("/pools", "DELETE", pool_delete)
|
|
registry.register("/pools/{poolid}", "GET", pool_get)
|
|
registry.register("/pools/{poolid}", "PUT", pool_update)
|
|
registry.register("/pools/{poolid}", "DELETE", pool_delete)
|