Skip to content

Commit 5bb2e28

Browse files
authored
Merge branch 'master' into fix/update-config-ssh-leak-silent-failure
2 parents 40f17cc + 45b24b6 commit 5bb2e28

File tree

3 files changed

+116
-17
lines changed

3 files changed

+116
-17
lines changed

openwisp_controller/connection/tasks.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@
1616
_TASK_NAME = "openwisp_controller.connection.tasks.update_config"
1717

1818

19-
def _is_update_in_progress(device_id):
19+
def _is_update_in_progress(device_id, current_task_id=None):
2020
active = current_app.control.inspect().active()
2121
if not active:
2222
return False
2323
# check if there's any other running task before adding it
24+
# exclude the current task by comparing task IDs
2425
for task_list in active.values():
2526
for task in task_list:
26-
if task["name"] == _TASK_NAME and str(device_id) in task["args"]:
27+
if (
28+
task["name"] == _TASK_NAME
29+
and str(device_id) in task["args"]
30+
and task["id"] != current_task_id
31+
):
2732
return True
2833
return False
2934

3035

31-
@shared_task
32-
def update_config(device_id):
36+
@shared_task(bind=True)
37+
def update_config(self, device_id):
3338
"""
3439
Launches the ``update_config()`` operation
3540
of a specific device in the background
@@ -48,7 +53,7 @@ def update_config(device_id):
4853
except ObjectDoesNotExist as e:
4954
logger.warning(f'update_config("{device_id}") failed: {e}')
5055
return
51-
if _is_update_in_progress(device_id):
56+
if _is_update_in_progress(device_id, current_task_id=self.request.id):
5257
return
5358
try:
5459
device_conn = DeviceConnection.get_working_connection(device)

openwisp_controller/connection/tests/test_models.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import socket
22
from unittest import mock
33
from unittest.mock import PropertyMock
4+
from uuid import uuid4
45

56
import paramiko
67
from django.contrib.auth.models import ContentType
@@ -1041,20 +1042,56 @@ def _assert_applying_conf_test_command(mocked_exec):
10411042
@mock.patch.object(DeviceConnection, "update_config")
10421043
@mock.patch.object(DeviceConnection, "get_working_connection")
10431044
def test_device_update_config_in_progress(
1044-
self, mocked_get_working_connection, update_config, mocked_sleep
1045+
self, mocked_get_working_connection, mocked_update_config, mocked_sleep
10451046
):
10461047
conf = self._prepare_conf_object()
10471048

1048-
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1049-
mocked_active.return_value = {
1050-
"task": [{"name": _TASK_NAME, "args": [str(conf.device.pk)]}]
1051-
}
1052-
conf.config = {"general": {"timezone": "UTC"}}
1053-
conf.full_clean()
1054-
conf.save()
1055-
mocked_active.assert_called_once()
1056-
mocked_get_working_connection.assert_not_called()
1057-
update_config.assert_not_called()
1049+
with self.subTest("More than one update_config task active for the device"):
1050+
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1051+
mocked_active.return_value = {
1052+
"task": [
1053+
{
1054+
"name": _TASK_NAME,
1055+
"args": [str(conf.device.pk)],
1056+
"id": str(uuid4()),
1057+
}
1058+
]
1059+
}
1060+
conf.config = {"general": {"timezone": "UTC"}}
1061+
conf.full_clean()
1062+
conf.save()
1063+
mocked_active.assert_called_once()
1064+
mocked_get_working_connection.assert_not_called()
1065+
mocked_update_config.assert_not_called()
1066+
1067+
Config.objects.update(status="applied")
1068+
mocked_get_working_connection.return_value = (
1069+
conf.device.deviceconnection_set.first()
1070+
)
1071+
with self.subTest("Only one task is active for the device"):
1072+
task_id = str(uuid4())
1073+
with mock.patch(
1074+
"celery.app.control.Inspect.active"
1075+
) as mocked_active, mock.patch(
1076+
"celery.app.task.Context.id",
1077+
new_callable=mock.PropertyMock,
1078+
return_value=task_id,
1079+
):
1080+
mocked_active.return_value = {
1081+
"task": [
1082+
{
1083+
"name": _TASK_NAME,
1084+
"args": [str(conf.device.pk)],
1085+
"id": task_id,
1086+
}
1087+
]
1088+
}
1089+
conf.config = {"general": {"timezone": "Asia/Kolkata"}}
1090+
conf.full_clean()
1091+
conf.save()
1092+
mocked_active.assert_called_once()
1093+
mocked_get_working_connection.assert_called_once()
1094+
mocked_update_config.assert_called_once()
10581095

10591096
@mock.patch("time.sleep")
10601097
@mock.patch.object(DeviceConnection, "update_config")
@@ -1068,8 +1105,15 @@ def test_device_update_config_not_in_progress(
10681105
)
10691106

10701107
with mock.patch("celery.app.control.Inspect.active") as mocked_active:
1108+
# Mock a task running for a different device (args is different)
10711109
mocked_active.return_value = {
1072-
"task": [{"name": _TASK_NAME, "args": ["..."]}]
1110+
"task": [
1111+
{
1112+
"name": _TASK_NAME,
1113+
"args": ["another-device-id"], # Different device
1114+
"id": "different-task-id",
1115+
}
1116+
]
10731117
}
10741118
conf.config = {"general": {"timezone": "UTC"}}
10751119
conf.full_clean()

openwisp_controller/connection/tests/test_tasks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,56 @@ class TestTasks(CreateConnectionsMixin, TestCase):
2121
"openwisp_controller.connection.base.models.AbstractDeviceConnection.connect"
2222
)
2323

24+
def _get_mocked_celery_active(self, device_id, task_id=None):
25+
return {
26+
"worker1": [
27+
{
28+
"name": tasks._TASK_NAME,
29+
"args": [device_id],
30+
"id": task_id or str(uuid.uuid4()),
31+
}
32+
]
33+
}
34+
35+
def test_is_update_in_progress_same_task(self):
36+
device_id = str(uuid.uuid4())
37+
task_id = str(uuid.uuid4())
38+
with mock.patch(
39+
"celery.app.control.Inspect.active",
40+
return_value=self._get_mocked_celery_active(device_id, task_id),
41+
):
42+
result = tasks._is_update_in_progress(device_id, current_task_id=task_id)
43+
self.assertEqual(result, False)
44+
45+
def test_is_update_in_progress_different_task(self):
46+
device_id = str(uuid.uuid4())
47+
current_task_id = str(uuid.uuid4())
48+
other_task_id = str(uuid.uuid4())
49+
with mock.patch(
50+
"celery.app.control.Inspect.active",
51+
return_value=self._get_mocked_celery_active(device_id, other_task_id),
52+
):
53+
result = tasks._is_update_in_progress(
54+
device_id, current_task_id=current_task_id
55+
)
56+
self.assertEqual(result, True)
57+
58+
def test_is_update_in_progress_no_tasks(self):
59+
device_id = str(uuid.uuid4())
60+
with mock.patch("celery.app.control.Inspect.active", return_value={}):
61+
result = tasks._is_update_in_progress(device_id)
62+
self.assertEqual(result, False)
63+
64+
def test_is_update_in_progress_different_device(self):
65+
device_id = str(uuid.uuid4())
66+
other_device_id = str(uuid.uuid4())
67+
with mock.patch(
68+
"celery.app.control.Inspect.active",
69+
return_value=self._get_mocked_celery_active(other_device_id),
70+
):
71+
result = tasks._is_update_in_progress(device_id)
72+
self.assertEqual(result, False)
73+
2474
@mock.patch("logging.Logger.warning")
2575
@mock.patch("time.sleep")
2676
def test_update_config_missing_config(self, mocked_sleep, mocked_warning):

0 commit comments

Comments
 (0)