From 55a168d78540557c9f02a0abf8d938226b4d610e Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 6 Dec 2018 14:33:54 +0100 Subject: [PATCH] [connection] Added auto_add feature to Credentials --- openwisp_controller/connection/admin.py | 10 +++- openwisp_controller/connection/apps.py | 34 +++++++---- .../migrations/0002_credentials_auto_add.py | 18 ++++++ openwisp_controller/connection/models.py | 60 +++++++++++++++++++ .../connection/tests/test_models.py | 60 +++++++++++++++++++ 5 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 openwisp_controller/connection/migrations/0002_credentials_auto_add.py diff --git a/openwisp_controller/connection/admin.py b/openwisp_controller/connection/admin.py index b8c29f34a..41fbd3254 100644 --- a/openwisp_controller/connection/admin.py +++ b/openwisp_controller/connection/admin.py @@ -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 @@ -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',) diff --git a/openwisp_controller/connection/apps.py b/openwisp_controller/connection/apps.py index 1a4e84c45..6dc7b9366 100644 --- a/openwisp_controller/connection/apps.py +++ b/openwisp_controller/connection/apps.py @@ -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 @@ -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 diff --git a/openwisp_controller/connection/migrations/0002_credentials_auto_add.py b/openwisp_controller/connection/migrations/0002_credentials_auto_add.py new file mode 100644 index 000000000..d7373f79e --- /dev/null +++ b/openwisp_controller/connection/migrations/0002_credentials_auto_add.py @@ -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'), + ), + ] diff --git a/openwisp_controller/connection/models.py b/openwisp_controller/connection/models.py index 872524592..9f48fa062 100644 --- a/openwisp_controller/connection/models.py +++ b/openwisp_controller/connection/models.py @@ -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__) @@ -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') @@ -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' diff --git a/openwisp_controller/connection/tests/test_models.py b/openwisp_controller/connection/tests/test_models.py index 63c760b1a..e10d137ba 100644 --- a/openwisp_controller/connection/tests/test_models.py +++ b/openwisp_controller/connection/tests/test_models.py @@ -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 @@ -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)