Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
289a77e
add notification
JanCaha Jul 19, 2023
9937786
move function
JanCaha Jul 21, 2023
c16102b
add tests
JanCaha Jul 21, 2023
a6360ce
only send email if previous error message was not the same as current
JanCaha Jul 21, 2023
cb6ecd0
add notification setting example
JanCaha Jul 21, 2023
087390e
fix typo
JanCaha Jul 21, 2023
ee6ca5c
fix error messages
JanCaha Jul 26, 2023
8aded71
password and user are optional
JanCaha Jul 26, 2023
be36fcf
new config
JanCaha Jul 26, 2023
664b83d
move smtp functions to separate file
JanCaha Jul 26, 2023
08d77c6
use new functions
JanCaha Jul 26, 2023
3f3522f
update tests
JanCaha Jul 26, 2023
acde9c7
add imports
JanCaha Jul 26, 2023
a41dbc9
update usage
JanCaha Jul 26, 2023
20199b5
use simple EmailMessage
JanCaha Jul 26, 2023
e9b8eaa
add option to send tes email
JanCaha Jul 26, 2023
9a2431b
fix error messages
JanCaha Jul 26, 2023
20ee1b8
add minimal email interval
JanCaha Jul 26, 2023
0b3c505
add time difference between emails
JanCaha Jul 26, 2023
712860c
fix message
JanCaha Aug 4, 2023
9a6f026
fix message
JanCaha Aug 4, 2023
c8b1d3a
fix message
JanCaha Aug 4, 2023
a289efa
zero port
JanCaha Aug 4, 2023
9192e9a
fix typo in variable
JanCaha Aug 4, 2023
7bb5759
catch errors and simplify into single function
JanCaha Aug 4, 2023
18d1a3a
fix typo
JanCaha Aug 4, 2023
18c22a3
add docstrings
JanCaha Aug 4, 2023
3d997ab
email sending should fail without crashing the deamon
JanCaha Aug 4, 2023
63716b3
restyle imports
JanCaha Aug 4, 2023
e64ef03
notification description in use.md
JanCaha Aug 4, 2023
873bfaf
drop unused imports
JanCaha Aug 4, 2023
c4ab272
drop unused import
JanCaha Aug 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
License: MIT
"""

from dynaconf import (
Dynaconf,
)
import platform
import tempfile
import pathlib
import platform
import smtplib
import subprocess
import tempfile

from dynaconf import Dynaconf

from smtp_functions import create_connection_and_log_user

config = Dynaconf(
envvar_prefix=False,
Expand Down Expand Up @@ -100,6 +102,46 @@ def validate_config(config):
):
raise ConfigError("Config error: Ignored tables parameter should be a list")

if "notification" in config:
settings = [
"smtp_server",
"email_sender",
"email_subject",
"email_recipients",
]

for setting in settings:
if setting not in config.notification:
raise ConfigError(f"Config error: `{setting}` is missing from `notification`.")

if not isinstance(config.notification.email_recipients, list):
raise ConfigError("Config error: `email_recipients` should be a list of addresses.")

if "use_ssl" in config.notification:
if not isinstance(config.notification.use_ssl, bool):
raise ConfigError("Config error: `use_ssl` must be set to either `true` or `false`.")

if "use_tls" in config.notification:
if not isinstance(config.notification.use_tls, bool):
raise ConfigError("Config error: `use_tls` must be set to either `true` or `false`.")

if "smtp_port" in config.notification:
if not isinstance(config.notification.smtp_port, int):
raise ConfigError("Config error: `smtp_port` must be set to an integer.")

if "minimal_email_interval" in config.notification:
if not isinstance(config.notification.minimal_email_interval, (int, float)):
raise ConfigError("Config error: `minimal_email_interval` must be set to a number.")

try:
smtp_conn = create_connection_and_log_user(config)
smtp_conn.quit()
except (OSError, smtplib.SMTPConnectError, smtplib.SMTPAuthenticationError) as e:
err = str(e)
if isinstance(e, (smtplib.SMTPConnectError, smtplib.SMTPAuthenticationError)):
err = str(e.smtp_error)
raise ConfigError(f"Config SMTP Error: {err}.")


def get_ignored_tables(
connection,
Expand Down
40 changes: 25 additions & 15 deletions dbsync_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,20 @@
# - pull
# - push

import datetime
import sys
import time
import argparse
import platform
import datetime
import logging
import os
import pathlib
import platform
import sys
import time

import dbsync
from version import (
__version__,
)
from config import (
config,
validate_config,
ConfigError,
update_config_path,
)

from log_functions import setup_logger, handle_error_and_exit
from config import ConfigError, config, update_config_path, validate_config
from log_functions import handle_error_and_exit, setup_logger
from smtp_functions import can_send_email, send_email
from version import __version__


def is_pyinstaller() -> bool:
Expand Down Expand Up @@ -98,6 +91,11 @@ def main():
default="DEBUG",
help="Set level of logging into log-file.",
)
parser.add_argument(
"--test-notification-email",
action="store_true",
help="Send test notification email using the `notification` settings. Should be used to validate settings.",
)

args = parser.parse_args()

Expand All @@ -120,6 +118,11 @@ def main():
except ConfigError as e:
handle_error_and_exit(e)

if args.test_notification_email:
send_email("Mergin DB Sync test email.", config)
logging.debug("Email sent!")
sys.exit(0)

if args.force_init and args.skip_init:
handle_error_and_exit("Cannot use `--force-init` with `--skip-init` Initialization is required. ")

Expand Down Expand Up @@ -148,12 +151,16 @@ def main():
handle_error_and_exit(e)

else:
error_msg = None

if not args.skip_init:
try:
dbsync.dbsync_init(mc)
except dbsync.DbSyncError as e:
handle_error_and_exit(e)

last_email_send = None

while True:
print(datetime.datetime.now())

Expand All @@ -171,6 +178,9 @@ def main():

except dbsync.DbSyncError as e:
logging.error(str(e))
if can_send_email(config):
send_email(str(e), last_email_send, config)
last_email_send = datetime.datetime.now()

logging.debug("Going to sleep")
time.sleep(sleep_time)
Expand Down
34 changes: 34 additions & 0 deletions docs/using.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ daemon:

- `--log-verbosity` use `errors` or `messages` to specify what should be logged. Default is `messages`.

- `--test-notification-email` used to test send notification email (see below for details about sending emails in case of sync fails)

## Excluding tables from sync

Expand All @@ -93,3 +94,36 @@ connections:
- table1
- table2
```

## Sending emails in case of synchronization fail

User can be notified about sync fails via email by including `notification` with proper setting in the config file

The command line option `--test-notification-email` can be used to only send the notification email without running the daemon. This should be useful to test the `notification` settings setup.

```yaml
# optional setting, if you do not want notifications on sync errors to be send via email just delete this section
notification:
# first four options are required to be present
# address of stmp server to send emails
smtp_server: "xxx.xxx"
# email sender to be displayed in the message
email_sender: "dbsync@info.com"
# email subject
email_subject: "DB Sync Error"
# list of recipients of the email notification
email_recipients: ["recipient1@email.xxx", "recipient1@email.xxx"]
# the following configuration is optional and does not need to be specified
# smtp server user
smtp_username: "user@email.xxx"
# smtp user's password
smtp_password: "password"
# smtp port
smtp_port: 25
# use ssl true/false
use_ssl: false
# use tls true/false
use_tls: false
# minimal interval for sending emails in hours (default value is 4) to avoid sending too many emails
minimal_email_interval: 4
```
73 changes: 73 additions & 0 deletions smtp_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import datetime
import smtplib
import typing
from email.message import EmailMessage

from dynaconf import Dynaconf


def create_connection_and_log_user(config: Dynaconf) -> typing.Union[smtplib.SMTP_SSL, smtplib.SMTP]:
"""Create connection and log user to the SMTP server using the configuration in config."""
if "smtp_port" in config.notification:
port = config.notification.smtp_port
else:
port = 0

if "use_ssl" in config.notification and config.notification.use_ssl:
host = smtplib.SMTP_SSL(config.notification.smtp_server, port)
else:
host = smtplib.SMTP(config.notification.smtp_server, port)

if "use_tls" in config.notification and config.notification.use_tls:
host.starttls()
if config.notification.smtp_username and config.notification.smtp_password:
host.login(config.notification.smtp_username, config.notification.smtp_password)

return host


def should_send_another_email(
current_time: datetime.datetime, last_email_sent: typing.Optional[datetime.datetime], config: Dynaconf
) -> bool:
"""Checks if another email should be sent. The time difference to last sent email needs to larger them
minimal interval specified in config or 4 hours if no value was specified."""
if last_email_sent is None:
return True
min_time_diff = config.notification.minimal_email_interval if "minimal_email_interval" in config.notification else 4
time_diff = current_time - last_email_sent
return time_diff.seconds > (min_time_diff * 3600)


def send_email(error: str, last_email_send: typing.Optional[datetime.datetime], config: Dynaconf) -> None:
"""Sends email with provided error using the settings in config."""
current_time = datetime.datetime.now()

if should_send_another_email(current_time, last_email_send, config):
msg = EmailMessage()
msg["Subject"] = config.notification.email_subject
msg["From"] = config.notification.email_sender
msg["To"] = ", ".join(config.notification.email_recipients)

date_time = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")

error = f"{date_time}: {error}"

msg.set_content(error)

sender_email = config.notification.email_sender
if "smtp_username" in config.notification:
sender_email = config.notification.smtp_username

try:
smtp_conn = create_connection_and_log_user(config)
smtp_conn.sendmail(sender_email, config.notification.email_recipients, msg.as_string())
smtp_conn.quit()
except:
pass


def can_send_email(config: Dynaconf) -> bool:
"""Checks if notification settings is in the config and emails can be send."""
if "notification" in config:
return True
return False
112 changes: 103 additions & 9 deletions test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,10 @@
"""
import pytest

