Skip to content

Commit f46fd1b

Browse files
[fix] Avoid mutating device.name in _get_common_name #<issue-number>
1 parent 762ae3c commit f46fd1b

File tree

3 files changed

+35
-118
lines changed

3 files changed

+35
-118
lines changed

openwisp_controller/config/base/vpn.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -892,12 +892,15 @@ def _get_common_name(self):
892892
"""
893893
d = self.config.device
894894
end = 63 - len(d.mac_address)
895-
d.name = d.name[:end]
895+
name = d.name[:end]
896896
unique_slug = shortuuid.ShortUUID().random(length=8)
897897
cn_format = app_settings.COMMON_NAME_FORMAT
898-
if cn_format == "{mac_address}-{name}" and d.name == d.mac_address:
898+
899+
if cn_format == "{mac_address}-{name}" and name == d.mac_address:
899900
cn_format = "{mac_address}"
900-
common_name = cn_format.format(**d.__dict__)[:55]
901+
902+
data = {**d.__dict__, "name": name}
903+
common_name = cn_format.format(**data)[:55]
901904
common_name = f"{common_name}-{unique_slug}"
902905
return common_name
903906

Lines changed: 0 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1 @@
1-
import logging
2-
import time
31

4-
import swapper
5-
from celery import current_app, shared_task
6-
from celery.exceptions import SoftTimeLimitExceeded
7-
from django.core.exceptions import ObjectDoesNotExist
8-
from django.utils.translation import gettext_lazy as _
9-
from swapper import load_model
10-
11-
from . import settings as app_settings
12-
from .connectors.exceptions import CommandTimeoutException
13-
from .exceptions import NoWorkingDeviceConnectionError
14-
15-
logger = logging.getLogger(__name__)
16-
_TASK_NAME = "openwisp_controller.connection.tasks.update_config"
17-
18-
19-
def _is_update_in_progress(device_id, current_task_id=None):
20-
active = current_app.control.inspect().active()
21-
if not active:
22-
return False
23-
# check if there's any other running task before adding it
24-
# exclude the current task by comparing task IDs
25-
for task_list in active.values():
26-
for task in task_list:
27-
if (
28-
task["name"] == _TASK_NAME
29-
and str(device_id) in task["args"]
30-
and task["id"] != current_task_id
31-
):
32-
return True
33-
return False
34-
35-
36-
@shared_task(bind=True)
37-
def update_config(self, device_id):
38-
"""
39-
Launches the ``update_config()`` operation
40-
of a specific device in the background
41-
"""
42-
Device = swapper.load_model(*swapper.split(app_settings.UPDATE_CONFIG_MODEL))
43-
DeviceConnection = swapper.load_model("connection", "DeviceConnection")
44-
# wait for the saving operations of this device to complete
45-
# (there may be multiple ones happening at the same time)
46-
time.sleep(2)
47-
try:
48-
device = Device.objects.select_related("config").get(pk=device_id)
49-
# abort operation if device shouldn't be updated
50-
if not device.can_be_updated():
51-
logger.info(f"{device} (pk: {device_id}) is not going to be updated")
52-
return
53-
except ObjectDoesNotExist as e:
54-
logger.warning(f'update_config("{device_id}") failed: {e}')
55-
return
56-
if _is_update_in_progress(device_id, current_task_id=self.request.id):
57-
return
58-
try:
59-
device_conn = DeviceConnection.get_working_connection(device)
60-
except NoWorkingDeviceConnectionError:
61-
return
62-
else:
63-
logger.info(f"Updating {device} (pk: {device_id})")
64-
65-
try:
66-
device_conn.update_config()
67-
except Exception as e:
68-
logger.error(f"update_config failed for device {device_id}: {e}")
69-
raise
70-
finally:
71-
# ensure connection is closed
72-
close_method = getattr(device_conn, "close", None)
73-
if callable(close_method):
74-
try:
75-
close_method()
76-
try:
77-
device_conn.close()
78-
except Exception as close_err:
79-
logger.warning(f"Error closing connection: {close_err}")
80-
81-
82-
# task timeout is SSH_COMMAND_TIMEOUT plus a 20% margin
83-
@shared_task(soft_time_limit=app_settings.SSH_COMMAND_TIMEOUT * 1.2)
84-
def launch_command(command_id):
85-
"""
86-
Launches execution of commands in the background
87-
"""
88-
Command = load_model("connection", "Command")
89-
try:
90-
command = Command.objects.get(pk=command_id)
91-
except Command.DoesNotExist as e:
92-
logger.warning(f'launch_command("{command_id}") failed: {e}')
93-
return
94-
try:
95-
command.execute()
96-
except SoftTimeLimitExceeded:
97-
command.status = "failed"
98-
command._add_output(_("Background task time limit exceeded."))
99-
command.save()
100-
except CommandTimeoutException as e:
101-
command.status = "failed"
102-
command._add_output(_(f"The command took longer than expected: {e}"))
103-
command.save()
104-
except Exception as e:
105-
logger.exception(
106-
f"An exception was raised while executing command {command_id}"
107-
)
108-
command.status = "failed"
109-
command._add_output(_(f"Internal system error: {e}"))
110-
command.save()
111-
112-
113-
@shared_task(soft_time_limit=3600)
114-
def auto_add_credentials_to_devices(credential_id, organization_id):
115-
Credentials = load_model("connection", "Credentials")
116-
Credentials.auto_add_to_devices(credential_id, organization_id)

openwisp_controller/connection/tests/test_tasks.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,32 @@ def test_update_config_hostname_changed_on_reregister(self, mocked_update_config
156156
)
157157
self.assertEqual(response.status_code, 201)
158158
mocked_update_config.assert_not_called()
159+
160+
def test_update_config_closes_connection_on_exception(self):
161+
with mock.patch(
162+
"openwisp_controller.connection.tasks.DeviceConnection.get_working_connection"
163+
) as mocked_get_conn, mock.patch(
164+
"openwisp_controller.connection.tasks.Device.objects.select_related"
165+
) as mocked_device, mock.patch(
166+
"openwisp_controller.connection.tasks._is_update_in_progress",
167+
return_value=False,
168+
):
169+
device = mock.MagicMock()
170+
device.can_be_updated.return_value = True
171+
mocked_device.return_value.get.return_value = device
172+
173+
device_conn = mock.MagicMock()
174+
device_conn.update_config.side_effect = Exception("Test exception")
175+
mocked_get_conn.return_value = device_conn
176+
177+
from openwisp_controller.connection.tasks import update_config
178+
179+
try:
180+
update_config(device_id=1)
181+
except Exception:
182+
pass
183+
184+
self.assertTrue(
185+
device_conn.disconnect.called or device_conn.close.called,
186+
"Connection was not properly closed",
187+
)

0 commit comments

Comments
 (0)