""" API endpoints для управления preset'ами Автор: Сергей Антропов Сайт: https://devops.org.ru """ from fastapi import APIRouter, Request, HTTPException, Form, Depends, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates from pathlib import Path from typing import List, Dict, Optional import yaml import json from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.services.preset_service import PresetService from app.db.session import get_async_db router = APIRouter() templates_path = Path(__file__).parent.parent.parent.parent / "templates" templates = Jinja2Templates(directory=str(templates_path)) @router.get("/presets", response_class=HTMLResponse) async def list_presets( request: Request, page: int = 1, per_page: int = 10, search: Optional[str] = None, category: Optional[str] = None, db: AsyncSession = Depends(get_async_db) ): """Страница списка preset'ов с пагинацией""" presets = await PresetService.get_all_presets(db, category=category) # Фильтрация по поиску if search: search_lower = search.lower() presets = [ p for p in presets if search_lower in p.get("name", "").lower() or search_lower in (p.get("description", "") or "").lower() ] # Пагинация total = len(presets) total_pages = (total + per_page - 1) // per_page if total > 0 else 1 page = max(1, min(page, total_pages)) start = (page - 1) * per_page end = start + per_page paginated_presets = presets[start:end] return templates.TemplateResponse( "pages/presets/list.html", { "request": request, "presets": paginated_presets, "total": total, "page": page, "per_page": per_page, "total_pages": total_pages, "search": search or "", "category": category or "" } ) @router.get("/api/v1/presets", response_model=List[Dict]) async def get_presets_api( category: Optional[str] = None, db: AsyncSession = Depends(get_async_db) ): """API endpoint для получения списка preset'ов""" return await PresetService.get_all_presets(db, category=category) @router.get("/presets/create", response_class=HTMLResponse) async def create_preset_page(request: Request): """Страница создания preset'а""" return templates.TemplateResponse( "pages/presets/create.html", {"request": request} ) @router.get("/api/v1/presets/{preset_name}") async def get_preset_api( preset_name: str, category: str = "main", db: AsyncSession = Depends(get_async_db) ): """Получение preset'а по имени""" preset = await PresetService.get_preset(db, preset_name, category) if not preset: raise HTTPException(status_code=404, detail=f"Preset '{preset_name}' не найден") return { "name": preset.name, "category": preset.category, "description": preset.description, "content": preset.content, "data": yaml.safe_load(preset.content) if preset.content else {} } @router.get("/presets/{preset_name}", response_class=HTMLResponse) async def preset_detail( request: Request, preset_name: str, category: str = "main", db: AsyncSession = Depends(get_async_db) ): """Страница деталей preset'а""" try: preset = await PresetService.get_preset_dict(db, preset_name, category) return templates.TemplateResponse( "pages/presets/detail.html", { "request": request, "preset": preset } ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.post("/api/v1/presets/create") async def create_preset_api( preset_name: str = Form(...), description: str = Form(""), category: str = Form("main"), hosts: str = Form(""), db: AsyncSession = Depends(get_async_db) ): """API endpoint для создания preset'а""" try: hosts_list = [] if hosts: hosts_list = json.loads(hosts) preset = await PresetService.create_preset( db=db, preset_name=preset_name, description=description, hosts=hosts_list, category=category ) return JSONResponse(content={ "success": True, "preset_name": preset.name, "message": f"Preset '{preset_name}' успешно создан" }, status_code=201) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Ошибка при создании preset'а: {str(e)}") @router.get("/presets/{preset_name}/edit", response_class=HTMLResponse) async def edit_preset_page( request: Request, preset_name: str, category: str = "main", db: AsyncSession = Depends(get_async_db) ): """Страница редактирования preset'а""" try: preset = await PresetService.get_preset_dict(db, preset_name, category) return templates.TemplateResponse( "pages/presets/edit.html", { "request": request, "preset": preset } ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.post("/api/v1/presets/{preset_name}/update") async def update_preset_api( preset_name: str, description: str = Form(""), category: str = Form("main"), docker_network: str = Form("labnet"), hosts: str = Form(""), images: str = Form(""), systemd_defaults: str = Form(""), kind_clusters: str = Form(""), content: Optional[str] = Form(None), db: AsyncSession = Depends(get_async_db) ): """API endpoint для обновления preset'а""" try: # Если передан content (YAML), используем старый метод if content: preset = await PresetService.update_preset( db=db, preset_name=preset_name, content=content, category=category ) return JSONResponse(content={ "success": True, "preset_name": preset.name, "message": f"Preset '{preset_name}' успешно обновлен" }) # Новый метод - из формы hosts_list = [] if hosts: hosts_list = json.loads(hosts) images_dict = {} if images: images_dict = json.loads(images) systemd_defaults_dict = {} if systemd_defaults: systemd_defaults_dict = json.loads(systemd_defaults) kind_clusters_list = [] if kind_clusters: kind_clusters_list = json.loads(kind_clusters) preset = await PresetService.update_preset_from_form( db=db, preset_name=preset_name, description=description, category=category, docker_network=docker_network, hosts=hosts_list, images=images_dict, systemd_defaults=systemd_defaults_dict, kind_clusters=kind_clusters_list ) return JSONResponse(content={ "success": True, "preset_name": preset.name, "message": f"Preset '{preset_name}' успешно обновлен" }) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Ошибка при обновлении preset'а: {str(e)}") @router.delete("/api/v1/presets/{preset_name}") async def delete_preset_api( preset_name: str, category: str = "main", db: AsyncSession = Depends(get_async_db) ): """API endpoint для удаления preset'а""" try: await PresetService.delete_preset(db, preset_name, category) return JSONResponse(content={ "success": True, "preset_name": preset_name, "message": f"Preset '{preset_name}' успешно удален" }) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Ошибка при удалении preset'а: {str(e)}") @router.websocket("/ws/preset/test/{preset_name}") async def preset_test_websocket(websocket: WebSocket, preset_name: str, category: str = "main"): """WebSocket для live логов тестирования preset'а""" await websocket.accept() try: # Получаем preset из БД async for db in get_async_db(): preset = await PresetService.get_preset(db, preset_name, category) if not preset: await websocket.send_json({ "type": "error", "data": f"Preset '{preset_name}' не найден" }) await websocket.close() return preset_content = preset.content break # Получаем действие от клиента data = await websocket.receive_json() action = data.get("action", "start") if action == "stop": await websocket.send_json({ "type": "info", "data": "⏹️ Остановка тестирования..." }) await websocket.close() return # Запуск тестирования preset'а from app.core.molecule_executor import MoleculeExecutor executor = MoleculeExecutor() # Создаем временный файл preset'а из БД executor.create_temp_preset_file(preset_name, preset_content, category) await websocket.send_json({ "type": "info", "data": f"🚀 Запуск тестирования preset'а '{preset_name}'..." }) # Запускаем тест (без указания роли - тестируем все роли) async for line in executor.test_role( role_name=None, preset_name=preset_name, preset_content=preset_content, preset_category=category, stream=True ): line = line.rstrip() if not line: continue log_type = executor.detect_log_level(line) await websocket.send_json({ "type": "log", "level": log_type, "data": line }) await websocket.send_json({ "type": "complete", "status": "success", "data": "✅ Тестирование preset'а завершено" }) # Удаляем временный файл if preset_name in executor._temp_preset_files: try: executor._temp_preset_files[preset_name].unlink() del executor._temp_preset_files[preset_name] except: pass except WebSocketDisconnect: pass except Exception as e: import traceback error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}" await websocket.send_json({ "type": "error", "data": error_msg }) finally: try: await websocket.close() except: pass