""" Главный файл FastAPI приложения Автор: Сергей Антропов Сайт: https://devops.org.ru """ from fastapi import FastAPI, Request, HTTPException, status from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from pathlib import Path import uvicorn import asyncio import traceback from app.core.config import settings from app.api.v1.router import api_router from app.db.session import init_db from app.auth.middleware import AuthMiddleware import logging logger = logging.getLogger(__name__) # Инициализация базы данных init_db() # Создание приложения app = FastAPI( title="DevOpsLab Web Interface", description="Веб-интерфейс для управления Ansible ролями", version="1.0.0", docs_url="/api/docs", redoc_url="/api/redoc" ) # Добавление middleware для аутентификации app.add_middleware(AuthMiddleware) # Запуск миграций при старте (только для PostgreSQL) @app.on_event("startup") async def run_migrations_on_startup(): """Запуск миграций Alembic при старте приложения""" if settings.DATABASE_URL.startswith("sqlite"): return import asyncio # Ждем, пока PostgreSQL будет готов max_retries = 30 for i in range(max_retries): try: from alembic.config import Config from alembic import command alembic_cfg = Config() # Устанавливаем путь к скриптам миграций alembic_cfg.set_main_option("script_location", str(Path(__file__).parent / "alembic")) # Устанавливаем URL базы данных db_url = str(settings.DATABASE_URL).replace("postgresql+asyncpg://", "postgresql://") alembic_cfg.set_main_option("sqlalchemy.url", db_url) # Применяем миграции command.upgrade(alembic_cfg, "head") logger.info("Миграции применены успешно") # Создаем пользователя admin, если его нет from app.db.session import get_async_db from app.services.user_service import UserService async for db in get_async_db(): await UserService.ensure_admin_user(db) break return except Exception as e: if i < max_retries - 1: logger.warning(f"Ожидание PostgreSQL... ({i+1}/{max_retries}): {e}") await asyncio.sleep(2) else: logger.error(f"Ошибка при применении миграций: {e}") # Подключение API роутера app.include_router(api_router) # Статические файлы static_path = Path(__file__).parent / "static" if static_path.exists(): app.mount("/static", StaticFiles(directory=str(static_path)), name="static") # Шаблоны templates_path = Path(__file__).parent / "templates" templates = Jinja2Templates(directory=str(templates_path)) # Главная страница @app.get("/", response_class=HTMLResponse) async def dashboard(request: Request): """Главная страница - Dashboard""" return templates.TemplateResponse( "pages/dashboard.html", {"request": request} ) # Роуты для ролей, тестов, preset'ов, деплоя, экспорта и импорта (подключаются через router) # ВАЖНО: import должен быть ПЕРЕД roles, чтобы /roles/import не обрабатывался как /roles/{role_name} from app.api.v1.endpoints import roles as roles_endpoints, tests as tests_endpoints, presets as presets_endpoints, deploy as deploy_endpoints, export as export_endpoints, auth as auth_endpoints, docker as docker_endpoints, vault as vault_endpoints, k8s as k8s_endpoints, playbooks as playbooks_endpoints, lint as lint_endpoints, profile as profile_endpoints from app.api.v1.endpoints.import_role import router as import_endpoints from app.api.v1.endpoints.dockerfiles_api import router as dockerfiles_endpoints app.include_router(auth_endpoints.router) # Аутентификация app.include_router(import_endpoints) # Первым, чтобы /roles/import работал app.include_router(roles_endpoints.router) app.include_router(tests_endpoints.router) app.include_router(presets_endpoints.router) app.include_router(deploy_endpoints.router) app.include_router(lint_endpoints.router) app.include_router(export_endpoints.router) app.include_router(docker_endpoints.router) app.include_router(vault_endpoints.router) app.include_router(k8s_endpoints.router) app.include_router(playbooks_endpoints.router) app.include_router(dockerfiles_endpoints) app.include_router(profile_endpoints.router) # Health check @app.get("/health") async def health(): """Проверка здоровья приложения""" return {"status": "ok", "version": "1.0.0"} # Обработчики ошибок @app.exception_handler(404) async def not_found_handler(request: Request, exc: HTTPException): """Обработчик 404 ошибки""" if request.url.path.startswith("/api/"): return JSONResponse( status_code=404, content={"detail": "Ресурс не найден"} ) return templates.TemplateResponse( "pages/errors/404.html", {"request": request, "hide_sidebar": True}, status_code=404 ) @app.exception_handler(403) async def forbidden_handler(request: Request, exc: HTTPException): """Обработчик 403 ошибки""" if request.url.path.startswith("/api/"): return JSONResponse( status_code=403, content={"detail": "Доступ запрещен"} ) return templates.TemplateResponse( "pages/errors/403.html", {"request": request, "hide_sidebar": True}, status_code=403 ) @app.exception_handler(401) async def unauthorized_handler(request: Request, exc: HTTPException): """Обработчик 401 ошибки""" if request.url.path.startswith("/api/"): return JSONResponse( status_code=401, content={"detail": "Требуется авторизация"} ) return templates.TemplateResponse( "pages/errors/401.html", {"request": request, "hide_sidebar": True}, status_code=401 ) @app.exception_handler(500) async def internal_server_error_handler(request: Request, exc: Exception): """Обработчик 500 ошибки""" error_detail = None # Показываем детали ошибки в режиме разработки или если включен DEBUG if settings.DEBUG or settings.API_RELOAD: error_detail = traceback.format_exc() logger.error(f"Internal server error: {exc}", exc_info=True) if request.url.path.startswith("/api/"): return JSONResponse( status_code=500, content={ "detail": "Внутренняя ошибка сервера", "error": str(exc) if (settings.DEBUG or settings.API_RELOAD) else None } ) return templates.TemplateResponse( "pages/errors/500.html", { "request": request, "error_detail": error_detail, "hide_sidebar": True }, status_code=500 ) @app.exception_handler(502) async def bad_gateway_handler(request: Request, exc: Exception): """Обработчик 502 ошибки""" if request.url.path.startswith("/api/"): return JSONResponse( status_code=502, content={"detail": "Ошибка шлюза"} ) return templates.TemplateResponse( "pages/errors/502.html", {"request": request, "hide_sidebar": True}, status_code=502 ) @app.exception_handler(503) async def service_unavailable_handler(request: Request, exc: Exception): """Обработчик 503 ошибки""" if request.url.path.startswith("/api/"): return JSONResponse( status_code=503, content={"detail": "Сервис недоступен"} ) return templates.TemplateResponse( "pages/errors/503.html", {"request": request, "hide_sidebar": True}, status_code=503 ) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): """Обработчик общих HTTP исключений""" status_code = exc.status_code # Обрабатываем уже определенные коды ошибок if status_code == 404: return await not_found_handler(request, exc) elif status_code == 403: return await forbidden_handler(request, exc) elif status_code == 401: return await unauthorized_handler(request, exc) # Для остальных кодов используем общий шаблон if request.url.path.startswith("/api/"): return JSONResponse( status_code=status_code, content={"detail": exc.detail} ) error_messages = { 400: ("400", "Неверный запрос", "Запрос содержит некорректные данные."), 405: ("405", "Метод не разрешен", "Используемый HTTP метод не поддерживается для этого ресурса."), 408: ("408", "Истекло время ожидания", "Запрос занял слишком много времени."), 429: ("429", "Слишком много запросов", "Превышен лимит запросов. Попробуйте позже."), } error_code, error_title, error_description = error_messages.get( status_code, (str(status_code), "Ошибка", exc.detail or "Произошла ошибка при обработке запроса.") ) return templates.TemplateResponse( "pages/errors/error.html", { "request": request, "error_code": error_code, "error_title": error_title, "error_message": exc.detail, "error_description": error_description, "hide_sidebar": True }, status_code=status_code ) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): """Обработчик всех необработанных исключений""" error_detail = None # Показываем детали ошибки в режиме разработки или если включен DEBUG if settings.DEBUG or settings.API_RELOAD: error_detail = traceback.format_exc() logger.error(f"Unhandled exception: {exc}", exc_info=True) if request.url.path.startswith("/api/"): return JSONResponse( status_code=500, content={ "detail": "Внутренняя ошибка сервера", "error": str(exc) if (settings.DEBUG or settings.API_RELOAD) else None } ) return templates.TemplateResponse( "pages/errors/500.html", { "request": request, "error_detail": error_detail, "hide_sidebar": True }, status_code=500 ) if __name__ == "__main__": uvicorn.run( "app.main:app", host=settings.API_HOST, port=settings.API_PORT, reload=settings.API_RELOAD, workers=settings.API_WORKERS )