Files
ns8-backup-monitor/ns8_backup_monitor/notifier.py
T

186 lines
6.7 KiB
Python
Raw Normal View History

#!/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'<tr style="background:{bg}">'
f'<td style="padding:4px 8px">{icon}</td>'
f'<td style="padding:4px 8px">{m["module_id"]}</td>'
f'<td style="padding:4px 8px">{m["backup_id"]}</td>'
f'<td style="padding:4px 8px">{m.get("timestamp", "")}</td>'
f'<td style="padding:4px 8px">{m.get("error", "") or ""}</td>'
'</tr>'
)
repo_section = ""
if repo_status:
repo_rows = ""
for dest in repo_status.get("destinations", []):
bg = "#e8f5e9" if dest["status"] == "OK" else "#ffebee"
repo_rows += (
f'<tr style="background:{bg}">'
f'<td style="padding:4px 8px">{dest["repo_id"]}</td>'
f'<td style="padding:4px 8px"><b>{dest["status"]}</b></td>'
f'<td style="padding:4px 8px">{dest.get("error", "")}</td>'
'</tr>'
)
if repo_rows:
repo_section = f"""
<h3 style="margin-top:24px">Repository check</h3>
<table border="1" cellspacing="0" cellpadding="0" style="border-collapse:collapse;font-size:13px;width:100%">
<thead><tr style="background:#f5f5f5">
<th style="padding:4px 8px">Repo ID</th>
<th style="padding:4px 8px">Status</th>
<th style="padding:4px 8px">Detail</th>
</tr></thead>
<tbody>{repo_rows}</tbody>
</table>
"""
if repo_status.get("note"):
repo_section += f'<p style="color:#777;font-size:12px">{repo_status["note"]}</p>'
html = f"""
<html><body style="font-family:monospace;font-size:14px;max-width:800px;margin:auto">
<div style="background:{color};color:#fff;padding:16px 20px;border-radius:6px 6px 0 0">
<b>NS8 Backup Monitor &mdash; {label}: {outcome}</b><br>
<small>{ts} &bull; Plans: {plan_ids}</small>
</div>
<div style="border:1px solid #ddd;border-top:none;padding:16px 20px;border-radius:0 0 6px 6px">
<p>
<b>{correlation['succeeded']}</b> module(s) OK &nbsp;|&nbsp;
<b>{correlation['failed']}</b> FAILED &nbsp;|&nbsp;
<b>{correlation['total']}</b> total
</p>
<table border="1" cellspacing="0" cellpadding="0" style="border-collapse:collapse;font-size:13px;width:100%">
<thead><tr style="background:#f5f5f5">
<th style="padding:4px 8px"></th>
<th style="padding:4px 8px">Module</th>
<th style="padding:4px 8px">Backup ID</th>
<th style="padding:4px 8px">Timestamp</th>
<th style="padding:4px 8px">Error</th>
</tr></thead>
<tbody>{rows}</tbody>
</table>
{repo_section}
</div>
</body></html>
"""
return html
def send_notification(
config: dict,
alerts: list,
correlation: dict,
repo_status: Optional[dict] = None
):
outcome = correlation["outcome"]
mail_cfg = config.get("mail", {})
smtp_cfg = config.get("smtp", {})
mail_from = mail_cfg.get("from", "ns8-backup-monitor@localhost")
mail_to = mail_cfg.get("to", [])
subject_prefix = mail_cfg.get("subject_prefix", "[NS8 Backup]")
subject = f"{subject_prefix} {OUTCOME_EMOJI[outcome]}: {outcome} - {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
text_body = _build_text(correlation, repo_status)
html_body = _build_html(correlation, repo_status)
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = mail_from
msg["To"] = ", ".join(mail_to)
msg.attach(MIMEText(text_body, "plain"))
msg.attach(MIMEText(html_body, "html"))
host = smtp_cfg.get("host", "localhost")
port = smtp_cfg.get("port", 25)
use_tls = smtp_cfg.get("use_tls", False)
use_starttls = smtp_cfg.get("use_starttls", False)
username = smtp_cfg.get("username", "")
password = smtp_cfg.get("password", "")
try:
if use_tls:
smtp = smtplib.SMTP_SSL(host, port)
else:
smtp = smtplib.SMTP(host, port)
if use_starttls:
smtp.starttls()
if username and password:
smtp.login(username, password)
smtp.sendmail(mail_from, mail_to, msg.as_string())
smtp.quit()
log.info(f"Notification sent: {subject} -> {mail_to}")
except Exception as e:
log.error(f"Failed to send notification: {e}")