Files
vmware-api-simulator/app/main.py
T
inecs 229a705987 Add SemVer VERSION, Jenkins CI/CD, and release 0.1.1.
Ship Harbor/Hub push with latest+version tags, deploy to vmware.devops.org.ru, and show the package version badge in Help → About.
2026-07-28 00:57:11 +03:00

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()