Files
DevOpsLab/app/api/v1/endpoints/tests.py
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

274 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API endpoints для тестирования ролей
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Dict, Optional
import json
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.molecule_executor import MoleculeExecutor
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))
molecule_executor = MoleculeExecutor()
@router.get("/roles/{role_name}/test", response_class=HTMLResponse)
async def test_role_page(
request: Request,
role_name: str,
db: AsyncSession = Depends(get_async_db)
):
"""Страница тестирования роли"""
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Получение списка preset'ов из БД
presets = await PresetService.get_all_presets(db)
return templates.TemplateResponse(
"pages/roles/test.html",
{
"request": request,
"role_name": role_name,
"presets": presets
}
)
@router.post("/api/v1/roles/{role_name}/test")
async def start_role_test(
role_name: str,
preset: str = "default",
variables: Optional[str] = None
):
"""Запуск теста роли"""
# TODO: Запуск через Celery для фонового выполнения
# Пока просто возвращаем информацию
return {
"success": True,
"role_name": role_name,
"preset": preset,
"message": "Тест запущен",
"test_id": f"{role_name}-{preset}"
}
@router.get("/tests", response_class=HTMLResponse)
async def tests_history_page(
request: Request,
role_name: Optional[str] = None,
page: int = 1,
per_page: int = 20,
db: AsyncSession = Depends(get_async_db)
):
"""Страница истории тестов"""
try:
from app.services.history_service import HistoryService
from sqlalchemy import select, func
from app.models.database import CommandHistory, TestResult
history_service = HistoryService()
# Получение тестов из БД
query = select(CommandHistory).where(
CommandHistory.command_type == "test"
)
if role_name:
query = query.where(CommandHistory.command.like(f"%{role_name}%"))
query = query.order_by(CommandHistory.created_at.desc())
# Подсчет общего количества
count_query = select(func.count(CommandHistory.id)).where(
CommandHistory.command_type == "test"
)
if role_name:
count_query = count_query.where(CommandHistory.command.like(f"%{role_name}%"))
total = (await db.execute(count_query)).scalar() or 0
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
page = max(1, min(page, total_pages))
# Пагинация
offset = (page - 1) * per_page
query = query.offset(offset).limit(per_page)
result = await db.execute(query)
tests = result.scalars().all()
# Получение списка ролей для фильтра
roles_dir = settings.PROJECT_ROOT / "roles"
roles = []
if roles_dir.exists():
for role_dir in roles_dir.iterdir():
if role_dir.is_dir() and role_dir.name != "deploy.yml":
roles.append(role_dir.name)
return templates.TemplateResponse(
"pages/tests/index.html",
{
"request": request,
"tests": tests,
"roles": sorted(roles),
"role_name": role_name or "",
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages
}
)
except Exception as e:
import logging
logging.error(f"Error loading tests history: {e}", exc_info=True)
# Если база данных не настроена, показываем пустую страницу
return templates.TemplateResponse(
"pages/tests/index.html",
{
"request": request,
"tests": [],
"roles": [],
"role_name": role_name or "",
"total": 0,
"page": 1,
"per_page": per_page,
"total_pages": 1,
"error": "База данных не настроена или недоступна"
}
)
@router.get("/api/v1/tests/recent")
async def get_recent_tests(limit: int = 10):
"""Получение последних тестов"""
try:
from app.services.history_service import HistoryService
history_service = HistoryService()
tests = history_service.get_command_history(
limit=limit,
command_type="test"
)
return tests
except Exception:
# Если база данных не настроена, возвращаем пустой список
return []
@router.websocket("/ws/test/{test_id}")
async def test_websocket(websocket: WebSocket, test_id: str):
"""WebSocket для live логов тестирования"""
await websocket.accept()
try:
# Парсинг test_id (формат: role_name-preset или role_name-preset-category)
parts = test_id.rsplit("-", 2)
if len(parts) < 2:
await websocket.send_json({
"type": "error",
"data": "Неверный формат test_id. Ожидается: role_name-preset или role_name-preset-category"
})
await websocket.close()
return
role_name = parts[0]
preset_name = parts[1] if len(parts) > 1 else "default"
preset_category = parts[2] if len(parts) > 2 else "main"
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
await websocket.send_json({
"type": "error",
"data": f"Роль '{role_name}' не найдена"
})
await websocket.close()
return
# Получаем preset из БД
async for db in get_async_db():
preset = await PresetService.get_preset(db, preset_name, preset_category)
if not preset:
await websocket.send_json({
"type": "error",
"data": f"Preset '{preset_name}' не найден"
})
await websocket.close()
return
preset_content = preset.content
break
# Отправка начального сообщения
await websocket.send_json({
"type": "info",
"data": f"🚀 Запуск теста роли '{role_name}' с preset '{preset_name}'..."
})
# Создаем временный файл preset'а из БД
molecule_executor.create_temp_preset_file(preset_name, preset_content, preset_category)
# Запуск теста через MoleculeExecutor (без Makefile)
async for line in molecule_executor.test_role(
role_name=role_name,
preset_name=preset_name,
preset_content=preset_content,
preset_category=preset_category,
stream=True
):
# Очистка строки от лишних символов
line = line.rstrip()
if not line:
continue
# Определение типа лога
log_type = molecule_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": "✅ Тест завершен успешно"
})
# Удаляем временный файл
if preset_name in molecule_executor._temp_preset_files:
try:
molecule_executor._temp_preset_files[preset_name].unlink()
del molecule_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