Skip to content

Commit 8f9f543

Browse files
[feature] Added connection notification logic from openwisp-monitoring #269
Moves code from openwisp-monitoring to openwisp-controller: openwisp/openwisp-monitoring#226 Closes #269
1 parent 53da00c commit 8f9f543

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

openwisp_controller/connection/apps.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
from django.db import transaction
44
from django.db.models.signals import post_save
55
from django.utils.translation import ugettext_lazy as _
6+
from openwisp_notifications.signals import notify
7+
from openwisp_notifications.types import register_notification_type
68
from swapper import load_model
79

810
from ..config.signals import config_modified
11+
from .signals import is_working_changed
912

1013
_TASK_NAME = 'openwisp_controller.connection.tasks.update_config'
1114

@@ -21,6 +24,7 @@ def ready(self):
2124
to the ``update_config`` celery task
2225
which will be executed in the background
2326
"""
27+
self.register_notification_types()
2428
config_modified.connect(
2529
self.config_modified_receiver, dispatch_uid='connection.update_config'
2630
)
@@ -31,6 +35,11 @@ def ready(self):
3135
sender=Config,
3236
dispatch_uid='connection.auto_add_credentials',
3337
)
38+
is_working_changed.connect(
39+
self.is_working_changed_receiver,
40+
sender=load_model('connection', 'DeviceConnection'),
41+
dispatch_uid='is_working_changed_receiver',
42+
)
3443

3544
@classmethod
3645
def config_modified_receiver(cls, **kwargs):
@@ -64,3 +73,49 @@ def _is_update_in_progress(cls, device_pk):
6473
if task['name'] == _TASK_NAME and str(device_pk) in task['args']:
6574
return True
6675
return False
76+
77+
@classmethod
78+
def is_working_changed_receiver(cls, instance, is_working, **kwargs):
79+
device = instance.device
80+
notification_opts = dict(sender=instance, target=device)
81+
if not is_working:
82+
notification_opts['type'] = 'connection_is_not_working'
83+
else:
84+
notification_opts['type'] = 'connection_is_working'
85+
notify.send(**notification_opts)
86+
87+
def register_notification_types(self):
88+
register_notification_type(
89+
'connection_is_not_working',
90+
{
91+
'verbose_name': 'Device Connection PROBLEM',
92+
'verb': 'not working',
93+
'level': 'error',
94+
'email_subject': (
95+
'[{site.name}] PROBLEM: Connection to '
96+
'device {notification.target}'
97+
),
98+
'message': (
99+
'{notification.actor.credentials} connection to '
100+
'device [{notification.target}]({notification.target_link}) '
101+
'is {notification.verb}. {notification.actor.failure_reason}'
102+
),
103+
},
104+
)
105+
register_notification_type(
106+
'connection_is_working',
107+
{
108+
'verbose_name': 'Device Connection RECOVERY',
109+
'verb': 'working',
110+
'level': 'info',
111+
'email_subject': (
112+
'[{site.name}] RECOVERY: Connection to '
113+
'device {notification.target}'
114+
),
115+
'message': (
116+
'{notification.actor.credentials} connection to '
117+
'device [{notification.target}]({notification.target_link}) '
118+
'is {notification.verb}. {notification.actor.failure_reason}'
119+
),
120+
},
121+
)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from django.apps.registry import apps
2+
from django.core import mail
3+
from django.urls import reverse
4+
from django.utils.html import strip_tags
5+
from openwisp_notifications.types import unregister_notification_type
6+
from swapper import load_model
7+
8+
from .utils import CreateConnectionsMixin
9+
10+
Notification = load_model('openwisp_notifications', 'Notification')
11+
Credentials = load_model('connection', 'Credentials')
12+
DeviceConnection = load_model('connection', 'DeviceConnection')
13+
14+
15+
class TestNotifications(CreateConnectionsMixin):
16+
app_label = 'connection'
17+
18+
def setUp(self):
19+
self._create_admin()
20+
self.d = self._create_device()
21+
self.creds = Credentials.objects.create(
22+
connector='openwisp_controller.connection.connectors.ssh.Ssh'
23+
)
24+
self.dc = DeviceConnection.objects.create(credentials=self.creds, device=self.d)
25+
26+
def _generic_notification_test(
27+
self, exp_level, exp_type, exp_verb, exp_message, exp_email_subject
28+
):
29+
n = Notification.objects.first()
30+
url_path = reverse('notifications:notification_read_redirect', args=[n.pk])
31+
exp_email_link = f'https://example.com{url_path}'
32+
exp_target_link = f'https://example.com/admin/config/device/{self.d.id}/change/'
33+
exp_email_body = '{message}' f'\n\nFor more information see {exp_email_link}.'
34+
35+
email = mail.outbox.pop()
36+
html_message, _ = email.alternatives.pop()
37+
self.assertEqual(n.type, exp_type)
38+
self.assertEqual(n.level, exp_level)
39+
self.assertEqual(n.verb, exp_verb)
40+
self.assertEqual(n.actor, self.dc)
41+
self.assertEqual(n.target, self.d)
42+
self.assertEqual(
43+
n.message, exp_message.format(n=n, target_link=exp_target_link)
44+
)
45+
self.assertEqual(
46+
n.email_subject, exp_email_subject.format(n=n),
47+
)
48+
self.assertEqual(email.subject, n.email_subject)
49+
self.assertEqual(
50+
email.body, exp_email_body.format(message=strip_tags(n.message))
51+
)
52+
self.assertIn(
53+
f'<a href="{exp_email_link}">'
54+
'For further information see "device: default.test.device".</a>',
55+
html_message,
56+
)
57+
58+
def test_connection_working_notification(self):
59+
self.assertEqual(Notification.objects.count(), 0)
60+
self.dc = DeviceConnection.objects.create(
61+
credentials=self.creds, device=self.d, is_working=False
62+
)
63+
self.dc.is_working = True
64+
self.dc.save()
65+
self.assertEqual(Notification.objects.count(), 1)
66+
self._generic_notification_test(
67+
exp_level='info',
68+
exp_type='connection_is_working',
69+
exp_verb='working',
70+
exp_message=(
71+
'<p>(SSH) connection to device <a href="{target_link}">'
72+
'{n.target}</a> is {n.verb}. </p>'
73+
),
74+
exp_email_subject='[example.com] RECOVERY: Connection to device {n.target}',
75+
)
76+
77+
def test_connection_not_working_notification(self):
78+
self.assertEqual(Notification.objects.count(), 0)
79+
self.dc.is_working = False
80+
self.dc.save()
81+
self.assertEqual(Notification.objects.count(), 1)
82+
self._generic_notification_test(
83+
exp_level='error',
84+
exp_type='connection_is_not_working',
85+
exp_verb='not working',
86+
exp_message=(
87+
'<p>(SSH) connection to device <a href="{target_link}">'
88+
'{n.target}</a> is {n.verb}. </p>'
89+
),
90+
exp_email_subject='[example.com] PROBLEM: Connection to device {n.target}',
91+
)
92+
93+
def test_unreachable_after_upgrade_notification(self):
94+
self.assertEqual(Notification.objects.count(), 0)
95+
self.dc.is_working = False
96+
self.dc.failure_reason = 'Giving up, device not reachable anymore after upgrade'
97+
self.dc.save()
98+
self.assertEqual(Notification.objects.count(), 1)
99+
self._generic_notification_test(
100+
exp_level='error',
101+
exp_type='connection_is_not_working',
102+
exp_verb='not working',
103+
exp_message=(
104+
'<p>(SSH) connection to device <a href="{target_link}">'
105+
'{n.target}</a> is {n.verb}. '
106+
'Giving up, device not reachable anymore after upgrade</p>'
107+
),
108+
exp_email_subject='[example.com] PROBLEM: Connection to device {n.target}',
109+
)
110+
111+
def test_default_notification_type_already_unregistered(self):
112+
# Simulates if 'default notification type is already unregistered
113+
# by some other module
114+
115+
# Unregister "config_error" and "device_registered" notification
116+
# types to avoid getting rasing ImproperlyConfigured exceptions
117+
unregister_notification_type('connection_is_not_working')
118+
unregister_notification_type('connection_is_working')
119+
120+
# This will try to unregister 'default' notification type
121+
# which is already got unregistered when Django loaded.
122+
# No exception should be raised as the exception is already handled.
123+
app = apps.get_app_config(self.app_label)
124+
app.register_notification_types()

tests/openwisp2/sample_connection/tests.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from openwisp_controller.connection.tests.test_models import (
66
TestModelsTransaction as BaseTestModelsTransaction,
77
)
8+
from openwisp_controller.connection.tests.test_notifications import (
9+
TestNotifications as BaseTestNotifications,
10+
)
811
from openwisp_controller.connection.tests.test_ssh import TestSsh as BaseTestSsh
912

1013

@@ -25,7 +28,12 @@ class TestSsh(BaseTestSsh):
2528
pass
2629

2730

31+
class TestNotifications(BaseTestNotifications):
32+
app_label = 'sample_connection'
33+
34+
2835
del BaseTestAdmin
2936
del BaseTestModels
3037
del BaseTestModelsTransaction
3138
del BaseTestSsh
39+
del BaseTestNotifications

0 commit comments

Comments
 (0)