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

View file

@ -7,7 +7,7 @@ __intname__ = "npbackup.core.metrics"
__author__ = "Orsiris de Jong" __author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent" __copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only" __license__ = "GPL-3.0-only"
__build__ = "2025061201" __build__ = "2025061301"
import os import os
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
@ -22,8 +22,8 @@ from npbackup.restic_metrics import (
write_metrics_file, write_metrics_file,
) )
from npbackup.__version__ import __intname__ as NAME, version_dict 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() logger = getLogger()
@ -44,21 +44,20 @@ def metric_analyser(
""" """
operation_success = True operation_success = True
backup_too_small = False 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 = [] metrics = []
print(repo_config) repo_name = repo_config.g("name")
try: try:
repo_name = repo_config.g("name")
labels = { labels = {
"npversion": f"{NAME}{version_dict['version']}-{version_dict['build_type']}", "npversion": f"{NAME}{version_dict['version']}-{version_dict['build_type']}",
"repo_name": repo_name, "repo_name": repo_name,
"action": operation, "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["backup_job"] = repo_config.g("prometheus.backup_job")
labels["group"] = repo_config.g("prometheus.group") labels["group"] = repo_config.g("prometheus.group")
labels["instance"] = repo_config.g("global_prometheus.instance") 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( prometheus_additional_labels = repo_config.g(
"global_prometheus.additional_labels" "global_prometheus.additional_labels"
) )
@ -70,9 +69,6 @@ def metric_analyser(
logger.error( logger.error(
f"Bogus value in configuration for prometheus additional labels: {prometheus_additional_labels}" 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 # We only analyse backup output of restic
if operation == "backup": if operation == "backup":
@ -117,7 +113,7 @@ def metric_analyser(
labels_string = create_labels_string(labels) labels_string = create_labels_string(labels)
metrics.append( 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 # Add upgrade state if upgrades activated
@ -127,14 +123,14 @@ def metric_analyser(
labels_string = create_labels_string(labels) labels_string = create_labels_string(labels)
metrics.append( 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): except (ValueError, TypeError):
pass pass
if isinstance(exec_time, (int, float)): if isinstance(exec_time, (int, float)):
try: try:
metrics.append( 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): except (ValueError, TypeError):
logger.warning("Cannot get exec time from environment") logger.warning("Cannot get exec time from environment")
@ -144,14 +140,19 @@ def metric_analyser(
send_prometheus_metrics( send_prometheus_metrics(
repo_config, repo_config,
metrics, metrics,
destination,
no_cert_verify,
dry_run, dry_run,
append_metrics_file, append_metrics_file,
repo_name,
operation, 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: except KeyError as exc:
logger.info("Metrics error: {}".format(exc)) logger.info("Metrics error: {}".format(exc))
logger.debug("Trace:", exc_info=True) logger.debug("Trace:", exc_info=True)
@ -164,13 +165,27 @@ def metric_analyser(
def send_prometheus_metrics( def send_prometheus_metrics(
repo_config: dict, repo_config: dict,
metrics: List[str], metrics: List[str],
destination: Optional[str] = None,
no_cert_verify: bool = False,
dry_run: bool = False, dry_run: bool = False,
append_metrics_file: bool = False, append_metrics_file: bool = False,
repo_name: Optional[str] = None,
operation: Optional[str] = None, operation: Optional[str] = None,
) -> bool: ) -> 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: if destination and dry_run:
logger.info("Dry run mode. Not sending metrics.") logger.info("Dry run mode. Not sending metrics.")
elif destination: elif destination:
@ -205,48 +220,108 @@ def send_prometheus_metrics(
logger.debug("No metrics destination set. Not sending 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. Sends metrics via email.
""" """
if not metrics:
logger.warning("No metrics to send via email.") op_success = (
return False True
if operation_success and not backup_too_small and exec_state == 0
if not repo_config.g("global_email.enable"): else False
logger.debug( )
"Metrics not enabled in configuration. Not sending metrics via email."
) repo_name = repo_config.g("name")
return False try:
if not repo_config.g("global_email") or repo_config.g("global_email.enable"):
smtp_server = repo_config.g("global_email.smtp_server") logger.debug(
smtp_port = repo_config.g("global_email.smtp_port") "Email not enabled in configuration. Not sending notifications."
smtp_security = repo_config.g("global_email.smtp_security") )
if not smtp_server or not smtp_port or not smtp_security: return False
logger.warning( instance = repo_config.g("global_email.instance")
"SMTP server/port or security not set. Not sending metrics via email." smtp_server = repo_config.g("global_email.smtp_server")
) smtp_port = repo_config.g("global_email.smtp_port")
return False smtp_security = repo_config.g("global_email.smtp_security")
smtp_username = repo_config.g("global_email.smtp_username") if not smtp_server or not smtp_port or not smtp_security:
smtp_password = repo_config.g("global_email.smtp_password") logger.warning(
sender = repo_config.g("global_email.sender") "SMTP server/port or security not set. Not sending notifications via email."
recipients = repo_config.g("global_email.recipients") )
if not sender or not recipients: return False
logger.warning("Sender or recipients not set. Not sending metrics via email.") smtp_username = repo_config.g("global_email.smtp_username")
smtp_password = repo_config.g("global_email.smtp_password")
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."
)
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 return False
logger.info(f"Sending metrics via email to {recipients}.")
recipients = [recipient.strip() for recipient in recipients.split(",")]
mailer = Mailer( mailer = Mailer(
smtp_server=smtp_server, smtp_server=smtp_server,
smtp_port=smtp_port, smtp_port=smtp_port,
security=smtp_security, security=smtp_security,
smtp_user=smtp_username, smtp_user=smtp_username,
smtp_password=smtp_password, 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 = ( 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: try:
result = mailer.send_email( result = mailer.send_email(
sender_mail=sender, recipient_mails=recipients, subject=subject, body=body sender_mail=sender, recipient_mails=recipients, subject=subject, body=body

View file

@ -7,7 +7,7 @@ __intname__ = "npbackup.gui.config"
__author__ = "Orsiris de Jong" __author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent" __copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only" __license__ = "GPL-3.0-only"
__build__ = "2025022301" __build__ = "2025061301"
from typing import List, Tuple from typing import List, Tuple
@ -16,6 +16,7 @@ import re
from logging import getLogger from logging import getLogger
import FreeSimpleGUI as sg import FreeSimpleGUI as sg
import textwrap import textwrap
from datetime import datetime, timezone
from ruamel.yaml.comments import CommentedMap from ruamel.yaml.comments import CommentedMap
from npbackup import configuration from npbackup import configuration
from ofunctions.misc import get_key_from_value, BytesConverter from ofunctions.misc import get_key_from_value, BytesConverter
@ -38,7 +39,6 @@ from resources.customization import (
INHERITED_SYMLINK_ICON, INHERITED_SYMLINK_ICON,
) )
from npbackup.task import create_scheduled_task from npbackup.task import create_scheduled_task
from npbackup.__debug__ import fmt_json
logger = getLogger() 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.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)), 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( repo_config, _ = configuration.get_repo_config(
full_config, object_name, eval_variables=False full_config, object_name, eval_variables=False
) )
print(fmt_json(repo_config))
if send_metrics_mail( 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) sg.Popup(_t("config_gui.test_email_success"), keep_on_top=True)
else: else:

View file

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

View file

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