Files
ovirt-api-simulator/app/web/routes.py
T
inecs 540675597a Add SemVer VERSION, Jenkins CI/CD, and release 0.1.1.
Ship Harbor/Hub push with latest+version tags, deploy to ovirt.devops.org.ru, and show the package version badge in Help → About.
2026-07-28 00:56:52 +03:00

265 lines
9.1 KiB
Python

"""Browser console for exercising the oVirt Engine API simulator."""
from __future__ import annotations
from typing import Annotated
from asyncpg import Pool # type: ignore[import-untyped]
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, JSONResponse
from app.db.pool import AsyncpgDatabase
from app.dependencies import get_database
from app.ovirt.demo_datacenter import CLUSTER_SIZES, normalize_cluster_size, seed_ovirt_demo
from app.ovirt.seed import clear_ovirt_state, ovirt_demo_summary, seed_ovirt
from app.version import get_app_version
from app.web.assets import console_html
router = APIRouter(tags=["Simulator"])
@router.get("/ui/static/{name}", include_in_schema=False)
async def ui_static(name: str):
from fastapi.responses import FileResponse
from app.web.assets import static_path
path = static_path(name)
if path is None:
raise HTTPException(status_code=404, detail="not found")
return FileResponse(path)
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
@router.get("/console", response_class=HTMLResponse, include_in_schema=True)
async def console() -> HTMLResponse:
"""Interactive API console and datacenter overview."""
return HTMLResponse(
console_html(),
headers={"Cache-Control": "no-store"},
)
@router.get("/ui/api/versions", include_in_schema=False)
async def ui_versions(request: Request) -> JSONResponse:
from app.web.ovirt_catalog import ovirt_series_majors
runtime_version = _runtime_version(request)
majors = ovirt_series_majors(runtime_version)
return JSONResponse(
{
"app_version": get_app_version(),
"majors": majors,
"runtime_version": runtime_version,
"default_major": next(
(m["major"] for m in majors if m.get("active")),
next((m["major"] for m in majors if m["series"] == "4.5"), 45),
),
}
)
@router.get("/ui/api/catalog", include_in_schema=False)
async def ui_catalog(
request: Request,
major: Annotated[int, Query(ge=30, le=50)],
) -> JSONResponse:
from app.web.ovirt_catalog import ovirt_catalog_payload
try:
return JSONResponse(ovirt_catalog_payload(major))
except FileNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
@router.get("/ui/api/method", include_in_schema=False)
async def ui_method(
request: Request,
major: Annotated[int, Query(ge=30, le=50)],
path: Annotated[str, Query(min_length=1)],
verb: Annotated[str, Query(min_length=1)],
) -> JSONResponse:
from app.web.ovirt_catalog import ovirt_method_payload
return JSONResponse(
ovirt_method_payload(
major=major,
path=path,
verb=verb,
runtime_version=_runtime_version(request),
)
)
@router.get("/ui/api/compatibility", include_in_schema=False)
async def ui_compatibility(
request: Request,
major: Annotated[int, Query(ge=30, le=50)],
) -> JSONResponse:
from app.web.ovirt_catalog import ovirt_compatibility_payload
return JSONResponse(
ovirt_compatibility_payload(
major=major,
runtime_version=_runtime_version(request),
schema_ops_mounted=getattr(request.app.state, "ovirt_schema_ops", None),
)
)
@router.post("/ui/api/contract/apply", include_in_schema=False)
async def ui_contract_apply(
request: Request,
major: Annotated[int, Query(ge=30, le=50)],
) -> JSONResponse:
"""Hot-swap the in-memory runtime Engine series pack."""
from app.ovirt.contract_loader import series_for_major
from app.ovirt.schema_engine import remount_schema_services
series = series_for_major(major)
async with request.app.state.contract_swap_lock:
try:
ops = remount_schema_services(request.app, series)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return JSONResponse(
{
"ok": True,
"major": major,
"series": series,
"runtime_version": f"ovirt-{series}",
"method_count": ops,
"operation_count": ops,
}
)
@router.get("/ui/api/demo/state", include_in_schema=False)
async def ui_demo_state(request: Request) -> JSONResponse:
pool = _database_pool(request)
async with pool.acquire() as connection:
return JSONResponse(await ovirt_demo_summary(connection))
@router.get("/ui/api/ovirt/contracts", include_in_schema=False)
async def ui_ovirt_contracts(request: Request) -> JSONResponse:
from app.ovirt.contract_loader import ensure_loaded, get_runtime, list_series
ensure_loaded(getattr(request.app.state, "ovirt_series", "4.5"))
runtime = get_runtime()
return JSONResponse(
{
"active": runtime.summary(),
"available": list_series(),
"schema_ops_mounted": getattr(request.app.state, "ovirt_schema_ops", 0),
}
)
@router.post("/ui/api/ovirt/contracts/activate", include_in_schema=False)
async def ui_ovirt_contracts_activate(request: Request) -> JSONResponse:
from app.ovirt.schema_engine import remount_schema_services
payload = await request.json()
series = str(payload.get("series") or "").lower().strip()
if not series:
raise HTTPException(status_code=400, detail="series is required")
async with request.app.state.contract_swap_lock:
try:
ops = remount_schema_services(request.app, series)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return JSONResponse(
{"ok": True, "runtime_version": f"ovirt-{series}", "series": series, "operation_count": ops}
)
@router.post("/ui/api/demo/load", include_in_schema=False)
async def ui_demo_load(request: Request) -> JSONResponse:
"""Load a sized demo cluster: small (3h/50vm), large (10h/1000vm), big (30h/2000vm)."""
size_raw = request.query_params.get("size") or request.query_params.get("profile")
if size_raw is None:
try:
body = await request.json()
except Exception:
body = {}
if isinstance(body, dict):
size_raw = body.get("size") or body.get("profile")
try:
size = normalize_cluster_size(str(size_raw) if size_raw else "large")
except ValueError as error:
raise HTTPException(
status_code=400,
detail=f"{error}; sizes: {', '.join(sorted(CLUSTER_SIZES))}",
) from error
pool = _database_pool(request)
try:
async with pool.acquire() as connection:
async with connection.transaction():
result = await seed_ovirt_demo(connection, size=size)
summary = await ovirt_demo_summary(connection)
except Exception as error:
raise HTTPException(
status_code=500, detail=f"failed to load oVirt demo datacenter: {error}"
) from error
return JSONResponse({"ok": True, "profile": result["profile"], "summary": summary, "seed": result})
@router.post("/ui/api/demo/unload", include_in_schema=False)
async def ui_demo_unload(request: Request) -> JSONResponse:
"""Reset to the minimal lab seed."""
pool = _database_pool(request)
try:
async with pool.acquire() as connection:
async with connection.transaction():
await clear_ovirt_state(connection)
result = await seed_ovirt(connection)
summary = await ovirt_demo_summary(connection)
except Exception as error:
raise HTTPException(
status_code=500, detail=f"failed to remove demo data: {error}"
) from error
return JSONResponse({"ok": True, "profile": result.get("profile", "minimal"), "summary": summary})
@router.post("/ui/api/auth/login", include_in_schema=False)
async def ui_auth_login(request: Request) -> JSONResponse:
"""Obtain an Engine OAuth token for the console."""
from app.ovirt.auth import issue_oauth_token
payload = await request.json()
username = str(payload.get("username") or "admin@internal")
password = str(payload.get("password") or "")
pool = _database_pool(request)
async with pool.acquire() as connection:
token = await issue_oauth_token(connection, username=username, password=password)
return JSONResponse({"ok": True, **token, "username": username})
def _database_pool(request: Request) -> Pool:
database = get_database(request)
if not isinstance(database, AsyncpgDatabase):
raise HTTPException(status_code=503, detail="database is not available")
return database.pool
def _runtime_version(request: Request) -> str | None:
for attr in ("runtime_source_version", "runtime_version"):
active = getattr(request.app.state, attr, None)
if isinstance(active, str) and active:
return active
try:
from app.ovirt.contract_loader import get_runtime
runtime = get_runtime()
if runtime.series:
return f"ovirt-{runtime.series}"
except Exception:
pass
return None