Files
cursor_ai/zfs_backup.py

126 lines
4.4 KiB
Python
Executable File
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.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Точка входа ZFS Backup — cron/CLI (PRD v1.8).
Конфигурация: config/config_log.yaml (логирование), config/zfs_backup.yaml (бэкап).
Запуск: python zfs_backup.py
или: zfs-backup (после pip install -e .)
"""
import argparse
import logging
import os
import sys
from typing import Any, Dict, Optional
import yaml
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from modules.logger import setup_root_logger, get_logger
from modules.zfs_backup_ops import backup_server, MAX_RETRIES
DEFAULT_LOG_FILE = "/var/log/zfs_backup.log"
CONFIG_LOG_PATH = os.path.join(SCRIPT_DIR, "config", "config_log.yaml")
def load_log_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"""Загрузка config_log.yaml. При отсутствии — возвращает defaults."""
path = config_path or CONFIG_LOG_PATH
if not os.path.isfile(path):
return {"log_file": DEFAULT_LOG_FILE, "log_level": "INFO"}
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def setup_logging(
log_file: Optional[str] = None,
log_config_path: Optional[str] = None,
) -> logging.Logger:
"""Настройка логирования из config_log.yaml (файл, консоль, Telegram)."""
cfg = load_log_config(log_config_path)
path = log_file or cfg.get("log_file", DEFAULT_LOG_FILE)
if not os.path.isabs(path):
path = os.path.normpath(os.path.join(SCRIPT_DIR, path))
setup_root_logger(
level=cfg.get("log_level", "INFO"),
log_file=path,
script_dir=SCRIPT_DIR,
telegram_config=cfg.get("telegram"),
)
return get_logger("zfs_backup")
def load_config(config_path: str) -> Dict[str, Any]:
"""Загрузка конфигурации из YAML (формат v1.1: servers[].pools[])."""
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
if not config or "servers" not in config:
raise ValueError("В config.yaml должен быть раздел 'servers'")
for i, srv in enumerate(config["servers"]):
if "pools" not in srv or not srv["pools"]:
raise ValueError(f"Сервер [{i}] (name={srv.get('name')}) должен содержать непустой раздел 'pools'")
for j, pool in enumerate(srv["pools"]):
for key in ("source_pool", "target_pool", "datasets"):
if key not in pool:
raise ValueError(f"Сервер [{i}] pool [{j}]: отсутствует поле '{key}'")
return config
def main() -> int:
parser = argparse.ArgumentParser(description="ZFS Backup — бэкап ZFS датасетов по config.yaml")
parser.add_argument(
"--config",
default="config/zfs_backup.yaml",
help="Путь к config (по умолчанию config/zfs_backup.yaml)",
)
parser.add_argument(
"--log-file",
default=None,
help="Путь к лог-файлу (переопределяет config/config_log.yaml)",
)
parser.add_argument(
"--log-config",
default=None,
help="Путь к config_log.yaml (по умолчанию config/config_log.yaml)",
)
args = parser.parse_args()
log = setup_logging(log_file=args.log_file, log_config_path=args.log_config)
config_path = args.config
if not os.path.isabs(config_path):
config_path = os.path.normpath(os.path.join(SCRIPT_DIR, config_path))
if not os.path.isfile(config_path):
log.error("Файл конфигурации не найден: %s", config_path)
return 1
try:
config = load_config(config_path)
except Exception as e:
log.exception("Ошибка загрузки config.yaml: %s", e)
return 1
ssh_defaults = config.get("ssh_defaults") or {}
servers = config["servers"]
for server in servers:
try:
backup_server(server, ssh_defaults, log)
except Exception as e:
log.exception(
"%s: бэкап завершился ошибкой после %s попыток: %s",
server["name"],
MAX_RETRIES,
e,
)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())