# [MODULE] Superset Dashboard Backup Script # @contract: Автоматизирует процесс резервного копирования дашбордов Superset из различных окружений. # @semantic_layers: # 1. Инициализация логгера и клиентов Superset. # 2. Выполнение бэкапа для каждого окружения (DEV, SBX, PROD). # 3. Формирование итогового отчета. # @coherence: # - Использует `SupersetClient` для взаимодействия с API Superset. # - Использует `SupersetLogger` для централизованного логирования. # - Работает с `Pathlib` для управления файлами и директориями. # - Интегрируется с `keyring` для безопасного хранения паролей. # [IMPORTS] Стандартная библиотека import logging from datetime import datetime import shutil import os from pathlib import Path # [IMPORTS] Сторонние библиотеки import keyring # [IMPORTS] Локальные модули from superset_tool.models import SupersetConfig from superset_tool.client import SupersetClient from superset_tool.utils.logger import SupersetLogger from superset_tool.utils.fileio import save_and_unpack_dashboard, archive_exports, sanitize_filename,consolidate_archive_folders,remove_empty_directories # [COHERENCE_CHECK_PASSED] Все необходимые модули импортированы и согласованы. # [FUNCTION] setup_clients # @contract: Инициализирует и возвращает SupersetClient для каждого заданного окружения. # @pre: # - `keyring` должен содержать необходимые пароли для "dev migrate", "prod migrate", "sandbox migrate". # - `logger` должен быть инициализирован. # @post: # - Возвращает словарь {env_name: SupersetClient_instance}. # - Логирует успешную инициализацию или ошибку. # @raise: # - `Exception`: При любой ошибке в процессе инициализации клиентов (например, отсутствие пароля в keyring, проблемы с сетью при первой аутентификации). def setup_clients(logger: SupersetLogger): """Инициализация клиентов для разных окружений""" # [ANCHOR] CLIENTS_INITIALIZATION clients = {} try: # [INFO] Инициализация конфигурации для Dev dev_config = SupersetConfig( base_url="https://devta.bi.dwh.rusal.com/api/v1", auth={ "provider": "db", "username": "migrate_user", "password": keyring.get_password("system", "dev migrate"), "refresh": True }, verify_ssl=False ) # [DEBUG] Dev config created: {dev_config.base_url} # [INFO] Инициализация конфигурации для Prod prod_config = SupersetConfig( base_url="https://prodta.bi.dwh.rusal.com/api/v1", auth={ "provider": "db", "username": "migrate_user", "password": keyring.get_password("system", "prod migrate"), "refresh": True }, verify_ssl=False ) # [DEBUG] Prod config created: {prod_config.base_url} # [INFO] Инициализация конфигурации для Sandbox sandbox_config = SupersetConfig( base_url="https://sandboxta.bi.dwh.rusal.com/api/v1", auth={ "provider": "db", "username": "migrate_user", "password": keyring.get_password("system", "sandbox migrate"), "refresh": True }, verify_ssl=False ) # [DEBUG] Sandbox config created: {sandbox_config.base_url} # [INFO] Создание экземпляров SupersetClient clients['dev'] = SupersetClient(dev_config, logger) clients['sbx'] = SupersetClient(sandbox_config,logger) clients['prod'] = SupersetClient(prod_config,logger) logger.info("[COHERENCE_CHECK_PASSED] Клиенты для окружений успешно инициализированы", extra={"envs": list(clients.keys())}) return clients except Exception as e: logger.error(f"[ERROR] Ошибка инициализации клиентов: {str(e)}", exc_info=True) raise # [FUNCTION] backup_dashboards def backup_dashboards(client: SupersetClient, env_name: str, backup_root: Path, logger: SupersetLogger, consolidate: bool = True, rotate_archive: bool = True, clean_folders:bool = True) -> bool: """ [CONTRACT] Выполняет бэкап всех доступных дашбордов для заданного клиента и окружения. @pre: - `client` должен быть инициализированным экземпляром `SupersetClient`. - `env_name` должен быть строкой, обозначающей окружение. - `backup_root` должен быть валидным путем к корневой директории бэкапа. - `logger` должен быть инициализирован. @post: - Дашборды экспортируются и сохраняются в поддиректориях `backup_root/env_name/dashboard_title`. - Старые экспорты архивируются. - Возвращает `True` если все дашборды были экспортированы без критических ошибок, `False` иначе. @side_effects: - Создает директории и файлы в файловой системе. - Логирует статус выполнения, успешные экспорты и ошибки. @exceptions: - `SupersetAPIError`, `NetworkError`, `DashboardNotFoundError`, `ExportError` могут быть подняты методами `SupersetClient` и будут логированы.""" # [ANCHOR] DASHBOARD_BACKUP_PROCESS logger.info(f"[INFO] Запуск бэкапа дашбордов для окружения: {env_name}") logger.debug( "[PARAMS] Флаги: consolidate=%s, rotate_archive=%s, clean_folders=%s", extra={ "consolidate": consolidate, "rotate_archive": rotate_archive, "clean_folders": clean_folders, "env": env_name } ) try: dashboard_count, dashboard_meta = client.get_dashboards() logger.info(f"[INFO] Найдено {dashboard_count} дашбордов для экспорта в {env_name}") if dashboard_count == 0: logger.warning(f"[WARN] Нет дашбордов для экспорта в {env_name}. Процесс завершен.") return True success_count = 0 error_details = [] for db in dashboard_meta: dashboard_id = db.get('id') dashboard_title = db.get('dashboard_title', 'Unknown Dashboard') dashboard_slug = db.get('slug', 'unknown-slug') # Используем slug для уникальности # [PRECONDITION] Проверка наличия ID и slug if not dashboard_id or not dashboard_slug: logger.warning( f"[SKIP] Пропущен дашборд с неполными метаданными: {dashboard_title} (ID: {dashboard_id}, Slug: {dashboard_slug})", extra={'dashboard_meta': db} ) continue logger.debug(f"[DEBUG] Попытка экспорта дашборда: '{dashboard_title}' (ID: {dashboard_id})") try: # [ANCHOR] CREATE_DASHBOARD_DIR # Используем slug в пути для большей уникальности и избежания конфликтов имен dashboard_base_dir_name = sanitize_filename(f"{dashboard_title}") dashboard_dir = backup_root / env_name / dashboard_base_dir_name dashboard_dir.mkdir(parents=True, exist_ok=True) logger.debug(f"[DEBUG] Директория для дашборда: {dashboard_dir}") # [ANCHOR] EXPORT_DASHBOARD_ZIP zip_content, filename = client.export_dashboard(dashboard_id) # [ANCHOR] SAVE_AND_UNPACK # Сохраняем только ZIP-файл, распаковка здесь не нужна для бэкапа save_and_unpack_dashboard( zip_content=zip_content, original_filename=filename, output_dir=dashboard_dir, unpack=False, # Только сохраняем ZIP, не распаковываем для бэкапа logger=logger ) logger.info(f"[INFO] Дашборд '{dashboard_title}' (ID: {dashboard_id}) успешно экспортирован.") if rotate_archive: # [ANCHOR] ARCHIVE_OLD_BACKUPS try: archive_exports(dashboard_dir, logger=logger) logger.debug(f"[DEBUG] Старые экспорты для '{dashboard_title}' архивированы.") except Exception as cleanup_error: logger.warning( f"[WARN] Ошибка архивирования старых бэкапов для '{dashboard_title}': {cleanup_error}", exc_info=False # Не показываем полный traceback для очистки, т.к. это второстепенно ) success_count += 1 except Exception as db_error: error_info = { 'dashboard_id': dashboard_id, 'dashboard_title': dashboard_title, 'error_message': str(db_error), 'env': env_name, 'error_type': type(db_error).__name__ } error_details.append(error_info) logger.error( f"[ERROR] Ошибка экспорта дашборда '{dashboard_title}' (ID: {dashboard_id})", extra=error_info, exc_info=True # Логируем полный traceback для ошибок экспорта ) if consolidate: # [ANCHOR] Объединяем архивы по SLUG в одну папку с максимальной датой try: consolidate_archive_folders(backup_root / env_name , logger=logger) logger.debug(f"[DEBUG] Файлы для '{dashboard_title}' консолидированы.") except Exception as consolidate_error: logger.warning( f"[WARN] Ошибка консолидации файлов для '{backup_root / env_name}': {consolidate_error}", exc_info=False # Не показываем полный traceback для консолидации, т.к. это второстепенно ) if clean_folders: # [ANCHOR] Удаляем пустые папки try: dirs_count = remove_empty_directories(backup_root / env_name , logger=logger) logger.debug(f"[DEBUG] {dirs_count} пустых папок в '{backup_root / env_name }' удалены.") except Exception as clean_error: logger.warning( f"[WARN] Ошибка очистки пустых директорий в '{backup_root / env_name}': {clean_error}", exc_info=False # Не показываем полный traceback для консолидации, т.к. это второстепенно ) if error_details: logger.error( f"[COHERENCE_CHECK_FAILED] Итоги экспорта для {env_name}:", extra={'success_count': success_count, 'errors': error_details, 'total_dashboards': dashboard_count} ) return False else: logger.info( f"[COHERENCE_CHECK_PASSED] Все {success_count} дашбордов для {env_name} успешно экспортированы." ) return True except Exception as e: logger.critical( f"[CRITICAL] Фатальная ошибка бэкапа для окружения {env_name}: {str(e)}", exc_info=True ) return False # [FUNCTION] main # @contract: Основная точка входа скрипта. # @semantic: Координирует инициализацию, выполнение бэкапа и логирование результатов. # @post: # - Возвращает 0 при успешном выполнении, 1 при фатальной ошибке. # @side_effects: # - Инициализирует логгер. # - Вызывает `setup_clients` и `backup_dashboards`. # - Записывает логи в файл и выводит в консоль. def main() -> int: """Основная функция выполнения бэкапа""" # [ANCHOR] MAIN_EXECUTION_START # [CONFIG] Инициализация логгера # @invariant: Логгер должен быть доступен на протяжении всей работы скрипта. log_dir = Path("P:\\Superset\\010 Бекапы\\Logs") # [COHERENCE_NOTE] Убедитесь, что путь доступен. logger = SupersetLogger( log_dir=log_dir, level=logging.INFO, console=True ) logger.info("="*50) logger.info("[INFO] Запуск процесса бэкапа Superset") logger.info("="*50) exit_code = 0 # [STATE] Код выхода скрипта try: # [ANCHOR] CLIENT_SETUP clients = setup_clients(logger) # [CONFIG] Определение корневой директории для бэкапов # @invariant: superset_backup_repo должен быть доступен для записи. superset_backup_repo = Path("P:\\Superset\\010 Бекапы") superset_backup_repo.mkdir(parents=True, exist_ok=True) # Гарантируем существование директории logger.info(f"[INFO] Корневая директория бэкапов: {superset_backup_repo}") # [ANCHOR] BACKUP_DEV_ENVIRONMENT dev_success = backup_dashboards( clients['dev'], "DEV", superset_backup_repo, logger=logger ) # [ANCHOR] BACKUP_SBX_ENVIRONMENT sbx_success = backup_dashboards( clients['sbx'], "SBX", superset_backup_repo, logger=logger ) # [ANCHOR] BACKUP_PROD_ENVIRONMENT prod_success = backup_dashboards( clients['prod'], "PROD", superset_backup_repo, logger=logger ) # [ANCHOR] FINAL_REPORT # [INFO] Итоговый отчет о выполнении бэкапа logger.info("="*50) logger.info("[INFO] Итоги выполнения бэкапа:") logger.info(f"[INFO] DEV: {'Успешно' if dev_success else 'С ошибками'}") logger.info(f"[INFO] SBX: {'Успешно' if sbx_success else 'С ошибками'}") logger.info(f"[INFO] PROD: {'Успешно' if prod_success else 'С ошибками'}") logger.info(f"[INFO] Полный лог доступен в: {log_dir}") if not (dev_success and sbx_success and prod_success): exit_code = 1 logger.warning("[COHERENCE_CHECK_FAILED] Бэкап завершен с ошибками в одном или нескольких окружениях.") else: logger.info("[COHERENCE_CHECK_PASSED] Все бэкапы успешно завершены без ошибок.") except Exception as e: logger.critical(f"[CRITICAL] Фатальная ошибка выполнения скрипта: {str(e)}", exc_info=True) exit_code = 1 logger.info("[INFO] Процесс бэкапа завершен") return exit_code # [ENTRYPOINT] Главная точка запуска скрипта if __name__ == "__main__": exit_code = main() exit(exit_code)