diff --git a/npbackup/configuration.py b/npbackup/configuration.py index 1a57c77..f5a1810 100644 --- a/npbackup/configuration.py +++ b/npbackup/configuration.py @@ -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": { diff --git a/npbackup/core/metrics.py b/npbackup/core/metrics.py index cbcd8ab..6813858 100644 --- a/npbackup/core/metrics.py +++ b/npbackup/core/metrics.py @@ -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) + repo_name = repo_config.g("name") try: - repo_name = repo_config.g("name") 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,48 +220,108 @@ 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"): - logger.debug( - "Metrics not enabled in configuration. Not sending metrics via email." - ) - return False - - 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." - ) - return False - 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.") + + 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( + "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 notifications via email." + ) + return False + 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 + 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 diff --git a/npbackup/gui/config.py b/npbackup/gui/config.py index 78c9d78..81d5d6a 100644 --- a/npbackup/gui/config.py +++ b/npbackup/gui/config.py @@ -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: diff --git a/npbackup/translations/config_gui.en.yml b/npbackup/translations/config_gui.en.yml index 6a44654..7235ad6 100644 --- a/npbackup/translations/config_gui.en.yml +++ b/npbackup/translations/config_gui.en.yml @@ -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) diff --git a/npbackup/translations/config_gui.fr.yml b/npbackup/translations/config_gui.fr.yml index 516b37b..17d478a 100644 --- a/npbackup/translations/config_gui.fr.yml +++ b/npbackup/translations/config_gui.fr.yml @@ -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 \ No newline at end of file