Skip to content

Commit

Permalink
[connection] Added auto_add feature to Credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesifier committed Dec 6, 2018
1 parent 70dab1c commit 55a168d
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 15 deletions.
10 changes: 8 additions & 2 deletions openwisp_controller/connection/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin

from openwisp_utils.admin import MultitenantOrgFilter, TimeReadonlyAdminMixin
from openwisp_users.multitenancy import MultitenantOrgFilter
from openwisp_utils.admin import TimeReadonlyAdminMixin

from ..admin import MultitenantAdminMixin
from ..config.admin import DeviceAdmin
Expand All @@ -9,7 +10,12 @@

@admin.register(Credentials)
class CredentialsAdmin(MultitenantAdminMixin, TimeReadonlyAdminMixin, admin.ModelAdmin):
list_display = ('name', 'organization', 'connector', 'created', 'modified')
list_display = ('name',
'organization',
'connector',
'auto_add',
'created',
'modified')
list_filter = [('organization', MultitenantOrgFilter),
'connector']
list_select_related = ('organization',)
Expand Down
34 changes: 21 additions & 13 deletions openwisp_controller/connection/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from celery.task.control import inspect
from django.apps import AppConfig
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from django_netjsonconfig.signals import config_modified

Expand All @@ -17,22 +18,29 @@ def ready(self):
to the ``update_config`` celery task
which will be executed in the background
"""
from .tasks import update_config
config_modified.connect(self.config_modified_receiver,
dispatch_uid='connection.update_config')

from ..config.models import Config
from .models import Credentials

def config_modified_receiver(**kwargs):
d = kwargs['device']
conn_count = d.deviceconnection_set.count()
# if device has no connection specified
# or update is already in progress, stop here
if conn_count < 1 or self._is_update_in_progress(d.id):
return
update_config.delay(d.id)
post_save.connect(Credentials.auto_add_credentials_to_device,
sender=Config,
dispatch_uid='connection.auto_add_credentials')

config_modified.connect(config_modified_receiver,
dispatch_uid='connection.update_config',
weak=False)
@classmethod
def config_modified_receiver(cls, **kwargs):
from .tasks import update_config
d = kwargs['device']
conn_count = d.deviceconnection_set.count()
# if device has no connection specified
# or update is already in progress, stop here
if conn_count < 1 or cls._is_update_in_progress(d.id):
return
update_config.delay(d.id)

def _is_update_in_progress(self, device_id):
@classmethod
def _is_update_in_progress(cls, device_id):
active = inspect().active()
if not active:
return False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.1.3 on 2018-12-05 13:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('connection', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='credentials',
name='auto_add',
field=models.BooleanField(default=False, help_text='automatically add these credentials to the devices of this organization; if no organization is specified will be added to all the new devices', verbose_name='auto add'),
),
]
60 changes: 60 additions & 0 deletions openwisp_controller/connection/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from openwisp_utils.base import TimeStampedEditableModel

from . import settings as app_settings
from ..config.models import Device
from .utils import get_interfaces

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -63,6 +64,12 @@ class Credentials(ConnectorMixin, ShareableOrgMixin, BaseModel):
help_text=_('global connection parameters'),
load_kwargs={'object_pairs_hook': collections.OrderedDict},
dump_kwargs={'indent': 4})
auto_add = models.BooleanField(_('auto add'),
default=False,
help_text=_('automatically add these credentials '
'to the devices of this organization; '
'if no organization is specified will '
'be added to all the new devices'))

class Meta:
verbose_name = _('Access credentials')
Expand All @@ -71,6 +78,59 @@ class Meta:
def __str__(self):
return '{0} ({1})'.format(self.name, self.get_connector_display())

def save(self, *args, **kwargs):
super(Credentials, self).save(*args, **kwargs)
self.auto_add_to_devices()

def auto_add_to_devices(self):
"""
When ``auto_add`` is ``True``, adds the credentials
to each relevant ``Device`` and ``DeviceConnection`` objects
"""
if not self.auto_add:
return
devices = Device.objects.all()
org = self.organization
if org:
devices = devices.filter(organization=org)
# exclude devices which have been already added
devices = devices.exclude(deviceconnection__credentials=self)
for device in devices:
conn = DeviceConnection(device=device,
credentials=self,
enabled=True)
conn.full_clean()
conn.save()

@classmethod
def auto_add_credentials_to_device(cls, instance, created, **kwargs):
"""
Adds relevant credentials as ``DeviceConnection``
when a device is created, this is called from a
post_save signal receiver hooked to the ``Config`` model
(why ``Config`` and not ``Device``? because at the moment
we can automatically create a DeviceConnection if we have
a ``Config`` object)
"""
if not created:
return
device = instance.device
# select credentials which
# - are flagged as auto_add
# - belong to the same organization of the device
# OR
# belong to no organization (hence are shared)
conditions = (models.Q(organization=device.organization) |
models.Q(organization=None))
credentials = cls.objects.filter(conditions) \
.filter(auto_add=True)
for cred in credentials:
conn = DeviceConnection(device=device,
credentials=cred,
enabled=True)
conn.full_clean()
conn.save()


class DeviceConnection(ConnectorMixin, TimeStampedEditableModel):
_connector_field = 'update_strategy'
Expand Down
60 changes: 60 additions & 0 deletions openwisp_controller/connection/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.core.exceptions import ValidationError
from django.test import TestCase

from openwisp_users.models import Organization

from .. import settings as app_settings
from ..models import Credentials, DeviceIp
from ..utils import get_interfaces
Expand Down Expand Up @@ -188,3 +190,61 @@ def test_device_connection_credential_org_validation(self):
self.assertIn('credentials', e.message_dict)
else:
self.fail('ValidationError not raised')

def test_auto_add_to_new_device(self):
c = self._create_credentials(auto_add=True,
organization=None)
self._create_credentials(name='cred2',
auto_add=False,
organization=None)
d = self._create_device(organization=Organization.objects.first())
self._create_config(device=d)
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 1)
self.assertEqual(d.deviceconnection_set.first().credentials, c)

def test_auto_add_to_existing_device_on_creation(self):
d = self._create_device(organization=Organization.objects.first())
self._create_config(device=d)
self.assertEqual(d.deviceconnection_set.count(), 0)
c = self._create_credentials(auto_add=True,
organization=None)
org2 = Organization.objects.create(name='org2', slug='org2')
self._create_credentials(name='cred2',
auto_add=True,
organization=org2)
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 1)
self.assertEqual(d.deviceconnection_set.first().credentials, c)
self._create_credentials(name='cred3',
auto_add=False,
organization=None)
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 1)
self.assertEqual(d.deviceconnection_set.first().credentials, c)

def test_auto_add_to_existing_device_on_edit(self):
d = self._create_device(organization=Organization.objects.first())
self._create_config(device=d)
self.assertEqual(d.deviceconnection_set.count(), 0)
c = self._create_credentials(auto_add=False,
organization=None)
org2 = Organization.objects.create(name='org2', slug='org2')
self._create_credentials(name='cred2',
auto_add=True,
organization=org2)
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 0)
c.auto_add = True
c.full_clean()
c.save()
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 1)
self.assertEqual(d.deviceconnection_set.first().credentials, c)
# ensure further edits are idempotent
c.name = 'changed'
c.full_clean()
c.save()
d.refresh_from_db()
self.assertEqual(d.deviceconnection_set.count(), 1)
self.assertEqual(d.deviceconnection_set.first().credentials, c)

0 comments on commit 55a168d

Please sign in to comment.