mirror of
https://github.com/netinvent/npbackup.git
synced 2025-10-11 22:16:34 +08:00
Add email notification support
This commit is contained in:
parent
a3b3573802
commit
4cf8e7185b
5 changed files with 142 additions and 61 deletions
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue