Skip to content

Commit 09e255b

Browse files
committed
[Fix] Send background VPN errors and recovery as generic_message notifications #1049
Modified tasks and tasks zerotier consistently with utils. Tested via manual testing. Fixes #1049
1 parent 0235ae5 commit 09e255b

File tree

4 files changed

+105
-68
lines changed

4 files changed

+105
-68
lines changed

openwisp_controller/config/tasks.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from openwisp_utils.tasks import OpenwispCeleryTask
1111

12+
from .utils import handle_error_notification, handle_recovery_notification
13+
1214
logger = logging.getLogger(__name__)
1315

1416

@@ -130,18 +132,32 @@ def trigger_vpn_server_endpoint(endpoint, auth_token, vpn_id):
130132

131133
# Cache the configuration here makes downloading the configuration faster.
132134
vpn.get_cached_configuration()
133-
response = requests.post(
134-
endpoint,
135-
params={"key": auth_token},
136-
verify=False if getattr(settings, "DEBUG") else True,
137-
)
138-
if response.status_code == 200:
139-
logger.info(f"Triggered update webhook of VPN Server UUID: {vpn_id}")
135+
task_key = f"vpn_update_task:{vpn_id}"
136+
try:
137+
response = requests.post(
138+
endpoint,
139+
params={"key": auth_token},
140+
verify=False if getattr(settings, "DEBUG") else True,
141+
)
142+
response.raise_for_status()
143+
except requests.RequestException as e:
144+
logger.warning(
145+
f"Failed to update VPN Server configuration. "
146+
f"Error: {str(e)}, "
147+
f"VPN Server UUID: {vpn_id}"
148+
)
149+
handle_error_notification(
150+
task_key,
151+
exception=e,
152+
instance=vpn,
153+
action="update",
154+
)
140155
else:
141-
logger.error(
142-
"Failed to update VPN Server configuration. "
143-
f"Response status code: {response.status_code}, "
144-
f"VPN Server UUID: {vpn_id}",
156+
logger.info(f"Triggered update webhook of VPN Server UUID: {vpn_id}")
157+
handle_recovery_notification(
158+
task_key,
159+
instance=vpn,
160+
action="update",
145161
)
146162

147163

openwisp_controller/config/tasks_zerotier.py

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from openwisp_utils.tasks import OpenwispCeleryTask
1515

1616
from .settings import API_TASK_RETRY_OPTIONS
17+
from .utils import handle_error_notification, handle_recovery_notification
1718

1819
logger = logging.getLogger(__name__)
1920

@@ -27,48 +28,6 @@ class OpenwispApiTask(OpenwispCeleryTask):
2728
HTTPStatus.GATEWAY_TIMEOUT, # 504
2829
]
2930

