Skip to content

Commit 0c74427

Browse files
codesankalppandafy
andauthored
[change] Support for instantiating OpenWrt with both 21 and legacy mode #618
Closes #618 Co-authored-by: Gagan Deep <pandafy.dev@gmail.com>
1 parent 15ce46d commit 0c74427

File tree

5 files changed

+154
-2
lines changed

5 files changed

+154
-2
lines changed

README.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,60 @@ reset to empty state to avoid potential conflicts.
730730
Set this to ``False`` if every organization has its dedicated management
731731
tunnel with a dedicated address space that is reachable by the OpenWISP server.
732732

733+
``OPENWISP_CONTROLLER_DSA_OS_MAPPING``
734+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
735+
736+
+--------------+----------+
737+
| **type**: | ``dict`` |
738+
+--------------+----------+
739+
| **default**: | ``{}`` |
740+
+--------------+----------+
741+
742+
OpenWISP Controller can figure out whether it should use the new OpenWrt syntax
743+
for DSA interfaces (Distributed Switch Architecture) introduced in OpenWrt 21 by
744+
reading the ``os`` field of the ``Device`` object. However, if the firmware you
745+
are using has a custom firmware identifier, the system will not be able to figure
746+
out whether it should use the new syntax and it will default to
747+
`OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK <#openwisp_controller_dsa_default_fallback>`_.
748+
749+
If you want to make sure the system can parse your custom firmware
750+
identifier properly, you can follow the example below.
751+
752+
For the sake of the example, the OS identifier ``MyCustomFirmware 2.0``
753+
corresponds to ``OpenWrt 19.07``, while ``MyCustomFirmware 2.1`` corresponds to
754+
``OpenWrt 21.02``. Configuring this setting as indicated below will allow
755+
OpenWISP to supply the right syntax automatically.
756+
757+
Example:
758+
759+
.. code-block:: python
760+
761+
OPENWISP_CONTROLLER_DSA_OS_MAPPING = {
762+
'netjsonconfig.OpenWrt': {
763+
# OpenWrt >=21.02 configuration syntax will be used for
764+
# these OS identifiers.
765+
'>=21.02': [r'MyCustomFirmware 2.1(.*)'],
766+
# OpenWrt <=21.02 configuration syntax will be used for
767+
# these OS identifiers.
768+
'<21.02': [r'MyCustomFirmware 2.0(.*)']
769+
}
770+
}
771+
772+
**Note**: The OS identifier should be a regular expression as shown in above example.
773+
774+
``OPENWISP_CONTROLLER_DSA_DEFAULT_FALLBACK``
775+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
776+
777+
+--------------+----------+
778+
| **type**: | ``bool`` |
779+
+--------------+----------+
780+
| **default**: | ``True`` |
781+
+--------------+----------+
782+
783+
The value of this setting decides whether to use DSA syntax
784+
(OpenWrt >=21 configuration syntax) if openwisp-controller fails
785+
to make that decision automatically.
786+
733787
REST API
734788
--------
735789

openwisp_controller/config/base/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ def backend_instance(self):
141141
"""
142142
return self.get_backend_instance()
143143

144-
def get_backend_instance(self, template_instances=None, context=None):
144+
def get_backend_instance(self, template_instances=None, context=None, **kwargs):
145145
"""
146146
allows overriding config and templates
147147
needed for pre validation of m2m
148148
"""
149149
backend = self.backend_class
150-
kwargs = {'config': self.get_config()}
150+
kwargs.update({'config': self.get_config()})
151151
context = context or {}
152152
# determine if we can pass templates
153153
# expecting a many2many relationship

openwisp_controller/config/base/config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import collections
22
import logging
3+
import re
34

45
from cache_memoize import cache_memoize
56
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
@@ -8,6 +9,8 @@
89
from jsonfield import JSONField
910
from model_utils import Choices
1011
from model_utils.fields import StatusField
12+
from netjsonconfig import OpenWrt
13+
from packaging import version
1114
from swapper import get_model_name
1215

1316
from .. import settings as app_settings
@@ -356,6 +359,47 @@ def get_default_templates(self):
356359
organization_id=org_id, queryset=queryset, backend=self.backend
357360
)
358361

362+
def _should_use_dsa(self):
363+
if not hasattr(self, 'device') or not issubclass(self.backend_class, OpenWrt):
364+
return
365+
366+
if not self.device.os:
367+
# Device os field is empty. Early return to
368+
# prevent unnecessary computation.
369+
return app_settings.DSA_DEFAULT_FALLBACK
370+
371+
# Check if the device is using stock OpenWrt.
372+
openwrt_match = re.search(
373+
'[oO][pP][eE][nN][wW][rR][tT]\s*([\d.]+)', self.device.os
374+
)
375+
if openwrt_match:
376+
if version.parse(openwrt_match.group(1)) >= version.parse('21'):
377+
return True
378+
else:
379+
return False
380+
381+
# Device is using custom firmware
382+
if app_settings.DSA_OS_MAPPING:
383+
openwrt_based_firmware = app_settings.DSA_OS_MAPPING.get(
384+
'netjsonconfig.OpenWrt', {}
385+
)
386+
dsa_enabled_os = openwrt_based_firmware.get('>=21.02', [])
387+
dsa_disabled_os = openwrt_based_firmware.get('<21.02', [])
388+
for os in dsa_enabled_os:
389+
if re.search(os, self.device.os):
390+
return True
391+
for os in dsa_disabled_os:
392+
if re.search(os, self.device.os):
393+
return False
394+
395+
return app_settings.DSA_DEFAULT_FALLBACK
396+
397+
def get_backend_instance(self, template_instances=None, context=None, **kwargs):
398+
dsa_enabled = self._should_use_dsa()
399+
if dsa_enabled is not None:
400+
kwargs['dsa'] = dsa_enabled
401+
return super().get_backend_instance(template_instances, context, **kwargs)
402+
359403
def clean(self):
360404
"""
361405
* validates context field

