feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
577
app/services/role_service.py
Normal file
577
app/services/role_service.py
Normal file
@@ -0,0 +1,577 @@
|
||||
"""
|
||||
Сервис для работы с ролями
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import yaml
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, or_, and_, func
|
||||
from app.core.config import settings
|
||||
from app.models.database import Role
|
||||
from app.core.make_executor import MakeExecutor
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""Сервис для управления ролями"""
|
||||
|
||||
def __init__(self):
|
||||
self.project_root = settings.PROJECT_ROOT
|
||||
# Папка для хранения ролей (для экспорта и работы с файлами)
|
||||
self.roles_dir_alembic = Path(__file__).parent.parent / "alembic" / "roles"
|
||||
self.roles_dir_alembic.mkdir(exist_ok=True)
|
||||
self.executor = MakeExecutor()
|
||||
|
||||
@staticmethod
|
||||
async def get_role(db: AsyncSession, role_id: int) -> Optional[Role]:
|
||||
"""Получение роли по ID"""
|
||||
result = await db.execute(select(Role).where(Role.id == role_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_role_by_name(db: AsyncSession, name: str) -> Optional[Role]:
|
||||
"""Получение роли по имени"""
|
||||
result = await db.execute(select(Role).where(Role.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def list_roles(
|
||||
db: AsyncSession,
|
||||
user_id: Optional[int] = None,
|
||||
is_global: Optional[bool] = None,
|
||||
is_personal: Optional[bool] = None,
|
||||
groups: Optional[List[str]] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
page: int = 1,
|
||||
per_page: int = 10
|
||||
) -> tuple[List[Role], int]:
|
||||
"""
|
||||
Список ролей с фильтрацией и пагинацией
|
||||
|
||||
Args:
|
||||
db: Сессия БД
|
||||
user_id: ID пользователя (для фильтрации личных ролей)
|
||||
is_global: Фильтр по глобальным ролям
|
||||
is_personal: Фильтр по личным ролям
|
||||
groups: Список групп пользователя (для фильтрации групповых ролей)
|
||||
status: Фильтр по статусу
|
||||
search: Поиск по имени/описанию
|
||||
page: Номер страницы
|
||||
per_page: Количество на странице
|
||||
|
||||
Returns:
|
||||
Кортеж (список ролей, общее количество)
|
||||
"""
|
||||
query = select(Role)
|
||||
|
||||
# Фильтры доступа
|
||||
conditions = []
|
||||
|
||||
# Глобальные роли доступны всем
|
||||
if is_global is None or is_global:
|
||||
conditions.append(Role.is_global == True)
|
||||
|
||||
# Личные роли доступны только владельцу
|
||||
if user_id:
|
||||
conditions.append(
|
||||
and_(
|
||||
Role.is_personal == True,
|
||||
Role.user_id == user_id
|
||||
)
|
||||
)
|
||||
|
||||
# Групповые роли доступны пользователям из соответствующих групп
|
||||
if groups:
|
||||
for group in groups:
|
||||
conditions.append(
|
||||
and_(
|
||||
Role.is_global == False,
|
||||
Role.is_personal == False,
|
||||
Role.groups.contains([group])
|
||||
)
|
||||
)
|
||||
|
||||
if conditions:
|
||||
query = query.where(or_(*conditions))
|
||||
|
||||
# Фильтр по статусу
|
||||
if status:
|
||||
query = query.where(Role.status == status)
|
||||
else:
|
||||
query = query.where(Role.status == "active")
|
||||
|
||||
# Поиск
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
or_(
|
||||
Role.name.ilike(search_pattern),
|
||||
Role.description.ilike(search_pattern)
|
||||
)
|
||||
)
|
||||
|
||||
# Подсчет общего количества
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
count_result = await db.execute(count_query)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
# Пагинация
|
||||
offset = (page - 1) * per_page
|
||||
query = query.order_by(Role.name).offset(offset).limit(per_page)
|
||||
|
||||
result = await db.execute(query)
|
||||
roles = result.scalars().all()
|
||||
|
||||
return roles, total
|
||||
|
||||
@staticmethod
|
||||
async def create_role(
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
template: str = "default",
|
||||
description: str = "",
|
||||
platforms: List[str] = None,
|
||||
variables: List[Dict] = None,
|
||||
is_global: bool = True,
|
||||
is_personal: bool = False,
|
||||
groups: Optional[List[str]] = None,
|
||||
user_id: Optional[int] = None,
|
||||
created_by: Optional[str] = None
|
||||
) -> Role:
|
||||
"""
|
||||
Создание новой роли в БД
|
||||
|
||||
Args:
|
||||
db: Сессия БД
|
||||
name: Имя роли
|
||||
template: Тип шаблона (default, service, package, config, etc.)
|
||||
description: Описание роли
|
||||
platforms: Список поддерживаемых платформ
|
||||
variables: Список переменных для defaults/main.yml
|
||||
is_global: Глобальная роль (доступна всем)
|
||||
is_personal: Личная роль пользователя
|
||||
groups: Список групп, которым доступна роль
|
||||
user_id: ID пользователя-владельца (если is_personal=True)
|
||||
created_by: Имя пользователя, создавшего роль
|
||||
|
||||
Returns:
|
||||
Созданная роль
|
||||
"""
|
||||
# Проверка существования роли
|
||||
existing = await RoleService.get_role_by_name(db, name)
|
||||
if existing:
|
||||
raise ValueError(f"Роль '{name}' уже существует")
|
||||
|
||||
# Генерация содержимого роли
|
||||
role_content = RoleService._generate_role_content(name, template, variables or [])
|
||||
|
||||
# Генерация метаданных
|
||||
author = "Сергей Антропов"
|
||||
galaxy_info = {
|
||||
"galaxy_info": {
|
||||
"author": author,
|
||||
"description": description or f"Роль {name}",
|
||||
"platforms": RoleService._format_platforms(platforms or []),
|
||||
"company": "https://devops.org.ru",
|
||||
"license": "MIT",
|
||||
"min_ansible_version": "2.9"
|
||||
}
|
||||
}
|
||||
|
||||
# Создание роли
|
||||
role = Role(
|
||||
name=name,
|
||||
description=description or f"Роль {name}",
|
||||
content=role_content,
|
||||
is_global=is_global,
|
||||
is_personal=is_personal,
|
||||
groups=groups if groups else None,
|
||||
user_id=user_id if is_personal else None,
|
||||
author=author,
|
||||
platforms=platforms if platforms else None,
|
||||
galaxy_info=galaxy_info,
|
||||
status="active",
|
||||
created_by=created_by,
|
||||
updated_by=created_by
|
||||
)
|
||||
|
||||
db.add(role)
|
||||
await db.commit()
|
||||
await db.refresh(role)
|
||||
|
||||
# Экспорт роли в файловую систему для совместимости
|
||||
await RoleService.export_role_to_filesystem(role)
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
async def update_role(
|
||||
db: AsyncSession,
|
||||
role_id: int,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
content: Optional[Dict] = None,
|
||||
is_global: Optional[bool] = None,
|
||||
is_personal: Optional[bool] = None,
|
||||
groups: Optional[List[str]] = None,
|
||||
user_id: Optional[int] = None,
|
||||
updated_by: Optional[str] = None
|
||||
) -> Optional[Role]:
|
||||
"""Обновление роли"""
|
||||
role = await RoleService.get_role(db, role_id)
|
||||
if not role:
|
||||
return None
|
||||
|
||||
if name is not None:
|
||||
# Проверка уникальности имени
|
||||
existing = await RoleService.get_role_by_name(db, name)
|
||||
if existing and existing.id != role_id:
|
||||
raise ValueError(f"Роль '{name}' уже существует")
|
||||
role.name = name
|
||||
|
||||
if description is not None:
|
||||
role.description = description
|
||||
|
||||
if content is not None:
|
||||
role.content = content
|
||||
|
||||
if is_global is not None:
|
||||
role.is_global = is_global
|
||||
|
||||
if is_personal is not None:
|
||||
role.is_personal = is_personal
|
||||
if is_personal and user_id:
|
||||
role.user_id = user_id
|
||||
elif not is_personal:
|
||||
role.user_id = None
|
||||
|
||||
if groups is not None:
|
||||
role.groups = groups
|
||||
|
||||
if updated_by:
|
||||
role.updated_by = updated_by
|
||||
|
||||
role.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(role)
|
||||
|
||||
# Экспорт обновленной роли в файловую систему
|
||||
await RoleService.export_role_to_filesystem(role)
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
async def update_role_file(
|
||||
db: AsyncSession,
|
||||
role_id: int,
|
||||
file_path: str,
|
||||
content: str,
|
||||
updated_by: Optional[str] = None
|
||||
) -> Optional[Role]:
|
||||
"""Обновление конкретного файла роли"""
|
||||
role = await RoleService.get_role(db, role_id)
|
||||
if not role:
|
||||
return None
|
||||
|
||||
# Обновляем содержимое роли
|
||||
role_content = role.content if isinstance(role.content, dict) else {}
|
||||
role_content[file_path] = content
|
||||
|
||||
role.content = role_content
|
||||
if updated_by:
|
||||
role.updated_by = updated_by
|
||||
role.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(role)
|
||||
|
||||
# Экспорт обновленной роли в файловую систему
|
||||
await RoleService.export_role_to_filesystem(role)
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
async def delete_role(db: AsyncSession, role_id: int) -> bool:
|
||||
"""Удаление роли"""
|
||||
role = await RoleService.get_role(db, role_id)
|
||||
if not role:
|
||||
return False
|
||||
|
||||
await db.delete(role)
|
||||
await db.commit()
|
||||
|
||||
# Удаление из файловой системы
|
||||
role_dir = RoleService().roles_dir_alembic / role.name
|
||||
if role_dir.exists():
|
||||
import shutil
|
||||
shutil.rmtree(role_dir)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def export_role_to_filesystem(role: Role) -> Path:
|
||||
"""
|
||||
Экспорт роли из БД в файловую систему
|
||||
|
||||
Args:
|
||||
role: Роль из БД
|
||||
|
||||
Returns:
|
||||
Путь к директории роли
|
||||
"""
|
||||
service = RoleService()
|
||||
role_dir = service.roles_dir_alembic / role.name
|
||||
role_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Записываем все файлы роли
|
||||
role_content = role.content if isinstance(role.content, dict) else {}
|
||||
for file_path, content in role_content.items():
|
||||
target_file = role_dir / file_path
|
||||
target_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_file.write_text(content, encoding='utf-8')
|
||||
|
||||
return role_dir
|
||||
|
||||
@staticmethod
|
||||
def _generate_role_content(role_name: str, template: str, variables: List[Dict]) -> Dict[str, str]:
|
||||
"""Генерация содержимого роли в виде словаря {file_path: content}"""
|
||||
content = {}
|
||||
|
||||
# Tasks
|
||||
content["tasks/main.yml"] = RoleService._generate_tasks_content(role_name, template)
|
||||
|
||||
# Defaults
|
||||
content["defaults/main.yml"] = RoleService._generate_defaults_content(role_name, variables)
|
||||
|
||||
# Handlers
|
||||
content["handlers/main.yml"] = RoleService._generate_handlers_content(role_name)
|
||||
|
||||
# Meta
|
||||
content["meta/main.yml"] = RoleService._generate_meta_content(role_name, "", [])
|
||||
|
||||
# README
|
||||
content["README.md"] = RoleService._generate_readme_content(role_name, "")
|
||||
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
def _generate_tasks_content(role_name: str, template: str) -> str:
|
||||
"""Генерация содержимого tasks/main.yml"""
|
||||
base_content = f"""---
|
||||
# Задачи для роли {role_name}
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: Пример задачи для роли {role_name}
|
||||
debug:
|
||||
msg: "Роль {role_name} выполнена"
|
||||
"""
|
||||
|
||||
templates_content = {
|
||||
"service": f"""---
|
||||
# Задачи для роли {role_name} (сервис)
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: Установка пакетов
|
||||
package:
|
||||
name: "{{{{ {role_name}_packages | default([]) }}}}"
|
||||
state: present
|
||||
when: {role_name}_enabled | default(true)
|
||||
|
||||
- name: Настройка конфигурации
|
||||
template:
|
||||
src: {role_name}.conf.j2
|
||||
dest: /etc/{role_name}/{role_name}.conf
|
||||
notify: restart {role_name}
|
||||
|
||||
- name: Запуск сервиса
|
||||
systemd:
|
||||
name: {role_name}
|
||||
enabled: true
|
||||
state: started
|
||||
when: {role_name}_enabled | default(true)
|
||||
""",
|
||||
"package": f"""---
|
||||
# Задачи для роли {role_name} (пакеты)
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: Установка пакетов
|
||||
package:
|
||||
name: "{{{{ {role_name}_packages }}}}"
|
||||
state: present
|
||||
when: {role_name}_enabled | default(true)
|
||||
""",
|
||||
"config": f"""---
|
||||
# Задачи для роли {role_name} (конфигурация)
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: Создание директорий
|
||||
file:
|
||||
path: "{{{{ item }}}}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
loop: "{{{{ {role_name}_directories | default([]) }}}}"
|
||||
when: {role_name}_enabled | default(true)
|
||||
|
||||
- name: Копирование конфигурационных файлов
|
||||
copy:
|
||||
src: "{{{{ item.src }}}}"
|
||||
dest: "{{{{ item.dest }}}}"
|
||||
mode: '0644'
|
||||
loop: "{{{{ {role_name}_config_files | default([]) }}}}"
|
||||
when: {role_name}_enabled | default(true)
|
||||
"""
|
||||
}
|
||||
|
||||
return templates_content.get(template, base_content)
|
||||
|
||||
@staticmethod
|
||||
def _generate_defaults_content(role_name: str, variables: List[Dict]) -> str:
|
||||
"""Генерация содержимого defaults/main.yml"""
|
||||
content = f"""---
|
||||
# Переменные по умолчанию для роли {role_name}
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
{role_name}_enabled: true
|
||||
"""
|
||||
|
||||
for var in variables:
|
||||
var_name = var.get("name", "")
|
||||
var_value = var.get("value", "")
|
||||
var_type = var.get("type", "string")
|
||||
|
||||
if var_type == "bool":
|
||||
content += f"{var_name}: {var_value.lower()}\n"
|
||||
elif var_type == "int":
|
||||
content += f"{var_name}: {var_value}\n"
|
||||
elif var_type == "list":
|
||||
content += f"{var_name}: []\n"
|
||||
elif var_type == "dict":
|
||||
content += f"{var_name}: {{}}\n"
|
||||
else:
|
||||
content += f"{var_name}: \"{var_value}\"\n"
|
||||
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
def _generate_handlers_content(role_name: str) -> str:
|
||||
"""Генерация содержимого handlers/main.yml"""
|
||||
return f"""---
|
||||
# Обработчики для роли {role_name}
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: restart {role_name}
|
||||
systemd:
|
||||
name: {role_name}
|
||||
state: restarted
|
||||
when: ansible_facts['service_mgr'] == 'systemd'
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _generate_meta_content(role_name: str, description: str, platforms: List[str]) -> str:
|
||||
"""Генерация содержимого meta/main.yml"""
|
||||
platform_map = {
|
||||
"ubuntu": {"name": "Ubuntu", "versions": ["focal", "jammy", "noble"]},
|
||||
"debian": {"name": "Debian", "versions": ["bullseye", "bookworm"]},
|
||||
"centos": {"name": "CentOS", "versions": ["8", "9"]},
|
||||
"rhel": {"name": "RHEL", "versions": ["8", "9"]},
|
||||
"almalinux": {"name": "AlmaLinux", "versions": ["8", "9"]},
|
||||
"rocky": {"name": "Rocky", "versions": ["8", "9"]},
|
||||
}
|
||||
|
||||
platforms_yaml = []
|
||||
for platform in platforms:
|
||||
if platform in platform_map:
|
||||
platforms_yaml.append(platform_map[platform])
|
||||
|
||||
if not platforms_yaml:
|
||||
platforms_yaml = [
|
||||
{"name": "Ubuntu", "versions": ["focal", "jammy"]},
|
||||
{"name": "Debian", "versions": ["bullseye", "bookworm"]},
|
||||
]
|
||||
|
||||
return f"""---
|
||||
galaxy_info:
|
||||
author: Сергей Антропов
|
||||
description: {description or f"Роль {role_name}"}
|
||||
company: https://devops.org.ru
|
||||
license: MIT
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
{yaml.dump(platforms_yaml, default_flow_style=False, indent=4, allow_unicode=True)}
|
||||
galaxy_tags:
|
||||
- {role_name}
|
||||
|
||||
dependencies: []
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _generate_readme_content(role_name: str, description: str) -> str:
|
||||
"""Генерация содержимого README.md"""
|
||||
return f"""# Роль {role_name}
|
||||
|
||||
{description or f"Роль для настройки и конфигурации {role_name}."}
|
||||
|
||||
## Описание
|
||||
|
||||
{description or "Описание роли"}
|
||||
|
||||
## Требования
|
||||
|
||||
- Ansible >= 2.9
|
||||
- Поддерживаемые ОС: Ubuntu, Debian, CentOS, RHEL, AlmaLinux, Rocky Linux
|
||||
|
||||
## Переменные
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|------------|--------------|----------|
|
||||
| `{role_name}_enabled` | `true` | Включить роль |
|
||||
|
||||
## Примеры использования
|
||||
|
||||
```yaml
|
||||
- hosts: all
|
||||
roles:
|
||||
- {role_name}
|
||||
```
|
||||
|
||||
## Автор
|
||||
|
||||
Сергей Антропов - https://devops.org.ru
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _format_platforms(platforms: List[str]) -> List[Dict]:
|
||||
"""Форматирование списка платформ для Galaxy"""
|
||||
platform_map = {
|
||||
"ubuntu": {"name": "Ubuntu", "versions": ["focal", "jammy", "noble"]},
|
||||
"debian": {"name": "Debian", "versions": ["bullseye", "bookworm"]},
|
||||
"centos": {"name": "CentOS", "versions": ["8", "9"]},
|
||||
"rhel": {"name": "RHEL", "versions": ["8", "9"]},
|
||||
"almalinux": {"name": "AlmaLinux", "versions": ["8", "9"]},
|
||||
"rocky": {"name": "Rocky", "versions": ["8", "9"]},
|
||||
}
|
||||
|
||||
result = []
|
||||
for platform in platforms:
|
||||
if platform in platform_map:
|
||||
result.append(platform_map[platform])
|
||||
|
||||
return result if result else [
|
||||
{"name": "Ubuntu", "versions": ["focal", "jammy"]},
|
||||
{"name": "Debian", "versions": ["bullseye", "bookworm"]},
|
||||
]
|
||||
Reference in New Issue
Block a user