777926487b
initial QEMU slice, backed by imported contracts for majors 6–9. - Implement durable handlers for access/auth, cluster, LXC, storage, HA, firewall, Ceph, SDN, ACME, notifications, pools, mapping, and node ops - Serve an interactive Web UI with catalog browsing, demo seed controls, and OpenAPI/help surfaces - Bundle PVE 6.4-15, 7.4-16, and 8.4.5 contract revisions alongside 9.2.3 - Support in-memory runtime contract Apply (POST /ui/api/contract/apply) so /version and /api2 routes follow the selected major until restart - Expand seed profiles (including demo-cluster), migrations 007–008, TLS gateway config, Compose/Makefile tooling, and compatibility evidence - Tighten .gitignore for macOS, hidden directories (.*/), and local secrets
163 lines
6.4 KiB
Python
163 lines
6.4 KiB
Python
"""First read/login semantic service handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, cast
|
|
|
|
from fastapi import Request
|
|
|
|
from app.api.errors import ApiError
|
|
from app.api.registry import HandlerRegistry
|
|
from app.config import Settings
|
|
from app.contracts.runtime import runtime_version_payload
|
|
from app.db.pool import AsyncpgDatabase
|
|
from app.handlers.access import register_access_handlers
|
|
from app.handlers.acme import register_acme_handlers
|
|
from app.handlers.backup import register_backup_handlers
|
|
from app.handlers.ceph import register_ceph_handlers
|
|
from app.handlers.cluster import register_cluster_handlers
|
|
from app.handlers.cluster_config import register_cluster_config_handlers
|
|
from app.handlers.cluster_extra import register_cluster_extra_handlers
|
|
from app.handlers.common import require_node, subdirs
|
|
from app.handlers.firewall import register_firewall_handlers
|
|
from app.handlers.ha import register_ha_handlers
|
|
from app.handlers.legacy_aliases import register_legacy_aliases
|
|
from app.handlers.lxc import register_lxc_handlers
|
|
from app.handlers.mapping import register_mapping_handlers
|
|
from app.handlers.nodes import register_node_ops_handlers
|
|
from app.handlers.nodes_extra import register_nodes_extra_handlers
|
|
from app.handlers.notifications import register_notifications_handlers
|
|
from app.handlers.pools import register_pool_handlers
|
|
from app.handlers.qemu import register_qemu_handlers
|
|
from app.handlers.sdn import register_sdn_handlers
|
|
from app.handlers.storage import register_storage_handlers
|
|
from app.security.auth import csrf_token, issue_ticket, verify_secret
|
|
|
|
|
|
def _database(request: Request) -> AsyncpgDatabase:
|
|
return cast(AsyncpgDatabase, request.app.state.database)
|
|
|
|
|
|
def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
|
registry = HandlerRegistry()
|
|
|
|
async def version(request: Request, _inputs: dict[str, Any]) -> dict[str, str]:
|
|
return runtime_version_payload(request)
|
|
|
|
async def login(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
values = cast(dict[str, Any], inputs["values"])
|
|
username = str(values["username"])
|
|
password = str(values["password"])
|
|
row = await _database(request).pool.fetchrow(
|
|
"SELECT name, password_hash FROM principals WHERE name=$1", username
|
|
)
|
|
if (
|
|
row is None
|
|
or row["password_hash"] is None
|
|
or not verify_secret(password, str(row["password_hash"]))
|
|
):
|
|
raise ApiError(401, "authentication failure")
|
|
key = settings.ticket_signing_key.get_secret_value().encode()
|
|
ticket = issue_ticket(username, key)
|
|
return {
|
|
"username": username,
|
|
"ticket": ticket,
|
|
"CSRFPreventionToken": csrf_token(ticket, key),
|
|
"cap": {"vms": {"VM.Audit": 1, "VM.PowerMgmt": 1}},
|
|
}
|
|
|
|
async def nodes(request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
rows = await _database(request).pool.fetch(
|
|
"SELECT name AS node, status FROM nodes ORDER BY name"
|
|
)
|
|
return [{"node": str(row["node"]), "status": str(row["status"])} for row in rows]
|
|
|
|
async def node_status(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
node = str(cast(dict[str, Any], inputs["values"])["node"])
|
|
row = await _database(request).pool.fetchrow(
|
|
"SELECT name, status FROM nodes WHERE name=$1", node
|
|
)
|
|
if row is None:
|
|
raise ApiError(404, "node does not exist")
|
|
return {
|
|
"status": str(row["status"]),
|
|
"node": str(row["name"]),
|
|
"uptime": 0,
|
|
"cpu": 0.0,
|
|
"memory": {"used": 0, "total": 0},
|
|
}
|
|
|
|
async def resources(request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
rows = await _database(request).pool.fetch(
|
|
"""SELECT r.kind AS type, r.external_id, r.state, n.name AS node
|
|
FROM resources r JOIN nodes n ON n.id=r.node_id
|
|
ORDER BY r.kind, r.external_id"""
|
|
)
|
|
result: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
raw_state = row["state"]
|
|
state = json.loads(raw_state) if isinstance(raw_state, str) else dict(raw_state)
|
|
result.append(
|
|
{
|
|
"type": str(row["type"]),
|
|
"id": f"{row['type']}/{row['external_id']}",
|
|
"node": str(row["node"]),
|
|
**state,
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def node_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
|
|
node = str(cast(dict[str, Any], inputs["values"])["node"])
|
|
await require_node(request, node)
|
|
return subdirs(
|
|
"apt",
|
|
"ceph",
|
|
"disks",
|
|
"firewall",
|
|
"lxc",
|
|
"network",
|
|
"qemu",
|
|
"services",
|
|
"status",
|
|
"storage",
|
|
"tasks",
|
|
"version",
|
|
"vzdump",
|
|
)
|
|
|
|
async def node_version(request: Request, inputs: dict[str, Any]) -> dict[str, str]:
|
|
node = str(cast(dict[str, Any], inputs["values"])["node"])
|
|
await require_node(request, node)
|
|
return runtime_version_payload(request)
|
|
|
|
registry.register("/version", "GET", version)
|
|
registry.register("/access/ticket", "POST", login)
|
|
registry.register("/nodes", "GET", nodes)
|
|
registry.register("/nodes/{node}", "GET", node_index)
|
|
registry.register("/nodes/{node}/status", "GET", node_status)
|
|
registry.register("/nodes/{node}/version", "GET", node_version)
|
|
registry.register("/cluster/resources", "GET", resources)
|
|
register_access_handlers(registry)
|
|
register_cluster_handlers(registry)
|
|
register_notifications_handlers(registry)
|
|
register_mapping_handlers(registry)
|
|
register_acme_handlers(registry)
|
|
register_cluster_config_handlers(registry)
|
|
register_sdn_handlers(registry)
|
|
register_storage_handlers(registry)
|
|
|
|
register_pool_handlers(registry)
|
|
register_ceph_handlers(registry)
|
|
register_backup_handlers(registry)
|
|
register_ha_handlers(registry)
|
|
register_node_ops_handlers(registry)
|
|
register_firewall_handlers(registry)
|
|
register_qemu_handlers(registry)
|
|
register_lxc_handlers(registry)
|
|
register_nodes_extra_handlers(registry)
|
|
register_cluster_extra_handlers(registry)
|
|
register_legacy_aliases(registry)
|
|
return registry
|