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

255 lines
8.2 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, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Dict, Optional, List
import yaml
import json
from app.core.config import settings
from app.services.deployment_service import DeploymentService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
deployment_service = DeploymentService()
@router.get("/deploy", response_class=HTMLResponse)
async def deploy_page(request: Request):
"""Страница деплоя"""
# Проверка наличия inventory
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
inventory_exists = inventory_file.exists()
# Чтение inventory если существует
inventory_content = ""
inventory_data = {}
if inventory_exists:
try:
inventory_content = inventory_file.read_text()
# Парсинг inventory для отображения групп и хостов
inventory_data = parse_inventory(inventory_content)
except Exception:
pass
# Получение списка ролей
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/deploy/index.html",
{
"request": request,
"inventory_exists": inventory_exists,
"inventory_content": inventory_content,
"inventory_data": inventory_data,
"roles": sorted(roles)
}
)
@router.get("/deploy/inventory", response_class=HTMLResponse)
async def inventory_page(request: Request):
"""Страница управления inventory"""
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
inventory_exists = inventory_file.exists()
inventory_content = ""
if inventory_exists:
inventory_content = inventory_file.read_text()
return templates.TemplateResponse(
"pages/deploy/inventory.html",
{
"request": request,
"inventory_exists": inventory_exists,
"inventory_content": inventory_content
}
)
@router.post("/api/v1/deploy/inventory")
async def save_inventory(request: Request):
"""Сохранение inventory файла"""
form_data = await request.form()
content = form_data.get("content", "")
if not content:
return JSONResponse(
status_code=400,
content={"success": False, "message": "Содержимое inventory не может быть пустым"}
)
inventory_dir = settings.PROJECT_ROOT / "inventory"
inventory_dir.mkdir(parents=True, exist_ok=True)
inventory_file = inventory_dir / "hosts.ini"
inventory_file.write_text(content)
return JSONResponse(
content={
"success": True,
"message": "Inventory файл успешно сохранен"
}
)
@router.post("/api/v1/deploy/start")
async def start_deploy(
role_name: Optional[str] = None,
tags: Optional[str] = None,
limit: Optional[str] = None,
check: bool = False,
extra_vars: Optional[str] = None
):
"""Запуск деплоя"""
# TODO: Запуск через Celery для фонового выполнения
# Пока просто возвращаем информацию
deploy_id = f"deploy-{role_name or 'all'}-{tags or 'none'}"
return {
"success": True,
"deploy_id": deploy_id,
"role_name": role_name,
"tags": tags,
"limit": limit,
"check": check,
"message": "Деплой запущен"
}
@router.websocket("/ws/deploy/{deploy_id}")
async def deploy_websocket(websocket: WebSocket, deploy_id: str):
"""WebSocket для live логов деплоя"""
await websocket.accept()
try:
# Ждем сообщение от клиента с параметрами деплоя
message = await websocket.receive_json()
if message.get("type") != "start":
await websocket.send_json({
"type": "error",
"data": "Ожидается сообщение типа 'start' с параметрами деплоя"
})
await websocket.close()
return
# Получаем параметры из сообщения
role_name = message.get("role_name")
inventory = message.get("inventory", "inventory/hosts.ini")
limit = message.get("limit")
tags = message.get("tags")
check = message.get("check", False)
extra_vars = message.get("extra_vars")
if not role_name:
await websocket.send_json({
"type": "error",
"data": "Не указано имя роли"
})
await websocket.close()
return
# Отправка начального сообщения
await websocket.send_json({
"type": "info",
"data": f"🚀 Запуск деплоя роли '{role_name}'..."
})
if check:
await websocket.send_json({
"type": "warning",
"data": "⚠️ Режим dry-run (--check) - изменения не будут применены"
})
# Запуск деплоя через DeploymentService
async for line in deployment_service.deploy_role(
role_name=role_name,
inventory=inventory,
limit=limit,
tags=tags,
check=check,
extra_vars=extra_vars,
stream=True
):
line = line.rstrip()
if not line:
continue
# Определение типа лога
log_type = deployment_service.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
# Завершение
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Деплой завершен успешно"
})
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
def parse_inventory(content: str) -> Dict:
"""Парсинг inventory файла для отображения структуры"""
result = {
"groups": {},
"hosts": []
}
current_group = None
for line in content.split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
# Группа
if line.startswith("[") and line.endswith("]"):
group_name = line[1:-1]
if ":" in group_name:
group_name = group_name.split(":")[0]
current_group = group_name
if current_group not in result["groups"]:
result["groups"][current_group] = []
# Хост
elif current_group and " " in line:
host_parts = line.split()
host_name = host_parts[0]
result["groups"][current_group].append(host_name)
result["hosts"].append({
"name": host_name,
"group": current_group,
"vars": " ".join(host_parts[1:]) if len(host_parts) > 1 else ""
})
return result