Add email notification support

This commit is contained in:
deajan 2025-06-13 14:43:55 +02:00
parent a3b3573802
commit 4cf8e7185b
5 changed files with 142 additions and 61 deletions

View file

@ -7,8 +7,8 @@ __intname__ = "npbackup.configuration"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025040401"
__version__ = "npbackup 3.0.0+"
__build__ = "2025061301"
__version__ = "npbackup 3.0.3+"
from typing import Tuple, Optional, List, Any, Union
@ -105,7 +105,8 @@ ENCRYPTED_OPTIONS = [
"repo_opts.repo_password_command",
"global_prometheus.http_username",
"global_prometheus.http_password",
"global_email.smtp_username" "global_email.smtp_password",
"global_email.smtp_username",
"global_email.smtp_password",
"env.encrypted_env_variables",
"global_options.auto_upgrade_server_username",
"global_options.auto_upgrade_server_password",
@ -231,7 +232,7 @@ empty_config_dict = {
"recipients": None,
"on_backup_success": True,
"on_backup_failure": True,
"on_operations_success": True,
"on_operations_success": False,
"on_operations_failure": True,
},
"global_options": {

View file

@ -7,7 +7,7 @@ __intname__ = "npbackup.core.metrics"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025061201"
__build__ = "2025061301"
import os
from typing import Optional, Tuple, List
@ -22,8 +22,8 @@ from npbackup.restic_metrics import (
write_metrics_file,
)
from npbackup.__version__ import __intname__ as NAME, version_dict
from npbackup.__debug__ import _DEBUG
from npbackup.__debug__ import _DEBUG, fmt_json
from resources.customization import OEM_STRING
logger = getLogger()
@ -44,21 +44,20 @@ def metric_analyser(
"""
operation_success = True
backup_too_small = False
timestamp = int(datetime.now(timezone.utc).timestamp())
date = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
metrics = []
print(repo_config)
try:
repo_name = repo_config.g("name")
try:
labels = {
"npversion": f"{NAME}{version_dict['version']}-{version_dict['build_type']}",
"repo_name": repo_name,
"action": operation,
}
if repo_config.g("global_prometheus.metrics"):
if repo_config.g("global_prometheus") and repo_config.g("global_prometheus.metrics"):
labels["backup_job"] = repo_config.g("prometheus.backup_job")
labels["group"] = repo_config.g("prometheus.group")
labels["instance"] = repo_config.g("global_prometheus.instance")
no_cert_verify = repo_config.g("global_prometheus.no_cert_verify")
destination = repo_config.g("global_prometheus.destination")
prometheus_additional_labels = repo_config.g(
"global_prometheus.additional_labels"
)
@ -70,9 +69,6 @@ def metric_analyser(
logger.error(
f"Bogus value in configuration for prometheus additional labels: {prometheus_additional_labels}"
)
else:
destination = None
no_cert_verify = False
# We only analyse backup output of restic
if operation == "backup":
@ -117,7 +113,7 @@ def metric_analyser(
labels_string = create_labels_string(labels)
metrics.append(
f'npbackup_exec_state{{{labels_string},timestamp="{int(datetime.now(timezone.utc).timestamp())}"}} {exec_state}'
f'npbackup_exec_state{{{labels_string},timestamp="{timestamp}"}} {exec_state}'
)
# Add upgrade state if upgrades activated
@ -127,14 +123,14 @@ def metric_analyser(
labels_string = create_labels_string(labels)
metrics.append(
f'npbackup_exec_state{{{labels_string},timestamp="{int(datetime.now(timezone.utc).timestamp())}"}} {upgrade_state}'
f'npbackup_exec_state{{{labels_string},timestamp="{timestamp}"}} {upgrade_state}'
)
except (ValueError, TypeError):
pass
if isinstance(exec_time, (int, float)):
try:
metrics.append(
f'npbackup_exec_time{{{labels_string},timestamp="{int(datetime.now(timezone.utc).timestamp())}"}} {exec_time}'
f'npbackup_exec_time{{{labels_string},timestamp="{timestamp}"}} {exec_time}'
)
except (ValueError, TypeError):
logger.warning("Cannot get exec time from environment")
@ -144,14 +140,19 @@ def metric_analyser(
send_prometheus_metrics(
repo_config,
metrics,
destination,
no_cert_verify,
dry_run,
append_metrics_file,
repo_name,
operation,
)
send_metrics_mail(repo_config, metrics)
send_metrics_mail(
repo_config,
operation,
restic_result=restic_result,
operation_success=operation_success,
backup_too_small=backup_too_small,
exec_state=exec_state,
date=date,
)
except KeyError as exc:
logger.info("Metrics error: {}".format(exc))
logger.debug("Trace:", exc_info=True)
@ -164,13 +165,27 @@ def metric_analyser(
def send_prometheus_metrics(
repo_config: dict,
metrics: List[str],
destination: Optional[str] = None,
no_cert_verify: bool = False,
dry_run: bool = False,
append_metrics_file: bool = False,
repo_name: Optional[str] = None,
operation: Optional[str] = None,
) -> bool:
try:
no_cert_verify = repo_config.g("global_prometheus.no_cert_verify")
if not no_cert_verify:
no_cert_verify = False
destination = repo_config.g("global_prometheus.destination")
repo_name = repo_config.g("name")
if repo_config.g("global_prometheus.metrics") is not True:
logger.debug(
"Metrics not enabled in configuration. Not sending metrics to Prometheus."
)
return False
except KeyError as exc:
logger.error("No prometheus configuration found in config file.")
return False
if destination and dry_run:
logger.info("Dry run mode. Not sending metrics.")
elif destination:
@ -205,26 +220,39 @@ def send_prometheus_metrics(
logger.debug("No metrics destination set. Not sending metrics")
def send_metrics_mail(repo_config: dict, metrics: List[str]):
def send_metrics_mail(
repo_config: dict,
operation: str,
restic_result: Optional[dict] = None,
operation_success: Optional[bool] = None,
backup_too_small: Optional[bool] = None,
exec_state: Optional[int] = None,
date: Optional[int] = None,
):
"""
Sends metrics via email.
"""
if not metrics:
logger.warning("No metrics to send via email.")
return False
if not repo_config.g("global_email.enable"):
op_success = (
True
if operation_success and not backup_too_small and exec_state == 0
else False
)
repo_name = repo_config.g("name")
try:
if not repo_config.g("global_email") or repo_config.g("global_email.enable"):
logger.debug(
"Metrics not enabled in configuration. Not sending metrics via email."
"Email not enabled in configuration. Not sending notifications."
)
return False
instance = repo_config.g("global_email.instance")
smtp_server = repo_config.g("global_email.smtp_server")
smtp_port = repo_config.g("global_email.smtp_port")
smtp_security = repo_config.g("global_email.smtp_security")
if not smtp_server or not smtp_port or not smtp_security:
logger.warning(
"SMTP server/port or security not set. Not sending metrics via email."
"SMTP server/port or security not set. Not sending notifications via email."
)
return False
smtp_username = repo_config.g("global_email.smtp_username")
@ -232,21 +260,68 @@ def send_metrics_mail(repo_config: dict, metrics: List[str]):
sender = repo_config.g("global_email.sender")
recipients = repo_config.g("global_email.recipients")
if not sender or not recipients:
logger.warning("Sender or recipients not set. Not sending metrics via email.")
logger.warning(
"Sender or recipients not set. Not sending metrics via email."
)
return False
on_backup_success = repo_config.g("global_email.on_backup_success")
on_backup_failure = repo_config.g("global_email.on_backup_failure")
on_operations_success = repo_config.g("global_email.on_operations_success")
on_operations_failure = repo_config.g("global_email.on_operations_failure")
if operation == "backup":
if not on_backup_success and op_success:
logger.debug("Not sending email for backup success.")
return True
if not on_backup_failure and not op_success:
logger.debug("Not sending email for backup failure.")
return False
elif operation != "test_email":
if not on_operations_success and op_success:
logger.debug("Not sending email for operation success.")
return True
if not on_operations_failure and not op_success:
logger.debug("Not sending email for operation failure.")
return False
except KeyError as exc:
logger.error(f"Missing email configuration: {exc}")
return False
logger.info(f"Sending metrics via email to {recipients}.")
recipients = [recipient.strip() for recipient in recipients.split(",")]
mailer = Mailer(
smtp_server=smtp_server,
smtp_port=smtp_port,
security=smtp_security,
smtp_user=smtp_username,
smtp_password=smtp_password,
debug=_DEBUG,
debug=False, # Make sure we don't send debug info so we don't get to leak passwords
)
subject = (
f"Metrics for {NAME} {version_dict['version']}-{version_dict['build_type']}"
f"{OEM_STRING} failure report for {instance} {operation} on repo {repo_name}"
)
body = "\n".join(metrics)
body = f"Operation: {operation}\nRepo: {repo_name}"
if op_success:
body += "\nStatus: Success"
subject = f"{OEM_STRING} success report for {instance} {operation} on repo {repo_name}"
elif backup_too_small:
body += "\nStatus: Backup too small"
elif exec_state == 1:
body += "\nStatus: Warning"
elif exec_state == 2:
body += "\nStatus: Error"
elif exec_state == 3:
body += "\nStatus: Critical error"
body += f"\nDate: {date}"
if isinstance(restic_result, dict):
body += f"\n\nDetail: {fmt_json(restic_result)}"
body += f"\n\nGenerated by {OEM_STRING} {version_dict['version']}\n"
try:
result = mailer.send_email(
sender_mail=sender, recipient_mails=recipients, subject=subject, body=body

View file

@ -7,7 +7,7 @@ __intname__ = "npbackup.gui.config"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025022301"
__build__ = "2025061301"
from typing import List, Tuple
@ -16,6 +16,7 @@ import re
from logging import getLogger
import FreeSimpleGUI as sg
import textwrap
from datetime import datetime, timezone
from ruamel.yaml.comments import CommentedMap
from npbackup import configuration
from ofunctions.misc import get_key_from_value, BytesConverter
@ -38,7 +39,6 @@ from resources.customization import (
INHERITED_SYMLINK_ICON,
)
from npbackup.task import create_scheduled_task
from npbackup.__debug__ import fmt_json
logger = getLogger()
@ -2332,7 +2332,7 @@ Google Cloud storage: GOOGLE_PROJECT_ID GOOGLE_APPLICATION_CREDENTIALS\n\
],
[
sg.Text(_t("config_gui.smtp_port"), size=(40, 1)),
sg.Input(key="global_email.smtp_port", size=(41, 1)),
sg.Input(key="global_email.smtp_port", size=(50, 1)),
],
[
sg.Text(_t("config_gui.smtp_security"), size=(40, 1)),
@ -2856,9 +2856,14 @@ Google Cloud storage: GOOGLE_PROJECT_ID GOOGLE_APPLICATION_CREDENTIALS\n\
repo_config, _ = configuration.get_repo_config(
full_config, object_name, eval_variables=False
)
print(fmt_json(repo_config))
if send_metrics_mail(
repo_config=repo_config, metrics=["Thisis a test email"]
repo_config=repo_config,
operation="test_email",
restic_result=None,
operation_success=True,
backup_too_small=False,
exec_state=0,
date=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
):
sg.Popup(_t("config_gui.test_email_success"), keep_on_top=True)
else:

View file

@ -196,7 +196,7 @@ en:
email_config: Email configuration
enable_email_notifications: Enable email notifications
email_instance: Email instance name
email_instance: Instance name
smtp_server: SMTP server
smtp_port: SMTP port
smtp_security: SMTP security (none, tls, ssl)

View file

@ -198,7 +198,7 @@ fr:
email_config: Configuration email
enable_email_notifications: Activer les notifications email
email_instance: Instance email
email_instance: Nom d'instance
smtp_server: Serveur SMTP
smtp_port: Port SMTP
smtp_security: Sécurité
@ -210,6 +210,6 @@ fr:
email_on_backup_failure: Email sur échec de sauvegarde
email_on_operations_success: Email sur succès des opérations
email_on_operations_failure: Email sur échec des opérations
test_email: Envoyer un email de testr
test_email: Envoyer un email de test
test_email_success: Email de test envoyé avec succès
test_email_failure: Échec de l'envoi du test email, veuillez consulter les journaux pour plus de détails