30-
def _send_api_task_notification(self, type, **kwargs):
31-
vpn = kwargs.get("instance")
32-
action = kwargs.get("action").replace("_", " ")
33-
status_code = kwargs.get("status_code")
34-
# Adding some delay here to prevent overlapping
35-
# of the django success message container
36-
# with the ow-notification container
37-
# https://github.com/openwisp/openwisp-notifications/issues/264
38-
sleep(2)
39-
message_map = {
40-
"error": {
41-
"verb": _("encountered an unrecoverable error"),
42-
"message": _(
43-
"Unable to perform {action} operation on the "
44-
"{target} VPN server due to an "
45-
"unrecoverable error "
46-
"(status code: {status_code})"
47-
),
48-
"level": "error",
49-
},
50-
"recovery": {
51-
"verb": _("has been completed successfully"),
52-
"message": _("The {action} operation on {target} {verb}."),
53-
"level": "info",
54-
},
55-
}
56-
meta = message_map[type]
57-
notify.send(
58-
type="generic_message",
59-
sender=vpn,
60-
target=vpn,
61-
action=action,
62-
verb=meta["verb"],
63-
message=meta["message"].format(
64-
action=action,
65-
target=str(vpn),
66-
status_code=status_code,
67-
verb=meta["verb"],
68-
),
69-
level=meta["level"],
70-
)
71-
7231
def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
7332
"""
7433
This method handles API calls and their responses
@@ -105,13 +64,10 @@ def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
10564
response.raise_for_status()
10665
logger.info(info_msg)
10766
if send_notification:
108-
task_result = cache.get(task_key)
109-
if task_result == "error":
110-
self._send_api_task_notification("recovery", **kwargs)
111-
cache.set(task_key, "success", None)
67+
handle_recovery_notification(task_key, **kwargs)
11268
except RequestException as e:
11369
if response.status_code in self._RECOVERABLE_API_CODES:
114-
retry_logger = logger.warn
70+
retry_logger = logger.warning
11571
# When retry limit is reached, use error logging
11672
if self.request.retries == self.max_retries:
11773
retry_logger = logger.error
@@ -122,12 +78,7 @@ def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
12278
raise e
12379
logger.error(f"{err_msg}, Error: {e}")
12480
if send_notification:
125-
task_result = cache.get(task_key)
126-
if task_result in (None, "success"):
127-
cache.set(task_key, "error", None)
128-
self._send_api_task_notification(
129-
"error", status_code=response.status_code, **kwargs
130-
)
81+
handle_error_notification(task_key, exception=e, **kwargs)
13182
return (response, updated_config) if updated_config else response
13283

13384

openwisp_controller/config/tests/test_vpn.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from subprocess import CalledProcessError, TimeoutExpired
44
from unittest import mock
55

6+
import requests
67
from celery.exceptions import Retry, SoftTimeLimitExceeded
78
from django.conf import settings
89
from django.core.exceptions import ValidationError
@@ -728,9 +729,12 @@ def test_trigger_vpn_server_endpoint_invalid_vpn_id(self):
728729

729730

730731
class TestWireguardTransaction(BaseTestVpn, TestWireguardVpnMixin, TransactionTestCase):
732+
mock_response = mock.Mock(spec=requests.Response)
733+
mock_response.status_code = 200
734+
mock_response.raise_for_status = mock.Mock()
735+
731736
@mock.patch(
732-
"openwisp_controller.config.tasks.requests.post",
733-
return_value=HttpResponse(status=200),
737+
"openwisp_controller.config.tasks.requests.post", return_value=mock_response
734738
)
735739
def test_auto_peer_configuration(self, *args):
736740
self.assertEqual(IpAddress.objects.count(), 0)
@@ -985,9 +989,12 @@ def test_auto_client(self):
985989
class TestVxlanTransaction(
986990
BaseTestVpn, TestVxlanWireguardVpnMixin, TransactionTestCase
987991
):
992+
mock_response = mock.Mock(spec=requests.Response)
993+
mock_response.status_code = 200
994+
mock_response.raise_for_status = mock.Mock()
995+
988996
@mock.patch(
989-
"openwisp_controller.config.tasks.requests.post",
990-
return_value=HttpResponse(status=200),
997+
"openwisp_controller.config.tasks.requests.post", return_value=mock_response
991998
)
992999
def test_auto_peer_configuration(self, *args):
9931000
self.assertEqual(IpAddress.objects.count(), 0)

openwisp_controller/config/utils.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import logging
22

3+
from django.core.cache import cache
34
from django.core.exceptions import ValidationError
45
from django.db.models import Q
56
from django.http import Http404, HttpResponse
67
from django.shortcuts import get_object_or_404 as base_get_object_or_404
78
from django.urls import path, re_path
9+
from django.utils.translation import gettext_lazy as _
10+
11+
# from time import sleep
12+
from openwisp_notifications.signals import notify
813
from openwisp_notifications.utils import _get_object_link
914

1015
logger = logging.getLogger(__name__)
@@ -206,3 +211,61 @@ def get_default_templates_queryset(
206211
def get_config_error_notification_target_url(obj, field, absolute_url=True):
207212
url = _get_object_link(obj._related_object(field), absolute_url)
208213
return f"{url}#config-group"
214+
215+
216+
def send_api_task_notification(type, **kwargs):
217+
vpn = kwargs.get("instance")
218+
action = kwargs.get("action").replace("_", " ")
219+
exception = kwargs.get("exception")
220+
# Adding some delay here to prevent overlapping
221+
# of the django success message container
222+
# with the ow-notification container
223+
# https://github.com/openwisp/openwisp-notifications/issues/264
224+
# sleep(2)
225+
message_map = {
226+
"error": {
227+
"verb": _("encountered an unrecoverable error"),
228+
"message": _(
229+
"Unable to perform {action} operation on the "
230+
"{target} VPN server due to an "
231+
"unrecoverable error "
232+
"({error_type})"
233+
),
234+
"level": "error",
235+
},
236+
"success": {
237+
"verb": _("has been completed successfully"),
238+
"message": _("The {action} operation on {target} {verb}."),
239+
"level": "info",
240+
},
241+
}
242+
meta = message_map[type]
243+
notify.send(
244+
sender=vpn,
245+
target=vpn,
246+
type="generic_message",
247+
action_object=vpn,
248+
verb=meta["verb"],
249+
message=meta["message"].format(
250+
action=action,
251+
target=str(vpn),
252+
error_type=exception.__class__.__name__ if exception else "",
253+
verb=meta["verb"],
254+
),
255+
description=str(exception) if exception else "",
256+
level=meta["level"],
257+
)
258+
259+
260+
def handle_recovery_notification(task_key, **kwargs):
261+
task_result = cache.get(task_key)
262+
if task_result == "error":
263+
send_api_task_notification("success", **kwargs)
264+
cache.set(task_key, "success", timeout=None)
265+
266+
267+
def handle_error_notification(task_key, **kwargs):
268+
cached_value = cache.get(task_key)
269+
if cached_value != "error":
270+
cache.set(task_key, "error", timeout=None)
271+
send_api_task_notification("error", **kwargs)

0 commit comments

Comments
 (0)