Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 27 additions & 11 deletions openwisp_controller/config/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from openwisp_utils.tasks import OpenwispCeleryTask

from .utils import handle_error_notification, handle_recovery_notification

logger = logging.getLogger(__name__)


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

# Cache the configuration here makes downloading the configuration faster.
vpn.get_cached_configuration()
response = requests.post(
endpoint,
params={"key": auth_token},
verify=False if getattr(settings, "DEBUG") else True,
)
if response.status_code == 200:
logger.info(f"Triggered update webhook of VPN Server UUID: {vpn_id}")
task_key = f"vpn_update_task:{vpn_id}"
try:
response = requests.post(
endpoint,
params={"key": auth_token},
verify=False if getattr(settings, "DEBUG") else True,
)
response.raise_for_status()
except requests.RequestException as e:
logger.warning(
f"Failed to update VPN Server configuration. "
f"Error: {str(e)}, "
f"VPN Server UUID: {vpn_id}"
)
handle_error_notification(
task_key,
exception=e,
instance=vpn,
action="update",
)
else:
logger.error(
"Failed to update VPN Server configuration. "
f"Response status code: {response.status_code}, "
f"VPN Server UUID: {vpn_id}",
logger.info(f"Triggered update webhook of VPN Server UUID: {vpn_id}")
handle_recovery_notification(
task_key,
instance=vpn,
action="update",
)


Expand Down
61 changes: 4 additions & 57 deletions openwisp_controller/config/tasks_zerotier.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import logging
from http import HTTPStatus
from time import sleep

from celery import shared_task
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext as _
from openwisp_notifications.signals import notify
from requests.exceptions import RequestException
from swapper import load_model

from openwisp_controller.config.api.zerotier_service import ZerotierService
from openwisp_utils.tasks import OpenwispCeleryTask

from .settings import API_TASK_RETRY_OPTIONS
from .utils import handle_error_notification, handle_recovery_notification

logger = logging.getLogger(__name__)

Expand All @@ -27,48 +24,6 @@ class OpenwispApiTask(OpenwispCeleryTask):
HTTPStatus.GATEWAY_TIMEOUT, # 504
]

def _send_api_task_notification(self, type, **kwargs):
vpn = kwargs.get("instance")
action = kwargs.get("action").replace("_", " ")
status_code = kwargs.get("status_code")
# Adding some delay here to prevent overlapping
# of the django success message container
# with the ow-notification container
# https://github.com/openwisp/openwisp-notifications/issues/264
sleep(2)
message_map = {
"error": {
"verb": _("encountered an unrecoverable error"),
"message": _(
"Unable to perform {action} operation on the "
"{target} VPN server due to an "
"unrecoverable error "
"(status code: {status_code})"
),
"level": "error",
},
"recovery": {
"verb": _("has been completed successfully"),
"message": _("The {action} operation on {target} {verb}."),
"level": "info",
},
}
meta = message_map[type]
notify.send(
type="generic_message",
sender=vpn,
target=vpn,
action=action,
verb=meta["verb"],
message=meta["message"].format(
action=action,
target=str(vpn),
status_code=status_code,
verb=meta["verb"],
),
level=meta["level"],
)

