diff --git a/ns8_backup_monitor/notifier.py b/ns8_backup_monitor/notifier.py new file mode 100644 index 0000000..ed3fa0a --- /dev/null +++ b/ns8_backup_monitor/notifier.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +notifier.py - Sends a single classified notification email. + +Formats a human-readable HTML + text email based on: + - correlation outcome (SUCCESS / PARTIAL / REPO_FAILURE) + - per-module statuses + - repository check results (if run) +""" + +import logging +import smtplib +from datetime import datetime, timezone +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import Optional + +log = logging.getLogger(__name__) + +OUTCOME_EMOJI = { + "SUCCESS": "OK", + "PARTIAL": "WARNING", + "REPO_FAILURE": "CRITICAL", +} + +OUTCOME_COLOR = { + "SUCCESS": "#2e7d32", + "PARTIAL": "#e65100", + "REPO_FAILURE": "#b71c1c", +} + + +def _build_text(correlation: dict, repo_status: Optional[dict]) -> str: + outcome = correlation["outcome"] + lines = [ + f"NS8 Backup Monitor - {OUTCOME_EMOJI[outcome]}: {outcome}", + f"Time: {datetime.now(timezone.utc).isoformat()}", + f"Plans checked: {', '.join(correlation.get('backup_ids', []))}", + f"Modules: {correlation['succeeded']} OK / {correlation['failed']} FAILED / {correlation['total']} total", + "", + ] + + if correlation["failed_modules"]: + lines.append("Failed modules:") + for m in correlation["failed_modules"]: + lines.append(f" - [{m['module_id']}] backup_id={m['backup_id']}: {m.get('error', 'unknown error')}") + lines.append("") + + if repo_status: + lines.append("Repository check:") + for dest in repo_status.get("destinations", []): + lines.append(f" - [{dest['repo_id']}] {dest['status']}: {dest.get('error', '')}") + if repo_status.get("note"): + lines.append(f" NOTE: {repo_status['note']}") + lines.append("") + + if correlation.get("note"): + lines.append(f"Note: {correlation['note']}") + + return "\n".join(lines) + + +def _build_html(correlation: dict, repo_status: Optional[dict]) -> str: + outcome = correlation["outcome"] + color = OUTCOME_COLOR[outcome] + label = OUTCOME_EMOJI[outcome] + ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + plan_ids = ", ".join(correlation.get("backup_ids", [])) or "N/A" + + rows = "" + for m in correlation.get("modules", []): + bg = "#e8f5e9" if m["result"] == "success" else "#ffebee" + icon = "✓" if m["result"] == "success" else "✗" + rows += ( + f'
| Repo ID | +Status | +Detail | +
|---|
{repo_status["note"]}
' + + html = f""" + ++ {correlation['succeeded']} module(s) OK | + {correlation['failed']} FAILED | + {correlation['total']} total +
+| + | Module | +Backup ID | +Timestamp | +Error | +
|---|