from config import (
config,
ConfigError,
validate_config,
get_ignored_tables,
)
from config import ConfigError, config, get_ignored_tables, validate_config
from smtp_functions import can_send_email

from .conftest import (
_reset_config,
)
from .conftest import _reset_config


def test_config():
Expand Down Expand Up @@ -256,3 +250,103 @@ def test_get_ignored_tables():
validate_config(config)
ignored_tables = get_ignored_tables(config.connections[0])
assert ignored_tables == ["table"]


def test_config_notification_setup():
_reset_config()

# no NOTIFICATIONS set should pass but cannot send email
validate_config(config)
assert can_send_email(config) is False

# incomplete setting
config.update(
{
"NOTIFICATION": {
"smtp_server": "server",
"smtp_username": "user",
}
}
)

with pytest.raises(ConfigError, match="Config error: `email_sender`"):
validate_config(config)

# another incomplete setting
_reset_config()

config.update(
{
"NOTIFICATION": {
"smtp_server": "server",
"smtp_username": "user",
"smtp_password": "pass",
"email_sender": "dbsync@info.com",
}
}
)

with pytest.raises(ConfigError, match="Config error: `email_subject`"):
validate_config(config)

# bool variable test
_reset_config()

config.update(
{
"NOTIFICATION": {
"smtp_server": "server",
"smtp_username": "user",
"smtp_password": "pass",
"email_sender": "dbsync@info.com",
"email_subject": "DB Sync Error",
"email_recipients": ["recipient1@test.com", "recipient2@test.com"],
"use_ssl": "some_string",
}
}
)

with pytest.raises(ConfigError, match="Config error: `use_ssl` must be set to either `true` or `false`"):
validate_config(config)

# int variable test
_reset_config()

config.update(
{
"NOTIFICATION": {
"smtp_server": "server",
"smtp_username": "user",
"smtp_password": "pass",
"email_sender": "dbsync@info.com",
"email_subject": "DB Sync Error",
"email_recipients": ["recipient1@test.com", "recipient2@test.com"],
"smtp_port": "some_string",
}
}
)

with pytest.raises(ConfigError, match="Config error: `smtp_port` must be set an integer"):
validate_config(config)
# complete setting but does not work
config.update(
{
"NOTIFICATION": {
"smtp_server": "server",
"smtp_username": "user",
"smtp_password": "pass",
"email_sender": "dbsync@sync.com",
"email_subject": "DB Sync Error",
"email_recipients": ["recipient1@test.com", "recipient2@test.com"],
"smtp_port": 25,
"use_ssl": False,
"use_tls": True,
}
}
)

with pytest.raises(ConfigError, match="Config SMTP Error"):
validate_config(config)

# notifications are set, emails can be send - but this config was not validated, as it would be in real run
assert can_send_email(config)