"""Migrate roles from filesystem to database Revision ID: 009 Revises: 008 Create Date: 2024-01-06 12:00:00.000000 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql import json import yaml from pathlib import Path from datetime import datetime import os # revision identifiers, used by Alembic. revision = '009' down_revision = '008' branch_labels = None depends_on = None def upgrade() -> None: """Перенос ролей из файловой системы в БД и в alembic/roles/""" connection = op.get_bind() # Определяем пути alembic_dir = Path(__file__).parent.parent roles_dir_alembic = alembic_dir / "roles" roles_dir_alembic.mkdir(exist_ok=True) # Определяем исходную папку с ролями project_root = Path(os.getenv("PROJECT_ROOT", "/workspace")) roles_dir_source = project_root / "roles" # Если исходная папка не найдена, пробуем относительный путь if not roles_dir_source.exists(): # Пробуем найти относительно alembic possible_paths = [ alembic_dir.parent.parent / "roles", Path.cwd() / "roles", Path("/workspace") / "roles" ] for path in possible_paths: if path.exists(): roles_dir_source = path break if not roles_dir_source.exists(): print(f"⚠️ Папка roles не найдена: {roles_dir_source}") return print(f"📁 Исходная папка ролей: {roles_dir_source}") print(f"📁 Целевая папка ролей: {roles_dir_alembic}") # Функция для чтения файла с обработкой ошибок def read_file_safe(file_path: Path) -> str: try: return file_path.read_text(encoding='utf-8') except Exception as e: print(f"⚠️ Ошибка чтения файла {file_path}: {e}") return "" # Функция для сбора всех файлов роли def collect_role_files(role_dir: Path) -> dict: """Собирает все файлы роли в словарь {relative_path: content}""" role_content = {} # Стандартные файлы и папки standard_files = { "tasks/main.yml": "tasks/main.yml", "handlers/main.yml": "handlers/main.yml", "defaults/main.yml": "defaults/main.yml", "vars/main.yml": "vars/main.yml", "meta/main.yml": "meta/main.yml", "README.md": "README.md" } # Читаем стандартные файлы for file_path, key in standard_files.items(): full_path = role_dir / file_path if full_path.exists(): role_content[key] = read_file_safe(full_path) # Читаем все файлы из templates/ templates_dir = role_dir / "templates" if templates_dir.exists(): for template_file in templates_dir.rglob("*"): if template_file.is_file(): rel_path = template_file.relative_to(role_dir) role_content[str(rel_path)] = read_file_safe(template_file) # Читаем все файлы из files/ files_dir = role_dir / "files" if files_dir.exists(): for file_item in files_dir.rglob("*"): if file_item.is_file(): rel_path = file_item.relative_to(role_dir) role_content[str(rel_path)] = read_file_safe(file_item) # Читаем все файлы из library/ (если есть) library_dir = role_dir / "library" if library_dir.exists(): for lib_file in library_dir.rglob("*"): if lib_file.is_file(): rel_path = lib_file.relative_to(role_dir) role_content[str(rel_path)] = read_file_safe(lib_file) return role_content # Функция для извлечения метаданных из meta/main.yml def extract_metadata(role_content: dict) -> tuple: """Извлекает метаданные из meta/main.yml""" meta_content = role_content.get("meta/main.yml", "") if not meta_content: return None, None, None try: meta_data = yaml.safe_load(meta_content) if not meta_data or not isinstance(meta_data, dict): return None, None, None galaxy_info = meta_data.get("galaxy_info", {}) author = galaxy_info.get("author", "") description = galaxy_info.get("description", "") platforms = galaxy_info.get("platforms", []) return author, description, platforms except Exception as e: print(f"⚠️ Ошибка парсинга meta/main.yml: {e}") return None, None, None # Обрабатываем каждую роль migrated_count = 0 for role_dir in roles_dir_source.iterdir(): if not role_dir.is_dir() or role_dir.name.startswith('.'): continue role_name = role_dir.name print(f"📦 Обработка роли: {role_name}") # Собираем все файлы роли role_content = collect_role_files(role_dir) if not role_content: print(f"⚠️ Роль {role_name} не содержит файлов, пропускаем") continue # Извлекаем метаданные author, description, platforms = extract_metadata(role_content) # Копируем роль в alembic/roles/ target_role_dir = roles_dir_alembic / role_name target_role_dir.mkdir(exist_ok=True) # Копируем структуру папок и файлов for rel_path, content in role_content.items(): target_file = target_role_dir / rel_path target_file.parent.mkdir(parents=True, exist_ok=True) try: target_file.write_text(content, encoding='utf-8') except Exception as e: print(f"⚠️ Ошибка записи файла {target_file}: {e}") # Сохраняем в БД try: connection.execute( sa.text(""" INSERT INTO roles (name, description, content, is_global, is_personal, author, platforms, galaxy_info, status, created_at, updated_at) VALUES (:name, :description, :content, :is_global, :is_personal, :author, :platforms, :galaxy_info, :status, :created_at, :updated_at) ON CONFLICT (name) DO NOTHING """), { 'name': role_name, 'description': description or f"Роль {role_name}", 'content': json.dumps(role_content), 'is_global': True, # По умолчанию все роли глобальные 'is_personal': False, 'author': author, 'platforms': json.dumps(platforms) if platforms else None, 'galaxy_info': json.dumps({"galaxy_info": {"author": author, "description": description, "platforms": platforms}}) if author or description or platforms else None, 'status': 'active', 'created_at': datetime.utcnow(), 'updated_at': datetime.utcnow() } ) migrated_count += 1 print(f"✅ Роль {role_name} успешно мигрирована") except Exception as e: print(f"❌ Ошибка при миграции роли {role_name}: {e}") print(f"\n✅ Миграция завершена. Перенесено ролей: {migrated_count}") def downgrade() -> None: """Откат миграции - удаление ролей из БД""" connection = op.get_bind() connection.execute(sa.text("DELETE FROM roles")) print("⚠️ Роли удалены из БД. Файлы в alembic/roles/ остаются для безопасности.")