refactor 1st stage
This commit is contained in:
248
backup_script.py
248
backup_script.py
@@ -1,20 +1,48 @@
|
||||
# [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 keyring
|
||||
import os
|
||||
from pathlib import Path
|
||||
from superset_tool.models import SupersetConfig, DatabaseConfig
|
||||
|
||||
# [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
|
||||
# [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:
|
||||
# Конфигурация для Dev
|
||||
# [INFO] Инициализация конфигурации для Dev
|
||||
dev_config = SupersetConfig(
|
||||
base_url="https://devta.bi.dwh.rusal.com/api/v1",
|
||||
auth={
|
||||
@@ -23,11 +51,11 @@ def setup_clients(logger: SupersetLogger):
|
||||
"password": keyring.get_password("system", "dev migrate"),
|
||||
"refresh": True
|
||||
},
|
||||
logger=logger,
|
||||
verify_ssl=False
|
||||
)
|
||||
# [DEBUG] Dev config created: {dev_config.base_url}
|
||||
|
||||
# Конфигурация для Prod
|
||||
# [INFO] Инициализация конфигурации для Prod
|
||||
prod_config = SupersetConfig(
|
||||
base_url="https://prodta.bi.dwh.rusal.com/api/v1",
|
||||
auth={
|
||||
@@ -36,11 +64,11 @@ def setup_clients(logger: SupersetLogger):
|
||||
"password": keyring.get_password("system", "prod migrate"),
|
||||
"refresh": True
|
||||
},
|
||||
logger=logger,
|
||||
verify_ssl=False
|
||||
)
|
||||
# [DEBUG] Prod config created: {prod_config.base_url}
|
||||
|
||||
# Конфигурация для Sandbox
|
||||
# [INFO] Инициализация конфигурации для Sandbox
|
||||
sandbox_config = SupersetConfig(
|
||||
base_url="https://sandboxta.bi.dwh.rusal.com/api/v1",
|
||||
auth={
|
||||
@@ -49,129 +77,215 @@ def setup_clients(logger: SupersetLogger):
|
||||
"password": keyring.get_password("system", "sandbox migrate"),
|
||||
"refresh": True
|
||||
},
|
||||
logger=logger,
|
||||
verify_ssl=False
|
||||
)
|
||||
# [DEBUG] Sandbox config created: {sandbox_config.base_url}
|
||||
|
||||
clients['dev'] = SupersetClient(dev_config)
|
||||
clients['sbx'] = SupersetClient(sandbox_config)
|
||||
clients['prod'] = SupersetClient(prod_config)
|
||||
logger.info("Клиенты для окружений успешно инициализированы")
|
||||
# [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"Ошибка инициализации клиентов: {str(e)}")
|
||||
logger.error(f"[ERROR] Ошибка инициализации клиентов: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def backup_dashboards(client, env_name, backup_root, logger):
|
||||
# [FUNCTION] backup_dashboards
|
||||
# @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` и будут логированы.
|
||||
def backup_dashboards(client: SupersetClient, env_name: str, backup_root: Path, logger: SupersetLogger) -> bool:
|
||||
"""Выполнение бэкапа дашбордов с детальным логированием ошибок"""
|
||||
# [ANCHOR] DASHBOARD_BACKUP_PROCESS
|
||||
logger.info(f"[INFO] Запуск бэкапа дашбордов для окружения: {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"Нет дашбордов для экспорта в {env_name}")
|
||||
logger.warning(f"[WARN] Нет дашбордов для экспорта в {env_name}. Процесс завершен.")
|
||||
return True
|
||||
|
||||
success = 0
|
||||
errors = []
|
||||
|
||||
success_count = 0
|
||||
error_details = []
|
||||
|
||||
for db in dashboard_meta:
|
||||
if not db.get('slug'):
|
||||
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:
|
||||
dashboard_title = db['dashboard_title']
|
||||
dashboard_dir = Path(backup_root) / env_name / sanitize_filename(dashboard_title)
|
||||
# [ANCHOR] CREATE_DASHBOARD_DIR
|
||||
# Используем slug в пути для большей уникальности и избежания конфликтов имен
|
||||
dashboard_base_dir_name = sanitize_filename(f"{dashboard_slug}-{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}")
|
||||
|
||||
zip_content, filename = client.export_dashboard(db['id'])
|
||||
# [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
|
||||
unpack=False, # Только сохраняем ZIP, не распаковываем для бэкапа
|
||||
logger=logger
|
||||
)
|
||||
logger.info(f"[INFO] Дашборд '{dashboard_title}' (ID: {dashboard_id}) успешно экспортирован.")
|
||||
|
||||
# Архивирование старых бэкапов
|
||||
# [ANCHOR] ARCHIVE_OLD_BACKUPS
|
||||
try:
|
||||
archive_exports(dashboard_dir)
|
||||
archive_exports(dashboard_dir, logger=logger)
|
||||
logger.debug(f"[DEBUG] Старые экспорты для '{dashboard_title}' архивированы.")
|
||||
except Exception as cleanup_error:
|
||||
logger.warning(f"Ошибка очистки архива: {cleanup_error}")
|
||||
logger.warning(
|
||||
f"[WARN] Ошибка архивирования старых бэкапов для '{dashboard_title}': {cleanup_error}",
|
||||
exc_info=False # Не показываем полный traceback для очистки, т.к. это второстепенно
|
||||
)
|
||||
|
||||
success += 1
|
||||
success_count += 1
|
||||
|
||||
except Exception as db_error:
|
||||
error_info = {
|
||||
'dashboard': db.get('dashboard_title'),
|
||||
'error': str(db_error),
|
||||
'env': env_name
|
||||
'dashboard_id': dashboard_id,
|
||||
'dashboard_title': dashboard_title,
|
||||
'error_message': str(db_error),
|
||||
'env': env_name,
|
||||
'error_type': type(db_error).__name__
|
||||
}
|
||||
errors.append(error_info)
|
||||
logger.error("Ошибка экспорта дашборда", extra=error_info)
|
||||
error_details.append(error_info)
|
||||
logger.error(
|
||||
f"[ERROR] Ошибка экспорта дашборда '{dashboard_title}' (ID: {dashboard_id})",
|
||||
extra=error_info, exc_info=True # Логируем полный traceback для ошибок экспорта
|
||||
)
|
||||
|
||||
if errors:
|
||||
logger.error(f"Итоги экспорта для {env_name}",
|
||||
extra={'success': success, 'errors': errors, 'total': dashboard_count})
|
||||
|
||||
return len(errors) == 0
|
||||
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"Фатальная ошибка бэкапа {env_name}: {str(e)}", exc_info=True)
|
||||
logger.critical(
|
||||
f"[CRITICAL] Фатальная ошибка бэкапа для окружения {env_name}: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
def main():
|
||||
# Инициализация логгера
|
||||
log_dir = Path("P:\\Superset\\010 Бекапы\\Logs")
|
||||
# [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("Запуск процесса бэкапа Superset")
|
||||
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}")
|
||||
|
||||
# Бэкап для DEV
|
||||
# [ANCHOR] BACKUP_DEV_ENVIRONMENT
|
||||
dev_success = backup_dashboards(
|
||||
clients['dev'],
|
||||
"DEV",
|
||||
clients['dev'],
|
||||
"DEV",
|
||||
superset_backup_repo,
|
||||
logger=logger
|
||||
)
|
||||
|
||||
#Бэкап для Sandbox
|
||||
# [ANCHOR] BACKUP_SBX_ENVIRONMENT
|
||||
sbx_success = backup_dashboards(
|
||||
clients['sbx'],
|
||||
"SBX",
|
||||
clients['sbx'],
|
||||
"SBX",
|
||||
superset_backup_repo,
|
||||
logger=logger
|
||||
)
|
||||
#Бэкап для Прода
|
||||
|
||||
# [ANCHOR] BACKUP_PROD_ENVIRONMENT
|
||||
prod_success = backup_dashboards(
|
||||
clients['prod'],
|
||||
"PROD",
|
||||
clients['prod'],
|
||||
"PROD",
|
||||
superset_backup_repo,
|
||||
logger=logger
|
||||
)
|
||||
|
||||
# Итоговый отчет
|
||||
# [ANCHOR] FINAL_REPORT
|
||||
# [INFO] Итоговый отчет о выполнении бэкапа
|
||||
logger.info("="*50)
|
||||
logger.info("Итоги выполнения бэкапа:")
|
||||
logger.info(f"DEV: {'Успешно' if dev_success else 'С ошибками'}")
|
||||
logger.info(f"SBX: {'Успешно' if sbx_success else 'С ошибками'}")
|
||||
logger.info(f"PROD: {'Успешно' if prod_success else 'С ошибками'}")
|
||||
logger.info(f"Полный лог доступен в: {log_dir}")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Фатальная ошибка выполнения скрипта: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
|
||||
logger.info("Процесс бэкапа завершен")
|
||||
return 0
|
||||
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)
|
||||
Reference in New Issue
Block a user