Compare commits
4 Commits
2655869814
...
feat-backu
| Author | SHA1 | Date | |
|---|---|---|---|
|
a31a07bd16
|
|||
|
54a951b96a
|
|||
|
e1379bc480
|
|||
|
037e0cab9b
|
@@ -39,18 +39,10 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TelegramConfig:
|
|
||||||
type: str
|
|
||||||
telegram_bot_token: str
|
|
||||||
telegram_chat_id: str
|
|
||||||
notifications_name: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
|
host_name: str
|
||||||
roots: List[Path]
|
roots: List[Path]
|
||||||
notifications: Dict[str, TelegramConfig]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -93,8 +85,13 @@ class ResticStorage(Storage):
|
|||||||
if not backup_dirs:
|
if not backup_dirs:
|
||||||
logger.warning("No backup directories found")
|
logger.warning("No backup directories found")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
return self.__backup_internal(backup_dirs)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.error("Restic backup process failed: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __backup_internal(self, backup_dirs: List[str]) -> bool:
|
||||||
logger.info("Starting restic backup")
|
logger.info("Starting restic backup")
|
||||||
logger.info("Destination: %s", self.restic_repository)
|
logger.info("Destination: %s", self.restic_repository)
|
||||||
|
|
||||||
@@ -154,28 +151,67 @@ class ResticStorage(Storage):
|
|||||||
logger.info("Final restic check completed successfully")
|
logger.info("Final restic check completed successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logger.error("Restic backup process failed: %s", exc)
|
class Notifier(ABC):
|
||||||
return False
|
def send(self, html_message: str):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramNotifier(Notifier):
|
||||||
|
TYPE_NAME = "telegram"
|
||||||
|
|
||||||
|
def __init__(self, name: str, params: Dict[str, Any]):
|
||||||
|
self.name = name
|
||||||
|
self.telegram_bot_token = str(params.get("telegram_bot_token", ""))
|
||||||
|
self.telegram_chat_id = str(params.get("telegram_chat_id", ""))
|
||||||
|
if not all(
|
||||||
|
[
|
||||||
|
self.telegram_bot_token,
|
||||||
|
self.telegram_chat_id,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing notification configuration values for backend {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, html_message: str):
|
||||||
|
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
|
||||||
|
data = {
|
||||||
|
"chat_id": self.telegram_chat_id,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
"text": html_message,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, data=data, timeout=30)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info("Telegram notification sent successfully")
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BackupManager:
|
class BackupManager:
|
||||||
def __init__(self, config: Config, storages: List[Storage]):
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
roots: List[Path],
|
||||||
|
storages: List[Storage],
|
||||||
|
notifiers: List[Notifier],
|
||||||
|
):
|
||||||
self.errors: List[str] = []
|
self.errors: List[str] = []
|
||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
self.successful_backups: List[str] = []
|
self.successful_backups: List[str] = []
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.roots: List[Path] = roots
|
||||||
self.storages = storages
|
self.storages = storages
|
||||||
|
self.notifiers = notifiers
|
||||||
def _select_telegram(self) -> Optional[TelegramConfig]:
|
|
||||||
if "telegram" in self.config.notifications:
|
|
||||||
return self.config.notifications["telegram"]
|
|
||||||
return next(iter(self.config.notifications.values()), None)
|
|
||||||
|
|
||||||
def find_applications(self) -> List[Application]:
|
def find_applications(self) -> List[Application]:
|
||||||
"""Get all application directories and their owners."""
|
"""Get all application directories and their owners."""
|
||||||
applications: List[Application] = []
|
applications: List[Application] = []
|
||||||
source_dirs = itertools.chain(*(root.iterdir() for root in self.config.roots))
|
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
||||||
|
|
||||||
for app_dir in source_dirs:
|
for app_dir in source_dirs:
|
||||||
if "lost+found" in str(app_dir):
|
if "lost+found" in str(app_dir):
|
||||||
@@ -306,24 +342,15 @@ class BackupManager:
|
|||||||
|
|
||||||
return backup_dirs
|
return backup_dirs
|
||||||
|
|
||||||
def send_telegram_notification(self, success: bool) -> None:
|
def send_notification(self, success: bool) -> None:
|
||||||
"""Send notification to Telegram"""
|
"""Send notification to Notifiers"""
|
||||||
telegram_cfg = self._select_telegram()
|
|
||||||
if telegram_cfg is None:
|
|
||||||
logger.warning("No telegram notification backend configured")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
if success and not self.errors:
|
if success and not self.errors:
|
||||||
message = (
|
message = f"<b>{self.config.host_name}</b>: бекап успешно завершен!"
|
||||||
f"<b>{telegram_cfg.notifications_name}</b>: бекап успешно завершен!"
|
|
||||||
)
|
|
||||||
if self.successful_backups:
|
if self.successful_backups:
|
||||||
message += (
|
message += f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
|
||||||
f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
message = f"<b>{telegram_cfg.notifications_name}</b>: бекап завершен с ошибками!"
|
message = f"<b>{self.config.host_name}</b>: бекап завершен с ошибками!"
|
||||||
|
|
||||||
if self.successful_backups:
|
if self.successful_backups:
|
||||||
message += (
|
message += (
|
||||||
@@ -336,24 +363,11 @@ class BackupManager:
|
|||||||
if self.errors:
|
if self.errors:
|
||||||
message += f"\n\n❌ Ошибки:\n" + "\n".join(self.errors)
|
message += f"\n\n❌ Ошибки:\n" + "\n".join(self.errors)
|
||||||
|
|
||||||
url = f"https://api.telegram.org/bot{telegram_cfg.telegram_bot_token}/sendMessage"
|
for notificator in self.notifiers:
|
||||||
data = {
|
try:
|
||||||
"chat_id": telegram_cfg.telegram_chat_id,
|
notificator.send(message)
|
||||||
"parse_mode": "HTML",
|
|
||||||
"text": message,
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url, data=data, timeout=30)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
logger.info("Telegram notification sent successfully")
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send Telegram notification: {str(e)}")
|
logger.error(f"Failed to send notification: {str(e)}")
|
||||||
|
|
||||||
def run_backup_process(self) -> bool:
|
def run_backup_process(self) -> bool:
|
||||||
"""Main backup process"""
|
"""Main backup process"""
|
||||||
@@ -397,7 +411,7 @@ class BackupManager:
|
|||||||
overall_success = overall_success and backup_result
|
overall_success = overall_success and backup_result
|
||||||
|
|
||||||
# Send notification
|
# Send notification
|
||||||
self.send_telegram_notification(overall_success)
|
self.send_notification(overall_success)
|
||||||
|
|
||||||
logger.info("Backup process completed")
|
logger.info("Backup process completed")
|
||||||
|
|
||||||
@@ -420,6 +434,8 @@ def initialize(config_path: Path) -> BackupManager:
|
|||||||
logger.error(f"Failed to read config file {config_path}: {e}")
|
logger.error(f"Failed to read config file {config_path}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
host_name = str(raw_config.get("host_name", "unknown"))
|
||||||
|
|
||||||
roots_raw = raw_config.get("roots") or []
|
roots_raw = raw_config.get("roots") or []
|
||||||
if not isinstance(roots_raw, list) or not roots_raw:
|
if not isinstance(roots_raw, list) or not roots_raw:
|
||||||
raise ValueError("roots must be a non-empty list of paths in config.toml")
|
raise ValueError("roots must be a non-empty list of paths in config.toml")
|
||||||
@@ -436,38 +452,23 @@ def initialize(config_path: Path) -> BackupManager:
|
|||||||
if not storages:
|
if not storages:
|
||||||
raise ValueError("At least one storage backend must be configured")
|
raise ValueError("At least one storage backend must be configured")
|
||||||
|
|
||||||
notifications_raw = raw_config.get("notifications") or {}
|
notifications_raw = raw_config.get("notifier") or {}
|
||||||
notifications: Dict[str, TelegramConfig] = {}
|
notifiers: List[Notifier] = []
|
||||||
for name, cfg in notifications_raw.items():
|
for name, params in notifications_raw.items():
|
||||||
if not isinstance(cfg, dict):
|
if not isinstance(params, dict):
|
||||||
raise ValueError(f"Notification config for {name} must be a table")
|
raise ValueError(f"Notificator config for {name} must be a table")
|
||||||
notifications[name] = TelegramConfig(
|
notifier_type = params.get("type", "")
|
||||||
type=cfg.get("type", ""),
|
if notifier_type == TelegramNotifier.TYPE_NAME:
|
||||||
telegram_bot_token=cfg.get("telegram_bot_token", ""),
|
notifiers.append(TelegramNotifier(name, params))
|
||||||
telegram_chat_id=cfg.get("telegram_chat_id", ""),
|
if not notifiers:
|
||||||
notifications_name=cfg.get("notifications_name", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not notifications:
|
|
||||||
raise ValueError("At least one notification backend must be configured")
|
raise ValueError("At least one notification backend must be configured")
|
||||||
|
|
||||||
for name, cfg in notifications.items():
|
config = Config(host_name=host_name, roots=roots)
|
||||||
if not all(
|
|
||||||
[
|
return BackupManager(
|
||||||
cfg.type,
|
config=config, roots=roots, storages=storages, notifiers=notifiers
|
||||||
cfg.telegram_bot_token,
|
|
||||||
cfg.telegram_chat_id,
|
|
||||||
cfg.notifications_name,
|
|
||||||
]
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
f"Missing notification configuration values for backend {name}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
config = Config(roots=roots, notifications=notifications)
|
|
||||||
|
|
||||||
return BackupManager(config=config, storages=storages)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[restic]
|
|
||||||
RESTIC_REPOSITORY={{ restic_repository }}
|
|
||||||
RESTIC_PASSWORD={{ restic_password }}
|
|
||||||
AWS_ACCESS_KEY_ID={{ restic_s3_access_key }}
|
|
||||||
AWS_SECRET_ACCESS_KEY={{ restic_s3_access_secret }}
|
|
||||||
AWS_DEFAULT_REGION={{ restic_s3_region }}
|
|
||||||
|
|
||||||
[telegram]
|
|
||||||
TELEGRAM_BOT_TOKEN={{ notifications_tg_bot_token }}
|
|
||||||
TELEGRAM_CHAT_ID={{ notifications_tg_chat_id }}
|
|
||||||
NOTIFICATIONS_NAME={{ notifications_name }}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
host_name = "{{ notifications_name }}"
|
||||||
|
|
||||||
roots = [
|
roots = [
|
||||||
"{{ application_dir }}"
|
"{{ application_dir }}"
|
||||||
]
|
]
|
||||||
|
|
||||||
[storage.yandex]
|
[storage.yandex_cloud_s3]
|
||||||
type = "restic"
|
type = "restic"
|
||||||
restic_repository = "{{ restic_repository }}"
|
restic_repository = "{{ restic_repository }}"
|
||||||
restic_password = "{{ restic_password }}"
|
restic_password = "{{ restic_password }}"
|
||||||
@@ -10,8 +12,7 @@ aws_access_key_id = "{{ restic_s3_access_key }}"
|
|||||||
aws_secret_access_key = "{{ restic_s3_access_secret }}"
|
aws_secret_access_key = "{{ restic_s3_access_secret }}"
|
||||||
aws_default_region = "{{ restic_s3_region }}"
|
aws_default_region = "{{ restic_s3_region }}"
|
||||||
|
|
||||||
[notifications.telegram]
|
[notifier.server_notifications_channel]
|
||||||
type = "telegram"
|
type = "telegram"
|
||||||
telegram_bot_token = "{{ notifications_tg_bot_token }}"
|
telegram_bot_token = "{{ notifications_tg_bot_token }}"
|
||||||
telegram_chat_id = "{{ notifications_tg_chat_id }}"
|
telegram_chat_id = "{{ notifications_tg_chat_id }}"
|
||||||
notifications_name = "{{ notifications_name }}"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user