Skip to content

Commit 5cf1012

Browse files
NoumbissiValerenemesifier
authored andcommitted
[fix] Fixed failures in update_config operation #205
The update_config operation will be executed only when the transaction is committed to the database. Also handle rare but possible error conditions. Fixes #205
1 parent bcffa55 commit 5cf1012

File tree

4 files changed

+71
-32
lines changed

4 files changed

+71
-32
lines changed

openwisp_controller/connection/apps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from celery.task.control import inspect
22
from django.apps import AppConfig
3+
from django.db import transaction
34
from django.db.models.signals import post_save
45
from django.utils.translation import ugettext_lazy as _
56
from swapper import load_model
@@ -41,7 +42,7 @@ def config_modified_receiver(cls, **kwargs):
4142
# or update is already in progress, stop here
4243
if conn_count < 1 or cls._is_update_in_progress(d.id):
4344
return
44-
update_config.delay(d.id)
45+
transaction.on_commit(lambda: update_config.delay(d.id))
4546

4647
@classmethod
4748
def _is_update_in_progress(cls, device_id):

openwisp_controller/connection/tasks.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import logging
12
from time import sleep
23

34
from celery import shared_task
5+
from django.core.exceptions import ObjectDoesNotExist
46
from swapper import load_model
57

68
Device = load_model('config', 'Device')
9+
logger = logging.getLogger(__name__)
710

811

912
@shared_task
@@ -17,7 +20,11 @@ def update_config(device_id):
1720
sleep(2)
1821
# avoid repeating the operation multiple times
1922
device = Device.objects.select_related('config').get(pk=device_id)
20-
if device.config.status == 'applied':
23+
try:
24+
if device.config.status == 'applied':
25+
return
26+
except ObjectDoesNotExist:
27+
logger.warning(f'Config with device id: {device_id} does not exist')
2128
return
2229
qs = device.deviceconnection_set.filter(device_id=device_id, enabled=True)
2330
conn = qs.first()

openwisp_controller/connection/tests/test_models.py

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from .. import settings as app_settings
1313
from ..signals import is_working_changed
14+
from ..tasks import update_config
1415
from .utils import CreateConnectionsMixin
1516

1617
Config = load_model('config', 'Config')
@@ -256,36 +257,6 @@ def _exec_command_return_value(
256257
stderr_.read().decode('utf8').strip.return_value = stderr
257258
return (stdin_, stdout_, stderr_)
258259

259-
@mock.patch(_connect_path)
260-
def test_device_config_update(self, mocked_connect):
261-
org1 = self._create_org(name='org1')
262-
cred = self._create_credentials_with_key(
263-
organization=org1, port=self.ssh_server.port
264-
)
265-
device = self._create_device(organization=org1)
266-
update_strategy = app_settings.UPDATE_STRATEGIES[0][0]
267-
c = self._create_config(device=device, status='applied')
268-
self._create_device_connection(
269-
device=device, credentials=cred, update_strategy=update_strategy
270-
)
271-
c.config = {
272-
'interfaces': [
273-
{
274-
'name': 'eth10',
275-
'type': 'ethernet',
276-
'addresses': [{'family': 'ipv4', 'proto': 'dhcp'}],
277-
}
278-
]
279-
}
280-
c.full_clean()
281-
282-
with mock.patch(self._exec_command_path) as mocked:
283-
mocked.return_value = self._exec_command_return_value()
284-
c.save()
285-
mocked.assert_called_once()
286-
c.refresh_from_db()
287-
self.assertEqual(c.status, 'applied')
288-
289260
@mock.patch(_connect_path)
290261
def test_ssh_exec_exit_code(self, *args):
291262
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
@@ -378,3 +349,9 @@ def test_device_connection_set_connector(self):
378349
self._create_config(device=dev2)
379350
dc2 = self._create_device_connection(device=dev2)
380351
self.assertFalse(hasattr(dc2.connector_instance, 'IS_MODIFIED'))
352+
353+
@mock.patch('logging.Logger.warning')
354+
def test_update_config_task_resilient_to_failure(self, mocked):
355+
pk = self._create_device().pk
356+
update_config.delay(pk)
357+
mocked.assert_called_with(f'Config with device id: {pk} does not exist')
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from unittest import mock
2+
3+
from django.test import TransactionTestCase
4+
5+
from .. import settings as app_settings
6+
from .utils import CreateConnectionsMixin
7+
8+
9+
class TestTransactionBlocks(CreateConnectionsMixin, TransactionTestCase):
10+
_connect_path = 'paramiko.SSHClient.connect'
11+
_exec_command_path = 'paramiko.SSHClient.exec_command'
12+
13+
def _exec_command_return_value(
14+
self, stdin='', stdout='mocked', stderr='', exit_code=0
15+
):
16+
stdin_ = mock.Mock()
17+
stdout_ = mock.Mock()
18+
stderr_ = mock.Mock()
19+
stdin_.read().decode('utf8').strip.return_value = stdin
20+
stdout_.read().decode('utf8').strip.return_value = stdout
21+
stdout_.channel.recv_exit_status.return_value = exit_code
22+
stderr_.read().decode('utf8').strip.return_value = stderr
23+
return (stdin_, stdout_, stderr_)
24+
25+
@mock.patch(_connect_path)
26+
def test_device_config_update(self, mocked_connect):
27+
org1 = self._create_org(name='org1')
28+
cred = self._create_credentials_with_key(
29+
organization=org1, port=self.ssh_server.port
30+
)
31+
device = self._create_device(organization=org1)
32+
update_strategy = app_settings.UPDATE_STRATEGIES[0][0]
33+
c = self._create_config(device=device, status='applied')
34+
self._create_device_connection(
35+
device=device, credentials=cred, update_strategy=update_strategy
36+
)
37+
c.config = {
38+
'interfaces': [
39+
{
40+
'name': 'eth10',
41+
'type': 'ethernet',
42+
'addresses': [{'family': 'ipv4', 'proto': 'dhcp'}],
43+
}
44+
]
45+
}
46+
c.full_clean()
47+
48+
with mock.patch(self._exec_command_path) as mocked:
49+
mocked.return_value = self._exec_command_return_value()
50+
c.save()
51+
c.refresh_from_db()
52+
mocked.assert_called_once()
53+
c.refresh_from_db()
54+
self.assertEqual(c.status, 'applied')

0 commit comments

Comments
 (0)