""" Сервис для работы с preset'ами Molecule (работа с БД) Автор: Сергей Антропов Сайт: https://devops.org.ru """ from pathlib import Path from typing import Dict, List, Optional import yaml from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.core.config import settings from app.models.database import Preset class PresetService: """Сервис для управления preset'ами из БД""" @staticmethod async def get_all_presets(db: AsyncSession, category: Optional[str] = None) -> List[Dict]: """Получение списка всех preset'ов из БД""" query = select(Preset) if category: query = query.where(Preset.category == category) result = await db.execute(query.order_by(Preset.category, Preset.name)) presets = result.scalars().all() presets_list = [] for preset in presets: # Парсинг YAML для получения информации try: data = yaml.safe_load(preset.content) if preset.content else {} hosts = data.get("hosts", []) images = data.get("images", {}) # Получение групп groups = set() for host in hosts: host_groups = host.get("groups", []) if isinstance(host_groups, list): groups.update(host_groups) presets_list.append({ "name": preset.name, "category": preset.category, "description": preset.description, "hosts_count": len(hosts), "hosts": hosts, "images": list(images.keys()) if images else [], "groups": sorted(list(groups)), "id": preset.id }) except Exception as e: presets_list.append({ "name": preset.name, "category": preset.category, "description": preset.description, "error": str(e), "id": preset.id }) return presets_list @staticmethod async def get_preset(db: AsyncSession, preset_name: str, category: str = "main") -> Optional[Preset]: """Получение preset'а по имени из БД""" result = await db.execute( select(Preset).where(Preset.name == preset_name, Preset.category == category) ) return result.scalar_one_or_none() @staticmethod async def get_preset_dict(db: AsyncSession, preset_name: str, category: str = "main") -> Dict: """Получение preset'а в виде словаря""" preset = await PresetService.get_preset(db, preset_name, category) if not preset: raise ValueError(f"Preset '{preset_name}' не найден") try: data = yaml.safe_load(preset.content) if preset.content else {} except Exception as e: raise ValueError(f"Ошибка парсинга YAML preset'а: {str(e)}") return { "name": preset.name, "category": preset.category, "path": f"presets/{preset.category}/{preset.name}.yml", # Виртуальный путь "content": preset.content, "data": data, "description": preset.description, "id": preset.id } @staticmethod async def create_preset( db: AsyncSession, preset_name: str, description: str = "", hosts: List[Dict] = None, category: str = "main", created_by: Optional[str] = None ) -> Preset: """Создание нового preset'а в БД""" # Проверка существования existing = await PresetService.get_preset(db, preset_name, category) if existing: raise ValueError(f"Preset '{preset_name}' уже существует") # Генерация содержимого preset'а content = PresetService._generate_preset_content(description, hosts or []) # Парсинг для извлечения данных data = yaml.safe_load(content) preset = Preset( name=preset_name, category=category, description=description, content=content, docker_network=data.get("docker_network", "labnet"), hosts=data.get("hosts", []), images=data.get("images", {}), systemd_defaults=data.get("systemd_defaults", {}), kind_clusters=data.get("kind_clusters", []), created_by=created_by ) db.add(preset) await db.commit() await db.refresh(preset) return preset @staticmethod async def update_preset( db: AsyncSession, preset_name: str, content: str, category: str = "main", updated_by: Optional[str] = None ) -> Preset: """Обновление preset'а (старый метод - через YAML)""" preset = await PresetService.get_preset(db, preset_name, category) if not preset: raise ValueError(f"Preset '{preset_name}' не найден") # Валидация YAML try: data = yaml.safe_load(content) except yaml.YAMLError as e: raise ValueError(f"Неверный формат YAML: {str(e)}") # Извлечение описания из комментария description = None for line in content.split('\n'): if line.strip().startswith('#description:'): description = line.split('#description:')[1].strip() break preset.content = content preset.description = description or preset.description preset.docker_network = data.get("docker_network", preset.docker_network) preset.hosts = data.get("hosts", preset.hosts) preset.images = data.get("images", preset.images) preset.systemd_defaults = data.get("systemd_defaults", preset.systemd_defaults) preset.kind_clusters = data.get("kind_clusters", preset.kind_clusters) preset.updated_by = updated_by await db.commit() await db.refresh(preset) return preset @staticmethod async def update_preset_from_form( db: AsyncSession, preset_name: str, description: str = "", category: str = "main", docker_network: str = "labnet", hosts: List[Dict] = None, images: Dict = None, systemd_defaults: Dict = None, kind_clusters: List = None, updated_by: Optional[str] = None ) -> Preset: """Обновление preset'а из формы""" preset = await PresetService.get_preset(db, preset_name, category) if not preset: raise ValueError(f"Preset '{preset_name}' не найден") # Генерация содержимого из формы content = PresetService._generate_preset_content_from_form( description=description, docker_network=docker_network, hosts=hosts or [], images=images or {}, systemd_defaults=systemd_defaults or {}, kind_clusters=kind_clusters or [] ) # Парсинг для обновления полей data = yaml.safe_load(content) preset.content = content preset.description = description preset.docker_network = docker_network preset.hosts = hosts or [] preset.images = images or {} preset.systemd_defaults = systemd_defaults or {} preset.kind_clusters = kind_clusters or [] preset.updated_by = updated_by await db.commit() await db.refresh(preset) return preset @staticmethod async def delete_preset(db: AsyncSession, preset_name: str, category: str = "main") -> bool: """Удаление preset'а из БД""" preset = await PresetService.get_preset(db, preset_name, category) if not preset: raise ValueError(f"Preset '{preset_name}' не найден") await db.delete(preset) await db.commit() return True @staticmethod def _generate_preset_content_from_form( description: str = "", docker_network: str = "labnet", hosts: List[Dict] = None, images: Dict = None, systemd_defaults: Dict = None, kind_clusters: List = None ) -> str: """Генерация содержимого preset'а из формы""" # Базовые образы по умолчанию default_images = { "alt9": "inecs/ansible-lab:alt9-latest", "alt10": "inecs/ansible-lab:alt10-latest", "astra": "inecs/ansible-lab:astra-linux-latest", "rhel": "inecs/ansible-lab:rhel-latest", "centos7": "inecs/ansible-lab:centos7-latest", "centos8": "inecs/ansible-lab:centos8-latest", "centos9": "inecs/ansible-lab:centos9-latest", "alma": "inecs/ansible-lab:alma-latest", "rocky": "inecs/ansible-lab:rocky-latest", "redos": "inecs/ansible-lab:redos-latest", "ubuntu20": "inecs/ansible-lab:ubuntu20-latest", "ubuntu22": "inecs/ansible-lab:ubuntu22-latest", "ubuntu24": "inecs/ansible-lab:ubuntu24-latest", "debian9": "inecs/ansible-lab:debian9-latest", "debian10": "inecs/ansible-lab:debian10-latest", "debian11": "inecs/ansible-lab:debian11-latest", "debian12": "inecs/ansible-lab:debian12-latest" } # Объединяем с переданными образами final_images = {**default_images, **(images or {})} # Systemd defaults по умолчанию default_systemd = { "privileged": True, "command": "/sbin/init", "volumes": ["/sys/fs/cgroup:/sys/fs/cgroup:rw"], "tmpfs": ["/run", "/run/lock"], "capabilities": ["SYS_ADMIN"] } # Объединяем с переданными настройками final_systemd = {**default_systemd, **(systemd_defaults or {})} # Заголовок content = f"""--- #description: {description or "Пользовательский preset"} # Автор: Сергей Антропов # Сайт: https://devops.org.ru docker_network: {docker_network} generated_inventory: "{{{{ molecule_ephemeral_directory }}}}/inventory/hosts.ini" # systemd-ready образы images: """ # Добавление образов for key, value in sorted(final_images.items()): content += f" {key}: \"{value}\"\n" # Systemd defaults content += "\nsystemd_defaults:\n" content += f" privileged: {str(final_systemd.get('privileged', True)).lower()}\n" content += f" command: \"{final_systemd.get('command', '/sbin/init')}\"\n" if final_systemd.get('volumes'): content += " volumes:\n" for vol in final_systemd['volumes']: content += f" - \"{vol}\"\n" if final_systemd.get('tmpfs'): content += " tmpfs: [" content += ", ".join([f'"{v}"' for v in final_systemd['tmpfs']]) content += "]\n" if final_systemd.get('capabilities'): content += " capabilities: [" content += ", ".join([f'"{v}"' for v in final_systemd['capabilities']]) content += "]\n" # Хосты content += "\nhosts:\n" if hosts: for host in hosts: content += f" - name: {host.get('name', 'host1')}\n" content += f" family: {host.get('family', 'ubuntu22')}\n" # Группы host_groups = host.get('groups', []) if isinstance(host_groups, str): host_groups = [g.strip() for g in host_groups.split(',') if g.strip()] if host_groups: content += " groups: [" content += ", ".join([f'"{g}"' for g in host_groups]) content += "]\n" # Дополнительные поля хоста if host.get('type'): content += f" type: {host['type']}\n" if host.get('supported_platforms'): platforms = host['supported_platforms'] if isinstance(platforms, str): platforms = [p.strip() for p in platforms.split(',') if p.strip()] content += " supported_platforms: [" content += ", ".join([f'"{p}"' for p in platforms]) content += "]\n" if host.get('publish'): content += f" publish: {host['publish']}\n" else: # Хост по умолчанию content += """ - name: u1 family: ubuntu22 groups: [test] """ # Kind clusters (для k8s preset'ов) if kind_clusters: content += "\nkind_clusters:\n" for cluster in kind_clusters: content += f" - {cluster}\n" return content @staticmethod def _generate_preset_content(description: str, hosts: List[Dict]) -> str: """Генерация содержимого preset'а""" return PresetService._generate_preset_content_from_form( description=description, docker_network="labnet", hosts=hosts, images=None, systemd_defaults=None, kind_clusters=None )