""" Исполнитель Ansible команд Автор: Сергей Антропов Сайт: https://devops.org.ru """ import asyncio import subprocess from pathlib import Path from typing import Optional, AsyncGenerator, Dict, List from datetime import datetime from app.core.config import settings class AnsibleExecutor: """Выполнение Ansible команд без использования Makefile""" def __init__(self): self.project_root = settings.PROJECT_ROOT async def run_playbook( self, playbook_path: str, inventory: str, tags: Optional[List[str]] = None, limit: Optional[str] = None, check: bool = False, extra_vars: Optional[Dict] = None, stream: bool = False ) -> AsyncGenerator[str, None]: """ Запуск ansible-playbook Args: playbook_path: Путь к playbook файлу inventory: Путь к inventory файлу tags: Список тегов для фильтрации задач limit: Ограничение на хосты check: Режим dry-run (--check) extra_vars: Дополнительные переменные stream: Если True, возвращает генератор строк Yields: Строки вывода команды """ cmd = ["ansible-playbook", playbook_path, "-i", inventory] if tags: cmd.extend(["--tags", ",".join(tags)]) if limit: cmd.extend(["--limit", limit]) if check: cmd.append("--check") if extra_vars: import json cmd.extend(["-e", json.dumps(extra_vars)]) # Добавляем цветной вывод env = {"ANSIBLE_FORCE_COLOR": "1"} process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=str(self.project_root), env=env ) async for line in process.stdout: yield line.decode('utf-8', errors='replace') await process.wait() async def run_ad_hoc( self, module: str, args: str, inventory: str, hosts: Optional[str] = None, stream: bool = False ) -> AsyncGenerator[str, None]: """ Запуск ad-hoc команды ansible Args: module: Имя модуля (например, 'ping', 'command', 'shell') args: Аргументы модуля inventory: Путь к inventory файлу hosts: Ограничение на хосты (по умолчанию 'all') stream: Если True, возвращает генератор строк Yields: Строки вывода команды """ cmd = ["ansible"] if hosts: cmd.extend([hosts]) else: cmd.append("all") cmd.extend(["-m", module, "-a", args, "-i", inventory]) # Добавляем цветной вывод env = {"ANSIBLE_FORCE_COLOR": "1"} process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=str(self.project_root), env=env ) 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", "unreachable"]): return "error" elif any(word in line_lower for word in ["warning", "warn", "skipping"]): return "warning" elif any(word in line_lower for word in ["changed", "ok", "success"]): return "info" else: return "debug" def parse_play_recap(self, line: str) -> Dict: """Парсинг PLAY RECAP из Ansible вывода""" import re # Пример: "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 {}