""" Сервис для импорта ролей из репозиториев и Ansible Galaxy Автор: Сергей Антропов Сайт: https://devops.org.ru """ import shutil import tempfile from pathlib import Path from typing import Dict, List, Optional import yaml from git import Repo from git.exc import GitCommandError from app.core.config import settings import logging import subprocess logger = logging.getLogger(__name__) class ImportService: """Сервис для импорта ролей""" def __init__(self): self.project_root = settings.PROJECT_ROOT self.roles_dir = self.project_root / "roles" async def import_from_git( self, repo_url: str, role_name: Optional[str] = None, branch: str = "main", subdirectory: Optional[str] = None ) -> Dict: """ Импорт роли из Git репозитория Args: repo_url: URL Git репозитория role_name: Имя роли (если не указано, берется из имени репозитория) branch: Ветка для клонирования subdirectory: Поддиректория в репозитории (если роль не в корне) Returns: Информация о результате импорта """ # Определение имени роли if not role_name: # Извлекаем имя из URL repo_name = repo_url.rstrip('/').split('/')[-1].replace('.git', '') # Убираем префиксы типа ansible-role- if repo_name.startswith('ansible-role-'): role_name = repo_name.replace('ansible-role-', '') elif repo_name.startswith('ansible-'): role_name = repo_name.replace('ansible-', '') else: role_name = repo_name role_dir = self.roles_dir / role_name if role_dir.exists(): raise ValueError(f"Роль '{role_name}' уже существует. Используйте другое имя или удалите существующую роль.") # Клонирование репозитория во временную директорию with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) repo_dir = temp_path / "repo" try: repo = Repo.clone_from(repo_url, repo_dir, branch=branch) except GitCommandError as e: raise ValueError(f"Ошибка клонирования репозитория: {str(e)}") # Определение исходной директории роли source_dir = repo_dir if subdirectory: source_dir = repo_dir / subdirectory if not source_dir.exists(): raise ValueError(f"Поддиректория '{subdirectory}' не найдена в репозитории") # Проверка структуры роли if not (source_dir / "tasks").exists() and not (source_dir / "tasks" / "main.yml").exists(): # Проверяем, может быть это роль Ansible Galaxy if (source_dir / "meta" / "main.yml").exists(): # Это роль, но структура может отличаться pass else: raise ValueError("Не найдена структура роли Ansible (tasks/main.yml или meta/main.yml)") # Копирование роли role_dir.mkdir(parents=True, exist_ok=True) # Копируем все стандартные директории и файлы for item in source_dir.iterdir(): if item.name.startswith('.'): continue dest = role_dir / item.name if item.is_dir(): shutil.copytree(item, dest) else: shutil.copy2(item, dest) # Обновление deploy.yml from app.core.make_executor import MakeExecutor executor = MakeExecutor() await executor.execute("update-playbooks") return { "success": True, "role_name": role_name, "repo_url": repo_url, "branch": branch, "message": f"Роль '{role_name}' успешно импортирована из {repo_url}" } async def import_from_galaxy( self, role_name: str, version: Optional[str] = None, namespace: Optional[str] = None ) -> Dict: """ Импорт роли из Ansible Galaxy Args: role_name: Имя роли (может быть с namespace: namespace.role_name) version: Версия роли (опционально) namespace: Namespace роли (опционально) Returns: Информация о результате импорта """ # Парсинг имени роли if '.' in role_name: parts = role_name.split('.', 1) namespace = parts[0] role_name = parts[1] full_role_name = f"{namespace}.{role_name}" if namespace else role_name role_dir = self.roles_dir / role_name if role_dir.exists(): raise ValueError(f"Роль '{role_name}' уже существует") # Создание временной директории with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Установка роли через ansible-galaxy cmd = ["ansible-galaxy", "install", full_role_name] if version: cmd.extend(["--version", version]) cmd.extend(["--roles-path", str(temp_path)]) try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=300 ) if result.returncode != 0: raise ValueError(f"Ошибка установки роли из Galaxy: {result.stderr}") # Поиск установленной роли installed_role_dir = None for item in temp_path.iterdir(): if item.is_dir() and (item / "tasks" / "main.yml").exists(): installed_role_dir = item break if not installed_role_dir: raise ValueError("Роль не найдена после установки из Galaxy") # Копирование роли role_dir.mkdir(parents=True, exist_ok=True) for item in installed_role_dir.iterdir(): dest = role_dir / item.name if item.is_dir(): shutil.copytree(item, dest) else: shutil.copy2(item, dest) # Обновление deploy.yml from app.core.make_executor import MakeExecutor executor = MakeExecutor() await executor.execute("update-playbooks") return { "success": True, "role_name": role_name, "galaxy_role": full_role_name, "version": version, "message": f"Роль '{role_name}' успешно импортирована из Ansible Galaxy" } except subprocess.TimeoutExpired: raise ValueError("Таймаут при установке роли из Galaxy") except Exception as e: raise ValueError(f"Ошибка при импорте из Galaxy: {str(e)}") async def validate_repo(self, repo_url: str, branch: str = "main") -> Dict: """Проверка доступности репозитория""" try: with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) repo_dir = temp_path / "repo" repo = Repo.clone_from(repo_url, repo_dir, branch=branch, depth=1) # Проверка структуры has_tasks = (repo_dir / "tasks" / "main.yml").exists() has_meta = (repo_dir / "meta" / "main.yml").exists() return { "valid": True, "has_tasks": has_tasks, "has_meta": has_meta, "is_role": has_tasks or has_meta } except GitCommandError as e: return { "valid": False, "error": str(e) } except Exception as e: return { "valid": False, "error": str(e) }