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

365 lines
14 KiB
Python
Raw 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.

"""
Сервис для работы с 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
)