229a705987
Ship Harbor/Hub push with latest+version tags, deploy to vmware.devops.org.ru, and show the package version badge in Help → About.
164 lines
6.5 KiB
Python
164 lines
6.5 KiB
Python
"""FastAPI application factory and ASGI entry point."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import cast
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
from starlette.requests import Request as StarletteRequest
|
|
|
|
from app.api.errors import ApiError, api_error_handler, unhandled_exception_handler
|
|
from app.api.middleware import HeadAsGetMiddleware, RequestContextMiddleware
|
|
from app.api.openapi import openapi_tag_metadata
|
|
from app.api.registry import HandlerRegistry
|
|
from app.config import Settings, get_settings
|
|
from app.contracts.model import Snapshot
|
|
from app.contracts.runtime import apply_runtime_contract, contract_store_root
|
|
from app.db.pool import AsyncpgDatabase, Database
|
|
from app.handlers.core import build_core_handlers
|
|
from app.lifespan import DatabaseFactory, WorkerFactory, create_lifespan, default_database_factory
|
|
from app.logging import configure_logging
|
|
from app.observability.health import router as health_router
|
|
from app.simulation.clock import AcceleratedClock
|
|
from app.tasks.backup import backup_handler
|
|
from app.tasks.lxc import lxc_handler
|
|
from app.tasks.qemu import qemu_handler
|
|
from app.tasks.repository import TaskRepository
|
|
from app.tasks.worker import TaskWorker
|
|
from app.vsphere.rest import vsphere_rest_router
|
|
from app.vsphere.soap.router import router as vsphere_soap_router
|
|
from app.version import get_app_version
|
|
from app.web.routes import router as web_router
|
|
|
|
|
|
async def _json_http_exception_handler(
|
|
_request: StarletteRequest, exc: StarletteHTTPException
|
|
) -> JSONResponse:
|
|
"""Always return JSON for HTTP errors — never HTML error pages from the app."""
|
|
|
|
detail = exc.detail
|
|
if isinstance(detail, dict):
|
|
# VsphereError and similar already carry the product envelope in detail.
|
|
content: dict | list = {"detail": detail}
|
|
elif isinstance(detail, list):
|
|
content = {"detail": detail}
|
|
else:
|
|
content = {"detail": detail}
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content=content,
|
|
media_type="application/json",
|
|
)
|
|
|
|
|
|
def create_app(
|
|
settings: Settings | None = None,
|
|
database_factory: DatabaseFactory = default_database_factory,
|
|
handlers: HandlerRegistry | None = None,
|
|
worker_factories: tuple[WorkerFactory, ...] | None = None,
|
|
) -> FastAPI:
|
|
"""Create an isolated application instance with explicit resource factories."""
|
|
|
|
resolved = settings or get_settings()
|
|
configure_logging(resolved.log_level)
|
|
resolved_workers = worker_factories
|
|
pve_stub = bool(getattr(resolved, "enable_pve_stub", False) and resolved.contract_snapshot)
|
|
if resolved_workers is None and pve_stub and handlers is None:
|
|
|
|
def task_worker(database: Database) -> TaskWorker:
|
|
adapter = cast(AsyncpgDatabase, database)
|
|
repository = TaskRepository(adapter.pool)
|
|
clock = AcceleratedClock(resolved.simulation_time_scale)
|
|
qemu = qemu_handler(repository, clock)
|
|
lxc = lxc_handler(repository, clock)
|
|
backup = backup_handler(repository, clock)
|
|
return TaskWorker(
|
|
repository,
|
|
"simulator-worker",
|
|
{
|
|
"qemu-clone": qemu,
|
|
"qemu-create": qemu,
|
|
"qemu-delete": qemu,
|
|
"qemu-reboot": qemu,
|
|
"qemu-reset": qemu,
|
|
"qemu-resume": qemu,
|
|
"qemu-shutdown": qemu,
|
|
"qemu-migrate": qemu,
|
|
"qemu-move-disk": qemu,
|
|
"qemu-snapshot-create": qemu,
|
|
"qemu-snapshot-delete": qemu,
|
|
"qemu-snapshot-rollback": qemu,
|
|
"qemu-start": qemu,
|
|
"qemu-stop": qemu,
|
|
"qemu-suspend": qemu,
|
|
"qemu-update": qemu,
|
|
"lxc-clone": lxc,
|
|
"lxc-create": lxc,
|
|
"lxc-delete": lxc,
|
|
"lxc-migrate": lxc,
|
|
"lxc-reboot": lxc,
|
|
"lxc-resume": lxc,
|
|
"lxc-shutdown": lxc,
|
|
"lxc-snapshot-create": lxc,
|
|
"lxc-snapshot-delete": lxc,
|
|
"lxc-snapshot-rollback": lxc,
|
|
"lxc-start": lxc,
|
|
"lxc-stop": lxc,
|
|
"lxc-suspend": lxc,
|
|
"vzdump": backup,
|
|
"aptupdate": backup,
|
|
},
|
|
concurrency=resolved.task_worker_concurrency,
|
|
lease_seconds=resolved.task_lease_seconds,
|
|
)
|
|
|
|
resolved_workers = (task_worker,)
|
|
app = FastAPI(
|
|
title=resolved.app_name,
|
|
version=get_app_version(),
|
|
openapi_tags=openapi_tag_metadata(include_pve=pve_stub),
|
|
lifespan=create_lifespan(resolved, database_factory, resolved_workers or ()),
|
|
)
|
|
app.state.settings = resolved
|
|
app.state.contract_swap_lock = asyncio.Lock()
|
|
app.state.vsphere_contract_major = 9
|
|
app.state.runtime_source_version = "8.0.2"
|
|
app.add_middleware(RequestContextMiddleware, header_name=resolved.request_id_header)
|
|
# Outermost-ish: translate HEAD→GET for all Automation routes (matrix probes).
|
|
app.add_middleware(HeadAsGetMiddleware)
|
|
from app.vsphere.rest.version_gate import VsphereVersionGateMiddleware
|
|
|
|
app.add_middleware(VsphereVersionGateMiddleware)
|
|
app.add_exception_handler(Exception, unhandled_exception_handler)
|
|
app.add_exception_handler(ApiError, api_error_handler)
|
|
app.add_exception_handler(StarletteHTTPException, _json_http_exception_handler)
|
|
# Native vSphere surface (survives contract hot-swap).
|
|
from app.vsphere.soap.pbm import router as vsphere_pbm_router
|
|
|
|
app.include_router(vsphere_rest_router)
|
|
app.include_router(vsphere_soap_router)
|
|
app.include_router(vsphere_pbm_router)
|
|
app.include_router(web_router)
|
|
app.include_router(health_router)
|
|
if pve_stub and resolved.contract_snapshot is not None:
|
|
snapshot = Snapshot.model_validate_json(resolved.contract_snapshot.read_bytes())
|
|
resolved_handlers = handlers or build_core_handlers(resolved)
|
|
apply_runtime_contract(
|
|
app,
|
|
snapshot,
|
|
handlers=resolved_handlers,
|
|
store_root=contract_store_root(resolved),
|
|
fallback=resolved.contract_fallback,
|
|
settings=resolved,
|
|
require_evidence_match=True,
|
|
register_admin=True,
|
|
)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|