__init__
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
"""FastAPI application factory and Uvicorn server launcher for uvm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import ip_address
|
||||
from pathlib import Path
|
||||
import ssl
|
||||
from typing import Any
|
||||
|
||||
from .app import Application, build_application
|
||||
from .errors import ConfigurationError, UvmError, ValidationError
|
||||
|
||||
|
||||
def create_api(application: Application | None = None, *, host: str | None = None) -> Any:
|
||||
"""Build the HTTP API after validating its intended bind address."""
|
||||
|
||||
if host is None:
|
||||
raise UvmError("create_api requires an explicit host. Start the API with uvm --serve.")
|
||||
|
||||
resolved_application = application or build_application(emit=None)
|
||||
_validate_server_settings(
|
||||
resolved_application.settings.api_token,
|
||||
resolved_application.settings.api_tls_cert,
|
||||
resolved_application.settings.api_tls_key,
|
||||
host,
|
||||
)
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
except ModuleNotFoundError as error:
|
||||
raise UvmError(
|
||||
"FastAPI server support is not installed. Install this project with its dependencies."
|
||||
) from error
|
||||
|
||||
from .routers.health import router as health_router
|
||||
from .routers.installation import router as installation_router
|
||||
from .routers.vms import router as vms_router
|
||||
|
||||
api = FastAPI(
|
||||
title="uvm",
|
||||
version="0.1.0",
|
||||
description="Local Firecracker microVM management API.",
|
||||
)
|
||||
api.state.uvm_application = resolved_application
|
||||
|
||||
@api.exception_handler(UvmError)
|
||||
async def handle_uvm_error(_request: Any, error: UvmError) -> Any:
|
||||
return JSONResponse(
|
||||
status_code=_http_status_for(error),
|
||||
content={"error": {"message": str(error)}},
|
||||
)
|
||||
|
||||
api.include_router(health_router)
|
||||
api.include_router(installation_router)
|
||||
api.include_router(vms_router)
|
||||
return api
|
||||
|
||||
|
||||
def run_server(application: Application, *, host: str, port: int) -> None:
|
||||
"""Run Uvicorn after enforcing the local-management security boundary."""
|
||||
|
||||
settings = application.settings
|
||||
_validate_server_settings(
|
||||
settings.api_token,
|
||||
settings.api_tls_cert,
|
||||
settings.api_tls_key,
|
||||
host,
|
||||
)
|
||||
try:
|
||||
import uvicorn
|
||||
except ModuleNotFoundError as error:
|
||||
raise UvmError(
|
||||
"Uvicorn server support is not installed. Install this project with its dependencies."
|
||||
) from error
|
||||
|
||||
options: dict[str, Any] = {"host": host, "port": port}
|
||||
if settings.api_tls_cert is not None:
|
||||
options["ssl_certfile"] = str(settings.api_tls_cert)
|
||||
options["ssl_keyfile"] = str(settings.api_tls_key)
|
||||
uvicorn.run(create_api(application, host=host), **options)
|
||||
|
||||
|
||||
def _is_loopback_host(host: str) -> bool:
|
||||
try:
|
||||
return ip_address(host).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_server_settings(
|
||||
api_token: str | None,
|
||||
cert: Path | None,
|
||||
key: Path | None,
|
||||
host: str,
|
||||
) -> None:
|
||||
if not api_token:
|
||||
raise UvmError("UVM_API_TOKEN is required before starting the management API")
|
||||
if (cert is None) != (key is None):
|
||||
raise UvmError("UVM_API_TLS_CERT and UVM_API_TLS_KEY must be configured together")
|
||||
if cert is not None:
|
||||
assert key is not None
|
||||
if not cert.is_file() or not key.is_file():
|
||||
raise UvmError("configured API TLS certificate or key does not exist")
|
||||
try:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=str(cert), keyfile=str(key))
|
||||
except (OSError, ssl.SSLError) as error:
|
||||
raise UvmError(f"configured API TLS certificate or key is invalid: {error}") from error
|
||||
if not _is_loopback_host(host) and cert is None:
|
||||
raise UvmError(
|
||||
"refusing to bind the API to a non-loopback host without TLS. "
|
||||
"Set UVM_API_TLS_CERT and UVM_API_TLS_KEY or bind behind a TLS reverse proxy."
|
||||
)
|
||||
|
||||
|
||||
def _http_status_for(error: UvmError) -> int:
|
||||
if isinstance(error, (ConfigurationError, ValidationError)):
|
||||
return 422
|
||||
message = str(error)
|
||||
if message.startswith("VM not found:"):
|
||||
return 404
|
||||
if "operation is already in progress" in message:
|
||||
return 409
|
||||
if "needs root" in message:
|
||||
return 403
|
||||
return 500
|
||||
Reference in New Issue
Block a user