openwisp_controller/config/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,5 @@ def get_settings_value(option, default):
7676
SHARED_MANAGEMENT_IP_ADDRESS_SPACE = get_settings_value(
7777
'SHARED_MANAGEMENT_IP_ADDRESS_SPACE', True
7878
)
79+
DSA_OS_MAPPING = get_settings_value('DSA_OS_MAPPING', {})
80+
DSA_DEFAULT_FALLBACK = get_settings_value('DSA_DEFAULT_FALLBACK', True)

openwisp_controller/config/tests/test_config.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,58 @@ def test_backend_instance(self):
5555
c = Config(backend='netjsonconfig.OpenWrt', config=config)
5656
self.assertIsInstance(c.backend_instance, OpenWrt)
5757

58+
@patch.object(app_settings, 'DSA_DEFAULT_FALLBACK', False)
59+
@patch.object(
60+
app_settings,
61+
'DSA_OS_MAPPING',
62+
{
63+
'netjsonconfig.OpenWrt': {
64+
'>=21.02': [r'MyCustomFirmware 2.1(.*)'],
65+
'<21.02': [r'MyCustomFirmware 2.0(.*)'],
66+
}
67+
},
68+
)
69+
def test_backend_openwrt_different_versions(self):
70+
with self.subTest('DSA enabled OpenWrt firmware'):
71+
c = Config(
72+
backend='netjsonconfig.OpenWrt',
73+
device=Device(name='test', os='OpenWrt 21.02.2 r16495-bf0c965af0'),
74+
)
75+
self.assertIsInstance(c.backend_instance, OpenWrt)
76+
self.assertEqual(c.backend_instance.dsa, True)
77+
78+
with self.subTest('DSA disabed OpenWrt Firmware'):
79+
c = Config(
80+
backend='netjsonconfig.OpenWrt',
81+
device=Device(name='test', os='OpenWrt 19.02.2 r16495-bf0c965af0'),
82+
)
83+
self.assertIsInstance(c.backend_instance, OpenWrt)
84+
self.assertEqual(c.backend_instance.dsa, False)
85+
86+
with self.subTest('DSA enabled custom firmware'):
87+
c = Config(
88+
backend='netjsonconfig.OpenWrt',
89+
device=Device(name='test', os='MyCustomFirmware 2.1.2'),
90+
)
91+
self.assertIsInstance(c.backend_instance, OpenWrt)
92+
self.assertEqual(c.backend_instance.dsa, True)
93+
94+
with self.subTest('DSA disabled custom firmware'):
95+
c = Config(
96+
backend='netjsonconfig.OpenWrt',
97+
device=Device(name='test', os='MyCustomFirmware 2.0.1'),
98+
)
99+
self.assertIsInstance(c.backend_instance, OpenWrt)
100+
self.assertEqual(c.backend_instance.dsa, False)
101+
102+
with self.subTest('Device os field is empty'):
103+
c = Config(
104+
backend='netjsonconfig.OpenWrt',
105+
device=Device(name='test', os=''),
106+
)
107+
self.assertIsInstance(c.backend_instance, OpenWrt)
108+
self.assertEqual(c.backend_instance.dsa, False)
109+
58110
def test_netjson_validation(self):
59111
config = {'interfaces': {'invalid': True}}
60112
c = Config(

0 commit comments

Comments
 (0)