feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
364
app/services/preset_service.py
Normal file
364
app/services/preset_service.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
Сервис для работы с 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
|
||||
)
|
||||
Reference in New Issue
Block a user