- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
"""
|
||
Выполнение Makefile команд
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
|
||
import subprocess
|
||
import asyncio
|
||
from typing import Dict, List, Optional, AsyncGenerator
|
||
from pathlib import Path
|
||
from app.core.config import settings
|
||
from datetime import datetime
|
||
import json
|
||
import re
|
||
|
||
|
||
class MakeExecutor:
|
||
"""Выполнение Makefile команд с отслеживанием прогресса"""
|
||
|
||
def __init__(self):
|
||
self.project_root = settings.PROJECT_ROOT
|
||
|
||
def detect_command_type(self, command: str) -> str:
|
||
"""Определение типа команды"""
|
||
if "test" in command:
|
||
return "test"
|
||
elif "deploy" in command:
|
||
return "deploy"
|
||
elif "export" in command:
|
||
return "export"
|
||
elif "import" in command:
|
||
return "import"
|
||
elif "lint" in command:
|
||
return "lint"
|
||
else:
|
||
return "other"
|
||
|
||
def parse_parameters(self, command: str, args: List[str] = None) -> dict:
|
||
"""Парсинг параметров команды"""
|
||
params = {}
|
||
|
||
# Парсинг команды
|
||
parts = command.split()
|
||
if len(parts) > 2:
|
||
params["role_name"] = parts[2]
|
||
if len(parts) > 3:
|
||
params["preset"] = parts[3]
|
||
|
||
# Парсинг аргументов
|
||
if args:
|
||
for i, arg in enumerate(args):
|
||
if arg.startswith("--"):
|
||
key = arg[2:].replace("-", "_")
|
||
if i + 1 < len(args) and not args[i + 1].startswith("--"):
|
||
params[key] = args[i + 1]
|
||
else:
|
||
params[key] = True
|
||
|
||
return params
|
||
|
||
async def execute(
|
||
self,
|
||
command: str,
|
||
args: List[str] = None,
|
||
user: str = None
|
||
) -> Dict:
|
||
"""
|
||
Выполнение команды make
|
||
|
||
Args:
|
||
command: Команда make (например, "role test nginx default")
|
||
args: Дополнительные аргументы
|
||
user: Пользователь, выполнивший команду
|
||
|
||
Returns:
|
||
Словарь с результатами выполнения
|
||
"""
|
||
cmd = ["make"] + command.split() + (args or [])
|
||
|
||
try:
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
cwd=self.project_root,
|
||
timeout=3600 # 1 час максимум
|
||
)
|
||
|
||
return {
|
||
"success": result.returncode == 0,
|
||
"stdout": result.stdout,
|
||
"stderr": result.stderr,
|
||
"returncode": result.returncode,
|
||
"command": " ".join(cmd),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
except subprocess.TimeoutExpired:
|
||
return {
|
||
"success": False,
|
||
"stdout": "",
|
||
"stderr": "Command timeout after 1 hour",
|
||
"returncode": -1,
|
||
"command": " ".join(cmd),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"stdout": "",
|
||
"stderr": str(e),
|
||
"returncode": -1,
|
||
"command": " ".join(cmd),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
|
||
async def execute_stream(
|
||
self,
|
||
command: str,
|
||
args: List[str] = None
|
||
) -> AsyncGenerator[str, None]:
|
||
"""
|
||
Выполнение команды с потоковым выводом для WebSocket
|
||
|
||
Args:
|
||
command: Команда make
|
||
args: Дополнительные аргументы
|
||
|
||
Yields:
|
||
Строки вывода команды
|
||
"""
|
||
cmd = ["make"] + command.split() + (args or [])
|
||
|
||
process = await asyncio.create_subprocess_exec(
|
||
*cmd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.STDOUT,
|
||
cwd=str(self.project_root)
|
||
)
|
||
|
||
async for line in process.stdout:
|
||
yield line.decode('utf-8', errors='replace')
|
||
|
||
await process.wait()
|
||
|
||
def detect_log_level(self, line: str) -> str:
|
||
"""Определение уровня лога из строки"""
|
||
line_lower = line.lower()
|
||
if any(word in line_lower for word in ["error", "failed", "fatal"]):
|
||
return "error"
|
||
elif any(word in line_lower for word in ["warning", "warn"]):
|
||
return "warning"
|
||
elif any(word in line_lower for word in ["changed", "ok"]):
|
||
return "info"
|
||
else:
|
||
return "debug"
|
||
|
||
def parse_play_recap(self, line: str) -> dict:
|
||
"""Парсинг PLAY RECAP из Ansible вывода"""
|
||
# Пример: "web1.example.com : ok=5 changed=3 unreachable=0 failed=0"
|
||
match = re.search(r'(\S+)\s*:\s*ok=(\d+)\s+changed=(\d+).*failed=(\d+)', line)
|
||
if match:
|
||
return {
|
||
"host": match.group(1),
|
||
"ok": int(match.group(2)),
|
||
"changed": int(match.group(3)),
|
||
"failed": int(match.group(4)),
|
||
"status": "success" if int(match.group(4)) == 0 else "failed"
|
||
}
|
||
return {}
|