Files
ss-tools/backup_script.py
2025-06-30 13:09:25 +03:00

325 lines
17 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.

# [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",
consolidate, rotate_archive, clean_folders,
extra={"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)