- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
360 lines
12 KiB
Python
360 lines
12 KiB
Python
"""
|
||
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
|