def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
"""
This method handles API calls and their responses
Expand Down Expand Up @@ -105,13 +60,10 @@ def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
response.raise_for_status()
logger.info(info_msg)
if send_notification:
task_result = cache.get(task_key)
if task_result == "error":
self._send_api_task_notification("recovery", **kwargs)
cache.set(task_key, "success", None)
handle_recovery_notification(task_key, **kwargs)
except RequestException as e:
if response.status_code in self._RECOVERABLE_API_CODES:
retry_logger = logger.warn
retry_logger = logger.warning
# When retry limit is reached, use error logging
if self.request.retries == self.max_retries:
retry_logger = logger.error
Expand All @@ -122,12 +74,7 @@ def handle_api_call(self, fn, *args, send_notification=True, **kwargs):
raise e
logger.error(f"{err_msg}, Error: {e}")
if send_notification:
task_result = cache.get(task_key)
if task_result in (None, "success"):
cache.set(task_key, "error", None)
self._send_api_task_notification(
"error", status_code=response.status_code, **kwargs
)
handle_error_notification(task_key, exception=e, **kwargs)
return (response, updated_config) if updated_config else response


Expand Down
4 changes: 3 additions & 1 deletion openwisp_controller/config/tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ class TestNotifications(
app_label = "config"
_ZT_SERVICE_REQUESTS = "openwisp_controller.config.api.zerotier_service.requests"
_ZT_API_TASKS_INFO_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.info"
_ZT_API_TASKS_WARN_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.warn"
_ZT_API_TASKS_WARN_LOGGER = (
"openwisp_controller.config.tasks_zerotier.logger.warning"
)
_ZT_API_TASKS_ERR_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.error"
# As the locmem cache does not support the redis backend cache.keys() method
_ZT_API_TASKS_LOCMEM_CACHE_KEYS = f"{settings.CACHES['default']['BACKEND']}.keys"
Expand Down
36 changes: 26 additions & 10 deletions openwisp_controller/config/tests/test_vpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from subprocess import CalledProcessError, TimeoutExpired
from unittest import mock

import requests
from celery.exceptions import Retry, SoftTimeLimitExceeded
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.signals import post_save
from django.db.utils import IntegrityError
from django.http.response import HttpResponse, HttpResponseNotFound
from django.test import TestCase, TransactionTestCase
from requests.exceptions import ConnectionError, RequestException, Timeout
from swapper import load_model
Expand Down Expand Up @@ -728,9 +728,12 @@ def test_trigger_vpn_server_endpoint_invalid_vpn_id(self):


class TestWireguardTransaction(BaseTestVpn, TestWireguardVpnMixin, TransactionTestCase):
mock_response = mock.Mock(spec=requests.Response)
mock_response.status_code = 200
mock_response.raise_for_status = mock.Mock()

@mock.patch(
"openwisp_controller.config.tasks.requests.post",
return_value=HttpResponse(status=200),
"openwisp_controller.config.tasks.requests.post", return_value=mock_response
)
def test_auto_peer_configuration(self, *args):
self.assertEqual(IpAddress.objects.count(), 0)
Expand Down Expand Up @@ -824,6 +827,9 @@ def test_update_vpn_server_configuration(self):
f"Cannot update configuration of {vpn.name} VPN server, "
"webhook endpoint and authentication token are empty."
)
success_response = mock.Mock(spec=requests.Response)
success_response.status_code = 200
success_response.raise_for_status = mock.Mock()

with self.subTest("Webhook endpoint and authentication endpoint is present"):
vpn.webhook_endpoint = "https://example.com"
Expand All @@ -834,24 +840,29 @@ def test_update_vpn_server_configuration(self):
with mock.patch(
"openwisp_controller.config.tasks.logger.info"
) as mocked_logger, mock.patch(
"requests.post", return_value=HttpResponse()
"requests.post", return_value=success_response
):
post_save.send(
instance=vpn_client, sender=vpn_client._meta.model, created=False
)
mocked_logger.assert_called_once_with(
f"Triggered update webhook of VPN Server UUID: {vpn.pk}"
)
fail_response = mock.Mock(spec=requests.Response)
fail_response.status_code = 404
fail_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
"Not Found"
)

with mock.patch("logging.Logger.error") as mocked_logger, mock.patch(
"requests.post", return_value=HttpResponseNotFound()
with mock.patch("logging.Logger.warning") as mocked_logger, mock.patch(
"requests.post", return_value=fail_response
):
post_save.send(
instance=vpn_client, sender=vpn_client._meta.model, created=False
)
mocked_logger.assert_called_once_with(
"Failed to update VPN Server configuration. "
f"Response status code: 404, VPN Server UUID: {vpn.pk}"
f"Error: Not Found, VPN Server UUID: {vpn.pk}"
)

def test_vpn_peers_changed(self):
Expand Down Expand Up @@ -985,9 +996,12 @@ def test_auto_client(self):
class TestVxlanTransaction(
BaseTestVpn, TestVxlanWireguardVpnMixin, TransactionTestCase
):
mock_response = mock.Mock(spec=requests.Response)
mock_response.status_code = 200
mock_response.raise_for_status = mock.Mock()

@mock.patch(
"openwisp_controller.config.tasks.requests.post",
return_value=HttpResponse(status=200),
"openwisp_controller.config.tasks.requests.post", return_value=mock_response
)
def test_auto_peer_configuration(self, *args):
self.assertEqual(IpAddress.objects.count(), 0)
Expand Down Expand Up @@ -1460,7 +1474,9 @@ class TestZeroTierTransaction(
):
_ZT_SERVICE_REQUESTS = "openwisp_controller.config.api.zerotier_service.requests"
_ZT_API_TASKS_INFO_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.info"
_ZT_API_TASKS_WARN_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.warn"
_ZT_API_TASKS_WARN_LOGGER = (
"openwisp_controller.config.tasks_zerotier.logger.warning"
)
_ZT_API_TASKS_ERR_LOGGER = "openwisp_controller.config.tasks_zerotier.logger.error"
# As the locmem cache does not support the redis backend cache.keys() method
_ZT_API_TASKS_LOCMEM_CACHE_KEYS = f"{settings.CACHES['default']['BACKEND']}.keys"
Expand Down
61 changes: 61 additions & 0 deletions openwisp_controller/config/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging

from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 as base_get_object_or_404
from django.urls import path, re_path
from django.utils.translation import gettext_lazy as _
from openwisp_notifications.signals import notify
from openwisp_notifications.utils import _get_object_link

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -206,3 +209,61 @@ def get_default_templates_queryset(
def get_config_error_notification_target_url(obj, field, absolute_url=True):
url = _get_object_link(obj._related_object(field), absolute_url)
return f"{url}#config-group"


def send_api_task_notification(type, **kwargs):
vpn = kwargs.get("instance")
action = kwargs.get("action").replace("_", " ")
exception = kwargs.get("exception")
# Adding some delay here to prevent overlapping
# of the django success message container
# with the ow-notification container
# https://github.com/openwisp/openwisp-notifications/issues/264
# sleep(2)
message_map = {
"error": {
"verb": _("encountered an unrecoverable error"),
"message": _(
"Unable to perform {action} operation on the "
"{target} VPN server due to an "
"unrecoverable error "
"({error_type})"
),
"level": "error",
},
"success": {
"verb": _("has been completed successfully"),
"message": _("The {action} operation on {target} {verb}."),
"level": "info",
},
}
meta = message_map[type]
notify.send(
sender=vpn,
target=vpn,
type="generic_message",
action_object=vpn,
verb=meta["verb"],
message=meta["message"].format(
action=action,
target=str(vpn),
error_type=exception.__class__.__name__ if exception else "",
verb=meta["verb"],
),
description=str(exception) if exception else "",
level=meta["level"],
)


def handle_recovery_notification(task_key, **kwargs):
task_result = cache.get(task_key)
if task_result == "error":
send_api_task_notification("success", **kwargs)
cache.set(task_key, "success", timeout=None)


def handle_error_notification(task_key, **kwargs):
cached_value = cache.get(task_key)
if cached_value != "error":
cache.set(task_key, "error", timeout=None)
send_api_task_notification("error", **kwargs)
Loading