From ff7065fcd5698346a5a80abcc11dd2feccc752bf Mon Sep 17 00:00:00 2001 From: Pablo Castellano Date: Thu, 4 Jun 2020 23:06:14 +0200 Subject: [PATCH] [deps] Bump openwisp-utils and re-format code with black --- .travis.yml | 6 +- django_netjsonconfig/admin.py | 11 +- django_netjsonconfig/apps.py | 36 +- django_netjsonconfig/base/admin.py | 225 +++++----- django_netjsonconfig/base/base.py | 34 +- django_netjsonconfig/base/config.py | 131 +++--- django_netjsonconfig/base/device.py | 101 +++-- django_netjsonconfig/base/tag.py | 8 +- django_netjsonconfig/base/template.py | 80 ++-- django_netjsonconfig/base/vpn.py | 126 +++--- django_netjsonconfig/controller/generics.py | 112 ++--- django_netjsonconfig/controller/views.py | 12 +- .../migrations/0001_initial.py | 134 +++++- .../migrations/0002_config_status.py | 7 +- .../migrations/0003_config_last_ip.py | 6 +- .../migrations/0004_config_allow_blank.py | 9 +- .../migrations/0005_template_default.py | 7 +- .../migrations/0008_vpn_integration.py | 152 ++++++- .../0010_basemodel_reorganization.py | 16 +- .../migrations/0011_template_config_blank.py | 9 +- .../migrations/0013_config_mac_address.py | 12 +- .../migrations/0014_randomize_mac_address.py | 4 +- .../0015_config_mac_address_unique.py | 12 +- .../migrations/0016_vpn_dh.py | 4 +- .../migrations/0019_cleanup_model_options.py | 17 +- .../migrations/0020_openvpn_resolv_retry.py | 5 +- .../migrations/0021_netjsonconfig_label.py | 20 +- .../migrations/0022_update_model_labels.py | 21 +- .../migrations/0023_template_tags.py | 59 ++- .../migrations/0024_add_device_model.py | 94 ++++- .../migrations/0025_populate_device.py | 14 +- .../migrations/0026_config_device_not_null.py | 5 +- .../migrations/0027_simplify_config.py | 15 +- .../migrations/0028_device_indexes.py | 15 +- .../migrations/0029_explicit_indexes.py | 14 +- .../migrations/0030_device_system.py | 8 +- .../0031_updated_mac_address_validator.py | 14 +- .../migrations/0033_migrate_last_ip.py | 11 +- .../migrations/0034_device_management_ip.py | 6 +- .../migrations/0035_renamed_status_choices.py | 9 +- .../migrations/0037_config_context.py | 8 +- .../migrations/0038_vpn_key.py | 14 +- .../migrations/0040_update_context.py | 8 +- .../migrations/0042_device_key_none.py | 16 +- .../0043_add_indexes_on_ip_fields.py | 14 +- django_netjsonconfig/models.py | 7 + django_netjsonconfig/settings.py | 42 +- django_netjsonconfig/tests/__init__.py | 40 +- django_netjsonconfig/tests/test_admin.py | 206 +++++----- django_netjsonconfig/tests/test_config.py | 149 +++---- django_netjsonconfig/tests/test_controller.py | 388 +++++++++++------- django_netjsonconfig/tests/test_device.py | 98 +++-- django_netjsonconfig/tests/test_tag.py | 1 + django_netjsonconfig/tests/test_template.py | 104 ++--- django_netjsonconfig/tests/test_views.py | 7 +- django_netjsonconfig/tests/test_vpn.py | 133 +++--- django_netjsonconfig/utils.py | 83 ++-- django_netjsonconfig/validators.py | 10 +- django_netjsonconfig/vpn_backends.py | 49 +-- django_netjsonconfig/widgets.py | 27 +- requirements-test.txt | 2 +- requirements.txt | 2 +- run-qa-checks | 7 + runtests.py | 1 + setup.cfg | 18 +- setup.py | 9 +- tests/local_settings.example.py | 4 +- tests/settings.py | 6 +- tests/urls.py | 1 + 69 files changed, 1935 insertions(+), 1100 deletions(-) create mode 100755 run-qa-checks diff --git a/.travis.yml b/.travis.yml index 028bdb1..dd2cc94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,17 +19,13 @@ before_install: - pip install -U pip wheel setuptools - pip install --no-cache-dir -U -r requirements-test.txt - npm install -g jslint - - jslint django_netjsonconfig/static/django-netjsonconfig/js/*.js install: - pip install $DJANGO - python setup.py -q develop script: - - | - openwisp-utils-qa-checks \ - --migration-path ./django_netjsonconfig/migrations/ \ - --migration-module django_netjsonconfig + - ./run-qa-checks - coverage run --source=django_netjsonconfig runtests.py after_success: diff --git a/django_netjsonconfig/admin.py b/django_netjsonconfig/admin.py index 393b728..9bb6d6f 100644 --- a/django_netjsonconfig/admin.py +++ b/django_netjsonconfig/admin.py @@ -1,7 +1,14 @@ from django.contrib import admin -from .base.admin import (AbstractConfigForm, AbstractConfigInline, AbstractDeviceAdmin, AbstractTemplateAdmin, - AbstractVpnAdmin, AbstractVpnForm, BaseForm) +from .base.admin import ( + AbstractConfigForm, + AbstractConfigInline, + AbstractDeviceAdmin, + AbstractTemplateAdmin, + AbstractVpnAdmin, + AbstractVpnForm, + BaseForm, +) from .models import Config, Device, Template, Vpn diff --git a/django_netjsonconfig/apps.py b/django_netjsonconfig/apps.py index f5df91a..1baa5d6 100644 --- a/django_netjsonconfig/apps.py +++ b/django_netjsonconfig/apps.py @@ -17,6 +17,7 @@ def __setmodels__(self): This method allows third party apps to set their own custom models """ from .models import Config, VpnClient + self.config_model = Config self.vpnclient_model = VpnClient @@ -26,20 +27,31 @@ def connect_signals(self): * automatic vpn client management on m2m_changed * automatic vpn client removal """ - m2m_changed.connect(self.config_model.clean_templates, - sender=self.config_model.templates.through) - m2m_changed.connect(self.config_model.templates_changed, - sender=self.config_model.templates.through) - m2m_changed.connect(self.config_model.manage_vpn_clients, - sender=self.config_model.templates.through) - post_delete.connect(self.vpnclient_model.post_delete, - sender=self.vpnclient_model) + m2m_changed.connect( + self.config_model.clean_templates, + sender=self.config_model.templates.through, + ) + m2m_changed.connect( + self.config_model.templates_changed, + sender=self.config_model.templates.through, + ) + m2m_changed.connect( + self.config_model.manage_vpn_clients, + sender=self.config_model.templates.through, + ) + post_delete.connect( + self.vpnclient_model.post_delete, sender=self.vpnclient_model + ) def check_settings(self): - if settings.DEBUG is False and REGISTRATION_ENABLED and not SHARED_SECRET: # pragma: nocover - raise ImproperlyConfigured('Security error: NETJSONCONFIG_SHARED_SECRET is not set. ' - 'Please set it or disable auto-registration by setting ' - 'NETJSONCONFIG_REGISTRATION_ENABLED to False') + if ( + settings.DEBUG is False and REGISTRATION_ENABLED and not SHARED_SECRET + ): # pragma: nocover + raise ImproperlyConfigured( + 'Security error: NETJSONCONFIG_SHARED_SECRET is not set. ' + 'Please set it or disable auto-registration by setting ' + 'NETJSONCONFIG_REGISTRATION_ENABLED to False' + ) def ready(self): self.__setmodels__() diff --git a/django_netjsonconfig/base/admin.py b/django_netjsonconfig/base/admin.py index 813b72d..55586a6 100644 --- a/django_netjsonconfig/base/admin.py +++ b/django_netjsonconfig/base/admin.py @@ -5,7 +5,11 @@ from django.conf import settings from django.conf.urls import url from django.contrib import admin, messages -from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError +from django.core.exceptions import ( + FieldDoesNotExist, + ObjectDoesNotExist, + ValidationError, +) from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse @@ -40,14 +44,15 @@ class BaseConfigAdmin(BaseAdmin): class Media: css = {'all': (static('{0}css/admin.css'.format(prefix)),)} - js = list(UUIDAdmin.Media.js) + \ - [static('{0}js/{1}'.format(prefix, f)) - for f in ('preview.js', - 'unsaved_changes.js', - 'switcher.js')] + js = list(UUIDAdmin.Media.js) + [ + static('{0}js/{1}'.format(prefix, f)) + for f in ('preview.js', 'unsaved_changes.js', 'switcher.js') + ] def get_extra_context(self, pk=None): - prefix = 'admin:{0}_{1}'.format(self.opts.app_label, self.model.__name__.lower()) + prefix = 'admin:{0}_{1}'.format( + self.opts.app_label, self.model.__name__.lower() + ) text = _('Preview configuration') ctx = { 'additional_buttons': [ @@ -56,15 +61,17 @@ def get_extra_context(self, pk=None): 'url': reverse('{0}_preview'.format(prefix)), 'class': 'previewlink', 'value': text, - 'title': '{0} (ALT+P)'.format(text) + 'title': '{0} (ALT+P)'.format(text), } ] } if pk: ctx['download_url'] = reverse('{0}_download'.format(prefix), args=[pk]) try: - has_config = (self.model.__name__ == 'Device' and - self.model.objects.get(pk=pk)._has_config()) + has_config = ( + self.model.__name__ == 'Device' + and self.model.objects.get(pk=pk)._has_config() + ) except (ObjectDoesNotExist, ValidationError): raise Http404() else: @@ -90,15 +97,21 @@ def get_urls(self): options = getattr(self.model, '_meta') url_prefix = '{0}_{1}'.format(options.app_label, options.model_name) return [ - url(r'^download/(?P[^/]+)/$', + url( + r'^download/(?P[^/]+)/$', self.admin_site.admin_view(self.download_view), - name='{0}_download'.format(url_prefix)), - url(r'^preview/$', + name='{0}_download'.format(url_prefix), + ), + url( + r'^preview/$', self.admin_site.admin_view(self.preview_view), - name='{0}_preview'.format(url_prefix)), - url(r'^(?P[^/]+)/context\.json$', + name='{0}_preview'.format(url_prefix), + ), + url( + r'^(?P[^/]+)/context\.json$', self.admin_site.admin_view(self.context_view), - name='{0}_context'.format(url_prefix)), + name='{0}_context'.format(url_prefix), + ), url(r'^netjsonconfig/schema\.json$', schema, name='schema'), ] + super().get_urls() @@ -141,8 +154,7 @@ def _get_preview_instance(self, request): # this object is instanciated only to generate the preview # it won't be saved to the database instance = config_model(**kwargs) - instance.full_clean(exclude=['device'], - validate_unique=False) + instance.full_clean(exclude=['device'], validate_unique=False) return instance preview_error_msg = _('Preview for {0} with name {1} failed') @@ -156,7 +168,9 @@ def preview_view(self, request): error = None output = None # error message for eventual exceptions - error_msg = self.preview_error_msg.format(config_model.__name__, request.POST.get('name')) + error_msg = self.preview_error_msg.format( + config_model.__name__, request.POST.get('name') + ) try: instance = self._get_preview_instance(request) except Exception as e: @@ -168,7 +182,9 @@ def preview_view(self, request): if template_ids: template_model = config_model.get_template_model() try: - templates = template_model.objects.filter(pk__in=template_ids.split(',')) + templates = template_model.objects.filter( + pk__in=template_ids.split(',') + ) templates = list(templates) # evaluating queryset performs query except ValidationError as e: logger.exception(error_msg, extra={'request': request}) @@ -184,18 +200,25 @@ def preview_view(self, request): error = str(e) context = self.admin_site.each_context(request) opts = self.model._meta - context.update({ - 'is_popup': True, - 'opts': opts, - 'change': False, - 'output': output, - 'media': self.media, - 'error': error, - }) - return TemplateResponse(request, self.preview_template or [ - 'admin/%s/%s/preview.html' % (opts.app_label, opts.model_name), - 'admin/%s/preview.html' % opts.app_label - ], context) + context.update( + { + 'is_popup': True, + 'opts': opts, + 'change': False, + 'output': output, + 'media': self.media, + 'error': error, + } + ) + return TemplateResponse( + request, + self.preview_template + or [ + 'admin/%s/%s/preview.html' % (opts.app_label, opts.model_name), + 'admin/%s/preview.html' % opts.app_label, + ], + context, + ) def download_view(self, request, pk): instance = get_object_or_404(self.model, pk=pk) @@ -206,8 +229,10 @@ def download_view(self, request, pk): else: raise Http404() config_archive = config.generate() - return send_file(filename='{0}.tar.gz'.format(config.name), - contents=config_archive.getvalue()) + return send_file( + filename='{0}.tar.gz'.format(config.name), + contents=config_archive.getvalue(), + ) def context_view(self, request, pk): instance = get_object_or_404(self.model, pk=pk) @@ -219,7 +244,9 @@ class BaseForm(forms.ModelForm): """ Adds support for ``NETJSONCONFIG_DEFAULT_BACKEND`` """ + if app_settings.DEFAULT_BACKEND: + def __init__(self, *args, **kwargs): # set initial backend value to use the default # backend but only for new instances @@ -249,12 +276,14 @@ def clean_templates(self): else: config = self.instance if config.backend and templates: - config_model.clean_templates(action='pre_add', - instance=config, - sender=config.templates, - reverse=False, - model=config.templates.model, - pk_set=templates) + config_model.clean_templates( + action='pre_add', + instance=config, + sender=config.templates, + reverse=False, + model=config.templates.model, + pk_set=templates, + ) return templates @@ -262,16 +291,9 @@ class AbstractConfigInline(TimeReadonlyAdminMixin, admin.StackedInline): verbose_name_plural = _('Device configuration details') readonly_fields = ['status'] fieldsets = ( - (None, { - 'fields': ('backend', 'status', 'templates', 'config') - }), - (_('Advanced options'), { - 'classes': ('collapse',), - 'fields': ('context',), - }), - (None, { - 'fields': ('created', 'modified') - }), + (None, {'fields': ('backend', 'status', 'templates', 'config')}), + (_('Advanced options'), {'classes': ('collapse',), 'fields': ('context',)}), + (None, {'fields': ('created', 'modified')}), ) change_select_related = ('device',) @@ -281,27 +303,25 @@ def get_queryset(self, request): class AbstractDeviceAdmin(BaseConfigAdmin, UUIDAdmin): - list_display = ['name', 'backend', 'config_status', - 'ip', 'created', 'modified'] + list_display = ['name', 'backend', 'config_status', 'ip', 'created', 'modified'] search_fields = ['id', 'name', 'mac_address', 'key', 'model', 'os', 'system'] - list_filter = ['config__backend', - 'config__templates', - 'config__status', - 'created'] + list_filter = ['config__backend', 'config__templates', 'config__status', 'created'] list_select_related = ('config',) readonly_fields = ['last_ip', 'management_ip', 'uuid'] - fields = ['name', - 'mac_address', - 'uuid', - 'key', - 'last_ip', - 'management_ip', - 'model', - 'os', - 'system', - 'notes', - 'created', - 'modified'] + fields = [ + 'name', + 'mac_address', + 'uuid', + 'key', + 'last_ip', + 'management_ip', + 'model', + 'os', + 'system', + 'notes', + 'created', + 'modified', + ] if app_settings.HARDWARE_ID_ENABLED: list_display.insert(1, 'hardware_id') search_fields.insert(1, 'hardware_id') @@ -323,10 +343,12 @@ def config_status(self, obj): def _get_preview_instance(self, request): c = super()._get_preview_instance(request) - c.device = self.model(id=request.POST.get('id'), - name=request.POST.get('name'), - mac_address=request.POST.get('mac_address'), - key=request.POST.get('key')) + c.device = self.model( + id=request.POST.get('id'), + name=request.POST.get('name'), + mac_address=request.POST.get('mac_address'), + key=request.POST.get('key'), + ) if 'hardware_id' in request.POST: c.device.hardware_id = request.POST.get('hardware_id') return c @@ -341,21 +363,25 @@ class AbstractTemplateAdmin(BaseConfigAdmin): list_display = ['name', 'type', 'backend', 'default', 'created', 'modified'] list_filter = ['backend', 'type', 'default', 'created'] search_fields = ['name'] - fields = ['name', - 'type', - 'backend', - 'vpn', - 'auto_cert', - 'tags', - 'default', - 'config', - 'created', - 'modified'] + fields = [ + 'name', + 'type', + 'backend', + 'vpn', + 'auto_cert', + 'tags', + 'default', + 'config', + 'created', + 'modified', + ] def clone_selected_templates(self, request, queryset): for templates in queryset: templates.clone(request.user) - self.message_user(request, _('Successfully cloned selected templates.'), messages.SUCCESS) + self.message_user( + request, _('Successfully cloned selected templates.'), messages.SUCCESS + ) actions = ['clone_selected_templates'] @@ -364,17 +390,16 @@ class AbstractVpnForm(forms.ModelForm): """ Adds support for ``NETJSONCONFIG_DEFAULT_BACKEND`` """ + if app_settings.DEFAULT_VPN_BACKEND: + def __init__(self, *args, **kwargs): if 'initial' in kwargs: kwargs['initial'].update({'backend': app_settings.DEFAULT_VPN_BACKEND}) super().__init__(*args, **kwargs) class Meta: - widgets = { - 'config': JsonSchemaWidget, - 'dh': forms.widgets.HiddenInput - } + widgets = {'config': JsonSchemaWidget, 'dh': forms.widgets.HiddenInput} exclude = [] @@ -383,15 +408,17 @@ class AbstractVpnAdmin(BaseConfigAdmin, UUIDAdmin): list_filter = ['backend', 'ca', 'created'] search_fields = ['id', 'name', 'host', 'key'] readonly_fields = ['id', 'uuid'] - fields = ['name', - 'host', - 'uuid', - 'key', - 'ca', - 'cert', - 'backend', - 'notes', - 'dh', - 'config', - 'created', - 'modified'] + fields = [ + 'name', + 'host', + 'uuid', + 'key', + 'ca', + 'cert', + 'backend', + 'notes', + 'dh', + 'config', + 'created', + 'modified', + ] diff --git a/django_netjsonconfig/base/base.py b/django_netjsonconfig/base/base.py index 9bd311b..6dc24aa 100644 --- a/django_netjsonconfig/base/base.py +++ b/django_netjsonconfig/base/base.py @@ -20,6 +20,7 @@ class BaseModel(TimeStampedEditableModel): """ Shared logic """ + name = models.CharField(max_length=64, unique=True, db_index=True) class Meta: @@ -33,16 +34,23 @@ class BaseConfig(BaseModel): """ Base configuration management model logic shared between models """ - backend = models.CharField(_('backend'), - choices=app_settings.BACKENDS, - max_length=128, - help_text=_('Select netjsonconfig backend')) - config = JSONField(_('configuration'), - default=dict, - help_text=_('configuration in NetJSON DeviceConfiguration format'), - load_kwargs={'object_pairs_hook': collections.OrderedDict}, - dump_kwargs={'indent': 4}) + + backend = models.CharField( + _('backend'), + choices=app_settings.BACKENDS, + max_length=128, + help_text=_( + 'Select netjsonconfig backend' + ), + ) + config = JSONField( + _('configuration'), + default=dict, + help_text=_('configuration in NetJSON DeviceConfiguration format'), + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + dump_kwargs={'indent': 4}, + ) __template__ = False __vpn__ = False @@ -113,8 +121,10 @@ def clean_netjsonconfig_backend(cls, backend): path = [str(el) for el in e.details.path] trigger = '/'.join(path) error = e.details.message - message = 'Invalid configuration triggered by "#/{0}", '\ - 'validator says:\n\n{1}'.format(trigger, error) + message = ( + 'Invalid configuration triggered by "#/{0}", ' + 'validator says:\n\n{1}'.format(trigger, error) + ) raise ValidationError(message) @cached_property diff --git a/django_netjsonconfig/base/config.py b/django_netjsonconfig/base/config.py index f3d7e5b..bb56a89 100644 --- a/django_netjsonconfig/base/config.py +++ b/django_netjsonconfig/base/config.py @@ -18,21 +18,31 @@ class AbstractConfig(BaseConfig): Abstract model implementing the NetJSON DeviceConfiguration object """ - device = models.OneToOneField('django_netjsonconfig.Device', on_delete=models.CASCADE) + + device = models.OneToOneField( + 'django_netjsonconfig.Device', on_delete=models.CASCADE + ) STATUS = Choices('modified', 'applied', 'error') - status = StatusField(_('configuration status'), help_text=_( - '"modified" means the configuration is not applied yet; \n' - '"applied" means the configuration is applied successfully; \n' - '"error" means the configuration caused issues and it was rolled back;' - )) - context = JSONField(blank=True, - default=dict, - help_text=_('Additional ' - '' - 'context (configuration variables) in JSON format'), - load_kwargs={'object_pairs_hook': collections.OrderedDict}, - dump_kwargs={'indent': 4}) + status = StatusField( + _('configuration status'), + help_text=_( + '"modified" means the configuration is not applied yet; \n' + '"applied" means the configuration is applied successfully; \n' + '"error" means the configuration caused issues and it was rolled back;' + ), + ) + context = JSONField( + blank=True, + default=dict, + help_text=_( + 'Additional ' + '' + 'context (configuration variables) in JSON format' + ), + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + dump_kwargs={'indent': 4}, + ) class Meta: abstract = True @@ -65,7 +75,9 @@ def clean(self): def save(self, *args, **kwargs): result = super().save(*args, **kwargs) - if not self._state.adding and getattr(self, '_send_config_modified_after_save', False): + if not self._state.adding and getattr( + self, '_send_config_modified_after_save', False + ): self._send_config_modified_signal() self._send_config_modified_after_save = None if getattr(self, '_send_config_status_changed', False): @@ -78,19 +90,20 @@ def _send_config_modified_signal(self): Emits ``config_modified`` signal. Called also by Template when templates of a device are modified """ - config_modified.send(sender=self.__class__, - instance=self, - # kept for backward compatibility - config=self, - device=self.device) + config_modified.send( + sender=self.__class__, + instance=self, + # kept for backward compatibility + config=self, + device=self.device, + ) def _send_config_status_changed_signal(self): """ Emits ``config_status_changed`` signal. Called also by Template when templates of a device are modified """ - config_status_changed.send(sender=self.__class__, - instance=self) + config_status_changed.send(sender=self.__class__, instance=self) def _set_status(self, status, save=True): self.status = status @@ -117,12 +130,14 @@ def get_context(self): """ c = {} if self._has_device(): - c.update({ - 'id': str(self.device.id), - 'key': self.key, - 'name': self.name, - 'mac_address': self.mac_address - }) + c.update( + { + 'id': str(self.device.id), + 'key': self.key, + 'name': self.name, + 'mac_address': self.mac_address, + } + ) if self.context: c.update(self.context) c.update(super().get_context()) @@ -175,17 +190,21 @@ class TemplatesVpnMixin(models.Model): * Template * Vpn """ - templates = SortedManyToManyField('django_netjsonconfig.Template', - related_name='config_relations', - verbose_name=_('templates'), - base_class=TemplatesThrough, - blank=True, - help_text=_('configuration templates, applied from ' - 'first to last')) - vpn = models.ManyToManyField('django_netjsonconfig.Vpn', - through='django_netjsonconfig.VpnClient', - related_name='vpn_relations', - blank=True) + + templates = SortedManyToManyField( + 'django_netjsonconfig.Template', + related_name='config_relations', + verbose_name=_('templates'), + base_class=TemplatesThrough, + blank=True, + help_text=_('configuration templates, applied from ' 'first to last'), + ) + vpn = models.ManyToManyField( + 'django_netjsonconfig.Vpn', + through='django_netjsonconfig.VpnClient', + related_name='vpn_relations', + blank=True, + ) def save(self, *args, **kwargs): created = self._state.adding @@ -287,9 +306,9 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs): # when adding or removing specific templates for template in templates.filter(type='vpn'): if action == 'post_add': - client = vpn_client_model(config=instance, - vpn=template.vpn, - auto_cert=template.auto_cert) + client = vpn_client_model( + config=instance, vpn=template.vpn, auto_cert=template.auto_cert + ) client.full_clean() client.save() elif action == 'post_remove': @@ -308,13 +327,17 @@ def get_context(self): ca = vpn.ca cert = vpnclient.cert # CA - ca_filename = 'ca-{0}-{1}.pem'.format(ca.pk, ca.common_name.replace(' ', '_')) + ca_filename = 'ca-{0}-{1}.pem'.format( + ca.pk, ca.common_name.replace(' ', '_') + ) ca_path = '{0}/{1}'.format(app_settings.CERT_PATH, ca_filename) # update context - c.update({ - context_keys['ca_path']: ca_path, - context_keys['ca_contents']: ca.certificate - }) + c.update( + { + context_keys['ca_path']: ca_path, + context_keys['ca_contents']: ca.certificate, + } + ) # conditional needed for VPN without x509 authentication # eg: simple password authentication if cert: @@ -325,12 +348,14 @@ def get_context(self): key_filename = 'key-{0}.pem'.format(vpn_id) key_path = '{0}/{1}'.format(app_settings.CERT_PATH, key_filename) # update context - c.update({ - context_keys['cert_path']: cert_path, - context_keys['cert_contents']: cert.certificate, - context_keys['key_path']: key_path, - context_keys['key_contents']: cert.private_key, - }) + c.update( + { + context_keys['cert_path']: cert_path, + context_keys['cert_contents']: cert.certificate, + context_keys['key_path']: key_path, + context_keys['key_contents']: cert.private_key, + } + ) return c class Meta: diff --git a/django_netjsonconfig/base/device.py b/django_netjsonconfig/base/device.py index b2b89ec..08d9f19 100644 --- a/django_netjsonconfig/base/device.py +++ b/django_netjsonconfig/base/device.py @@ -16,51 +16,70 @@ class AbstractDevice(BaseModel): Stores information related to the physical properties of a network device """ - mac_address = models.CharField(max_length=17, - unique=True, - db_index=True, - validators=[mac_address_validator], - help_text=_('primary mac address')) - key = KeyField(unique=True, - blank=True, - default=None, - db_index=True, - help_text=_('unique device key')) - model = models.CharField(max_length=64, - blank=True, - db_index=True, - help_text=_('device model and manufacturer')) - os = models.CharField(_('operating system'), - blank=True, - db_index=True, - max_length=128, - help_text=_('operating system identifier')) - system = models.CharField(_('SOC / CPU'), - blank=True, - db_index=True, - max_length=128, - help_text=_('system on chip or CPU info')) + + mac_address = models.CharField( + max_length=17, + unique=True, + db_index=True, + validators=[mac_address_validator], + help_text=_('primary mac address'), + ) + key = KeyField( + unique=True, + blank=True, + default=None, + db_index=True, + help_text=_('unique device key'), + ) + model = models.CharField( + max_length=64, + blank=True, + db_index=True, + help_text=_('device model and manufacturer'), + ) + os = models.CharField( + _('operating system'), + blank=True, + db_index=True, + max_length=128, + help_text=_('operating system identifier'), + ) + system = models.CharField( + _('SOC / CPU'), + blank=True, + db_index=True, + max_length=128, + help_text=_('system on chip or CPU info'), + ) notes = models.TextField(blank=True, help_text=_('internal notes')) # these fields are filled automatically # with data received from devices - last_ip = models.GenericIPAddressField(blank=True, - null=True, - db_index=True, - help_text=_('indicates the IP address logged from ' - 'the last request coming from the device')) - management_ip = models.GenericIPAddressField(blank=True, - null=True, - db_index=True, - help_text=_('ip address of the management interface, ' - 'if available')) + last_ip = models.GenericIPAddressField( + blank=True, + null=True, + db_index=True, + help_text=_( + 'indicates the IP address logged from ' + 'the last request coming from the device' + ), + ) + management_ip = models.GenericIPAddressField( + blank=True, + null=True, + db_index=True, + help_text=_('ip address of the management interface, ' 'if available'), + ) hardware_id = models.CharField(**(app_settings.HARDWARE_ID_OPTIONS)) class Meta: abstract = True def __str__(self): - return self.hardware_id if (app_settings.HARDWARE_ID_ENABLED - and app_settings.HARDWARE_ID_AS_NAME) else self.name + return ( + self.hardware_id + if (app_settings.HARDWARE_ID_ENABLED and app_settings.HARDWARE_ID_AS_NAME) + else self.name + ) def clean(self): """ @@ -96,7 +115,11 @@ def get_context(self): def generate_key(self, shared_secret): if app_settings.CONSISTENT_REGISTRATION: - keybase = self.hardware_id if app_settings.HARDWARE_ID_ENABLED else self.mac_address + keybase = ( + self.hardware_id + if app_settings.HARDWARE_ID_ENABLED + else self.mac_address + ) hash = md5('{}+{}'.format(keybase, shared_secret).encode('utf-8')) return hash.hexdigest() else: @@ -146,6 +169,4 @@ def get_temp_config_instance(self, **options): # (to avoid modifying parent classes) # and add device_name_validator name_field = AbstractDevice._meta.get_field('name') -name_field.validators = name_field.validators[:] + [ - device_name_validator -] +name_field.validators = name_field.validators[:] + [device_name_validator] diff --git a/django_netjsonconfig/base/tag.py b/django_netjsonconfig/base/tag.py index 24c2088..420fa39 100644 --- a/django_netjsonconfig/base/tag.py +++ b/django_netjsonconfig/base/tag.py @@ -13,9 +13,11 @@ class Meta: class AbstractTaggedTemplate(GenericUUIDTaggedItemBase, TaggedItemBase): - tag = models.ForeignKey('django_netjsonconfig.TemplateTag', - related_name='%(app_label)s_%(class)s_items', - on_delete=models.CASCADE) + tag = models.ForeignKey( + 'django_netjsonconfig.TemplateTag', + related_name='%(app_label)s_%(class)s_items', + on_delete=models.CASCADE, + ) class Meta: abstract = True diff --git a/django_netjsonconfig/base/template.py b/django_netjsonconfig/base/template.py index 957e876..491e1b9 100644 --- a/django_netjsonconfig/base/template.py +++ b/django_netjsonconfig/base/template.py @@ -29,34 +29,50 @@ class AbstractTemplate(BaseConfig): Abstract model implementing a netjsonconfig template """ - tags = TaggableManager(through='django_netjsonconfig.TaggedTemplate', blank=True, - help_text=_('A comma-separated list of template tags, may be used ' - 'to ease auto configuration with specific settings (eg: ' - '4G, mesh, WDS, VPN, ecc.)')) - vpn = models.ForeignKey('django_netjsonconfig.Vpn', - verbose_name=_('VPN'), - blank=True, - null=True, - on_delete=models.CASCADE) - type = models.CharField(_('type'), - max_length=16, - choices=TYPE_CHOICES, - default='generic', - db_index=True, - help_text=_('template type, determines which ' - 'features are available')) - default = models.BooleanField(_('enabled by default'), - default=False, - db_index=True, - help_text=_('whether new configurations will have ' - 'this template enabled by default')) - auto_cert = models.BooleanField(_('auto certificate'), - default=default_auto_cert, - db_index=True, - help_text=_('whether x509 client certificates should ' - 'be automatically managed behind the scenes ' - 'for each configuration using this template, ' - 'valid only for the VPN type')) + + tags = TaggableManager( + through='django_netjsonconfig.TaggedTemplate', + blank=True, + help_text=_( + 'A comma-separated list of template tags, may be used ' + 'to ease auto configuration with specific settings (eg: ' + '4G, mesh, WDS, VPN, ecc.)' + ), + ) + vpn = models.ForeignKey( + 'django_netjsonconfig.Vpn', + verbose_name=_('VPN'), + blank=True, + null=True, + on_delete=models.CASCADE, + ) + type = models.CharField( + _('type'), + max_length=16, + choices=TYPE_CHOICES, + default='generic', + db_index=True, + help_text=_('template type, determines which ' 'features are available'), + ) + default = models.BooleanField( + _('enabled by default'), + default=False, + db_index=True, + help_text=_( + 'whether new configurations will have ' 'this template enabled by default' + ), + ) + auto_cert = models.BooleanField( + _('auto certificate'), + default=default_auto_cert, + db_index=True, + help_text=_( + 'whether x509 client certificates should ' + 'be automatically managed behind the scenes ' + 'for each configuration using this template, ' + 'valid only for the VPN type' + ), + ) __template__ = True class Meta: @@ -100,9 +116,9 @@ def clean(self, *args, **kwargs): """ super().clean(*args, **kwargs) if self.type == 'vpn' and not self.vpn: - raise ValidationError({ - 'vpn': _('A VPN must be selected when template type is "VPN"') - }) + raise ValidationError( + {'vpn': _('A VPN must be selected when template type is "VPN"')} + ) elif self.type != 'vpn': self.vpn = None self.auto_cert = False @@ -134,7 +150,7 @@ def clone(self, user): content_type_id=ct.pk, object_id=clone.pk, object_repr=clone.name, - action_flag=ADDITION + action_flag=ADDITION, ) return clone diff --git a/django_netjsonconfig/base/vpn.py b/django_netjsonconfig/base/vpn.py index 4eb93d1..a156bbb 100644 --- a/django_netjsonconfig/base/vpn.py +++ b/django_netjsonconfig/base/vpn.py @@ -15,19 +15,28 @@ class AbstractVpn(BaseConfig): """ Abstract VPN model """ - host = models.CharField(max_length=64, help_text=_('VPN server hostname or ip address')) - ca = models.ForeignKey('django_x509.Ca', verbose_name=_('CA'), on_delete=models.CASCADE) + + host = models.CharField( + max_length=64, help_text=_('VPN server hostname or ip address') + ) + ca = models.ForeignKey( + 'django_x509.Ca', verbose_name=_('CA'), on_delete=models.CASCADE + ) key = KeyField(db_index=True) - cert = models.ForeignKey('django_x509.Cert', - verbose_name=_('x509 Certificate'), - help_text=_('leave blank to create automatically'), - blank=True, - null=True, - on_delete=models.CASCADE) - backend = models.CharField(_('VPN backend'), - choices=app_settings.VPN_BACKENDS, - max_length=128, - help_text=_('Select VPN configuration backend')) + cert = models.ForeignKey( + 'django_x509.Cert', + verbose_name=_('x509 Certificate'), + help_text=_('leave blank to create automatically'), + blank=True, + null=True, + on_delete=models.CASCADE, + ) + backend = models.CharField( + _('VPN backend'), + choices=app_settings.VPN_BACKENDS, + max_length=128, + help_text=_('Select VPN configuration backend'), + ) notes = models.TextField(blank=True) # diffie hellman parameters are required # in some VPN solutions (eg: OpenVPN) @@ -65,8 +74,9 @@ def dhparam(cls, length): """ Returns an automatically generated set of DH parameters in PEM """ - return subprocess.check_output('openssl dhparam {0} 2> /dev/null'.format(length), - shell=True).decode('utf-8') + return subprocess.check_output( + 'openssl dhparam {0} 2> /dev/null'.format(length), shell=True + ).decode('utf-8') def _auto_create_cert(self): """ @@ -74,24 +84,22 @@ def _auto_create_cert(self): """ common_name = slugify(self.name) server_extensions = [ - { - "name": "nsCertType", - "value": "server", - "critical": False - } + {"name": "nsCertType", "value": "server", "critical": False} ] cert_model = self.__class__.cert.field.related_model - cert = cert_model(name=self.name, - ca=self.ca, - key_length=self.ca.key_length, - digest=self.ca.digest, - country_code=self.ca.country_code, - state=self.ca.state, - city=self.ca.city, - organization_name=self.ca.organization_name, - email=self.ca.email, - common_name=common_name, - extensions=server_extensions) + cert = cert_model( + name=self.name, + ca=self.ca, + key_length=self.ca.key_length, + digest=self.ca.digest, + country_code=self.ca.country_code, + state=self.ca.state, + city=self.ca.city, + organization_name=self.ca.organization_name, + email=self.ca.email, + common_name=common_name, + extensions=server_extensions, + ) cert = self._auto_create_cert_extra(cert) cert.save() return cert @@ -113,10 +121,7 @@ def get_context(self): except ObjectDoesNotExist: c = {} if self.cert: - c.update({ - 'cert': self.cert.certificate, - 'key': self.cert.private_key - }) + c.update({'cert': self.cert.certificate, 'key': self.cert.private_key}) if self.dh: c.update({'dh': self.dh}) c.update(super().get_context()) @@ -162,9 +167,9 @@ def auto_client(self, auto_cert=True): for key in ['cert_path', 'cert_contents', 'key_path', 'key_contents']: del context_keys[key] conifg_dict_key = self.backend_class.__name__.lower() - auto = backend.auto_client(host=self.host, - server=self.config[conifg_dict_key][0], - **context_keys) + auto = backend.auto_client( + host=self.host, server=self.config[conifg_dict_key][0], **context_keys + ) config.update(auto) return config @@ -173,14 +178,12 @@ class AbstractVpnClient(models.Model): """ m2m through model """ - config = models.ForeignKey('django_netjsonconfig.Config', - on_delete=models.CASCADE) - vpn = models.ForeignKey('django_netjsonconfig.Vpn', - on_delete=models.CASCADE) - cert = models.OneToOneField('django_x509.Cert', - on_delete=models.CASCADE, - blank=True, - null=True) + + config = models.ForeignKey('django_netjsonconfig.Config', on_delete=models.CASCADE) + vpn = models.ForeignKey('django_netjsonconfig.Vpn', on_delete=models.CASCADE) + cert = models.OneToOneField( + 'django_x509.Cert', on_delete=models.CASCADE, blank=True, null=True + ) # this flags indicates whether the certificate must be # automatically managed, which is going to be almost in all cases auto_cert = models.BooleanField(default=False) @@ -197,8 +200,7 @@ def save(self, *args, **kwargs): """ if self.auto_cert: cn = self._get_common_name() - self._auto_create_cert(name=self.config.device.name, - common_name=cn) + self._auto_create_cert(name=self.config.device.name, common_name=cn) super().save(*args, **kwargs) def _get_common_name(self): @@ -226,25 +228,23 @@ def _auto_create_cert(self, name, common_name): Automatically creates and assigns a client x509 certificate """ server_extensions = [ - { - "name": "nsCertType", - "value": "client", - "critical": False - } + {"name": "nsCertType", "value": "client", "critical": False} ] ca = self.vpn.ca cert_model = self.__class__.cert.field.related_model - cert = cert_model(name=name, - ca=ca, - key_length=ca.key_length, - digest=str(ca.digest), - country_code=ca.country_code, - state=ca.state, - city=ca.city, - organization_name=ca.organization_name, - email=ca.email, - common_name=common_name, - extensions=server_extensions) + cert = cert_model( + name=name, + ca=ca, + key_length=ca.key_length, + digest=str(ca.digest), + country_code=ca.country_code, + state=ca.state, + city=ca.city, + organization_name=ca.organization_name, + email=ca.email, + common_name=common_name, + extensions=server_extensions, + ) cert = self._auto_create_cert_extra(cert) cert.full_clean() cert.save() diff --git a/django_netjsonconfig/controller/generics.py b/django_netjsonconfig/controller/generics.py index f5b16b5..4f15877 100644 --- a/django_netjsonconfig/controller/generics.py +++ b/django_netjsonconfig/controller/generics.py @@ -9,8 +9,14 @@ from .. import settings from ..signals import checksum_requested, config_download_requested -from ..utils import (ControllerResponse, forbid_unallowed, get_object_or_404, send_device_config, - send_vpn_config, update_last_ip) +from ..utils import ( + ControllerResponse, + forbid_unallowed, + get_object_or_404, + send_device_config, + send_vpn_config, + update_last_ip, +) class BaseConfigView(SingleObjectMixin, View): @@ -18,6 +24,7 @@ class BaseConfigView(SingleObjectMixin, View): Base view that implements a ``get_object`` method Subclassed by all views dealing with existing objects """ + def get_object(self, *args, **kwargs): kwargs['config__isnull'] = False return get_object_or_404(self.model, *args, **kwargs) @@ -27,6 +34,7 @@ class CsrfExtemptMixin(object): """ Mixin that makes the view extempt from CSFR protection """ + @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) @@ -41,6 +49,7 @@ class BaseDeviceChecksumView(UpdateLastIpMixin, BaseConfigView): """ returns device's configuration checksum """ + def get(self, request, *args, **kwargs): device = self.get_object(*args, **kwargs) bad_request = forbid_unallowed(request, 'GET', 'key', device.key) @@ -48,9 +57,7 @@ def get(self, request, *args, **kwargs): return bad_request self.update_last_ip(device, request) checksum_requested.send( - sender=device.__class__, - instance=device, - request=request + sender=device.__class__, instance=device, request=request ) return ControllerResponse(device.config.checksum, content_type='text/plain') @@ -59,16 +66,13 @@ class BaseVpnChecksumView(BaseConfigView): """ returns vpn's configuration checksum """ + def get(self, request, *args, **kwargs): vpn = self.get_object(*args, **kwargs) bad_request = forbid_unallowed(request, 'GET', 'key', vpn.key) if bad_request: return bad_request - checksum_requested.send( - sender=vpn.__class__, - instance=vpn, - request=request - ) + checksum_requested.send(sender=vpn.__class__, instance=vpn, request=request) return ControllerResponse(vpn.checksum, content_type='text/plain') @@ -76,15 +80,14 @@ class BaseDeviceDownloadConfigView(BaseConfigView): """ returns configuration archive as attachment """ + def get(self, request, *args, **kwargs): device = self.get_object(*args, **kwargs) bad_request = forbid_unallowed(request, 'GET', 'key', device.key) if bad_request: return bad_request config_download_requested.send( - sender=device.__class__, - instance=device, - request=request + sender=device.__class__, instance=device, request=request ) return send_device_config(device.config, request) @@ -93,15 +96,14 @@ class BaseVpnDownloadConfigView(BaseConfigView): """ returns configuration archive as attachment """ + def get(self, request, *args, **kwargs): vpn = self.get_object(*args, **kwargs) bad_request = forbid_unallowed(request, 'GET', 'key', vpn.key) if bad_request: return bad_request config_download_requested.send( - sender=vpn.__class__, - instance=vpn, - request=request + sender=vpn.__class__, instance=vpn, request=request ) return send_vpn_config(vpn, request) @@ -110,6 +112,7 @@ class BaseDeviceUpdateInfoView(CsrfExtemptMixin, BaseConfigView): """ updates general information about the device """ + UPDATABLE_FIELDS = ['os', 'model', 'system'] def post(self, request, *args, **kwargs): @@ -129,25 +132,26 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # dump message_dict as JSON, # this should make it easy to debug - return ControllerResponse(json.dumps(e.message_dict, indent=4, sort_keys=True), - content_type='text/plain', - status=400) - return ControllerResponse('update-info: success', - content_type='text/plain') + return ControllerResponse( + json.dumps(e.message_dict, indent=4, sort_keys=True), + content_type='text/plain', + status=400, + ) + return ControllerResponse('update-info: success', content_type='text/plain') class BaseDeviceReportStatusView(CsrfExtemptMixin, BaseConfigView): """ updates status of config objects """ + def post(self, request, *args, **kwargs): device = self.get_object(*args, **kwargs) config = device.config # ensure request is well formed and authorized allowed_status = [choices[0] for choices in config.STATUS] allowed_status.append('running') # backward compatibility - required_params = [('key', device.key), - ('status', allowed_status)] + required_params = [('key', device.key), ('status', allowed_status)] for key, value in required_params: bad_response = forbid_unallowed(request, 'POST', key, value) if bad_response: @@ -159,15 +163,17 @@ def post(self, request, *args, **kwargs): # call set_status_{status} method on Config model method_name = 'set_status_{}'.format(status) getattr(config, method_name)() - return ControllerResponse('report-result: success\n' - 'current-status: {}\n'.format(config.status), - content_type='text/plain') + return ControllerResponse( + 'report-result: success\n' 'current-status: {}\n'.format(config.status), + content_type='text/plain', + ) class BaseDeviceRegisterView(UpdateLastIpMixin, CsrfExtemptMixin, View): """ registers new Config objects """ + UPDATABLE_FIELDS = ['os', 'model', 'system'] def init_object(self, **kwargs): @@ -187,13 +193,13 @@ def init_object(self, **kwargs): # do not specify key if: # settings.CONSISTENT_REGISTRATION is False # if key is ``None`` (it would cause exception) - if 'key' in options and (settings.CONSISTENT_REGISTRATION is False - or options['key'] is None): + if 'key' in options and ( + settings.CONSISTENT_REGISTRATION is False or options['key'] is None + ): del options['key'] if 'hardware_id' in options and options['hardware_id'] == "": options['hardware_id'] = None - return config_model(device=device_model(**options), - backend=kwargs['backend']) + return config_model(device=device_model(**options), backend=kwargs['backend']) def get_template_queryset(self, config): """ @@ -211,9 +217,7 @@ def add_tagged_templates(self, config, request): # retrieve tags and add them to current config tags = tags.split() queryset = self.get_template_queryset(config) - templates = queryset.filter(tags__name__in=tags) \ - .only('id') \ - .distinct() + templates = queryset.filter(tags__name__in=tags).only('id').distinct() for template in templates: config.templates.add(template) @@ -222,10 +226,12 @@ def invalid(self, request): ensures request is well formed """ allowed_backends = [path for path, name in settings.BACKENDS] - required_params = [('secret', None), - ('name', None), - ('mac_address', None), - ('backend', allowed_backends)] + required_params = [ + ('secret', None), + ('name', None), + ('mac_address', None), + ('backend', allowed_backends), + ] # valid required params or forbid for key, value in required_params: invalid_response = forbid_unallowed(request, 'POST', key, value) @@ -272,7 +278,7 @@ def post(self, request, *args, **kwargs): if not settings.REGISTRATION_SELF_CREATION: return ControllerResponse( 'Device not found in the system, please create it first.', - status=404 + status=404, ) new = True config = self.init_object(**request.POST.dict()) @@ -294,23 +300,23 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # dump message_dict as JSON, # this should make it easy to debug - return ControllerResponse(json.dumps(e.message_dict, indent=4, sort_keys=True), - content_type='text/plain', - status=400) + return ControllerResponse( + json.dumps(e.message_dict, indent=4, sort_keys=True), + content_type='text/plain', + status=400, + ) # add templates specified in tags self.add_tagged_templates(config, request) # prepare response - s = 'registration-result: success\n' \ - 'uuid: {id}\n' \ - 'key: {key}\n' \ - 'hostname: {name}\n' \ + s = ( + 'registration-result: success\n' + 'uuid: {id}\n' + 'key: {key}\n' + 'hostname: {name}\n' 'is-new: {is_new}\n' + ) attributes = device.__dict__.copy() - attributes.update({ - 'id': device.pk.hex, - 'key': device.key, - 'is_new': int(new) - }) - return ControllerResponse(s.format(**attributes), - content_type='text/plain', - status=201) + attributes.update({'id': device.pk.hex, 'key': device.key, 'is_new': int(new)}) + return ControllerResponse( + s.format(**attributes), content_type='text/plain', status=201 + ) diff --git a/django_netjsonconfig/controller/views.py b/django_netjsonconfig/controller/views.py index ee25bbd..1d564cb 100644 --- a/django_netjsonconfig/controller/views.py +++ b/django_netjsonconfig/controller/views.py @@ -1,7 +1,13 @@ from ..models import Device, Vpn -from .generics import (BaseDeviceChecksumView, BaseDeviceDownloadConfigView, BaseDeviceRegisterView, - BaseDeviceReportStatusView, BaseDeviceUpdateInfoView, BaseVpnChecksumView, - BaseVpnDownloadConfigView) +from .generics import ( + BaseDeviceChecksumView, + BaseDeviceDownloadConfigView, + BaseDeviceRegisterView, + BaseDeviceReportStatusView, + BaseDeviceUpdateInfoView, + BaseVpnChecksumView, + BaseVpnDownloadConfigView, +) class DeviceChecksumView(BaseDeviceChecksumView): diff --git a/django_netjsonconfig/migrations/0001_initial.py b/django_netjsonconfig/migrations/0001_initial.py index 468b62e..be97c34 100644 --- a/django_netjsonconfig/migrations/0001_initial.py +++ b/django_netjsonconfig/migrations/0001_initial.py @@ -23,13 +23,71 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Config', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ('name', models.CharField(max_length=63)), - ('backend', models.CharField(choices=[('netjsonconfig.OpenWrt', 'OpenWRT'), ('netjsonconfig.OpenWisp', 'OpenWISP')], help_text='Select netjsonconfig backend', max_length=128, verbose_name='backend')), - ('config', jsonfield.fields.JSONField(default=dict, dump_kwargs={'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='configuration')), - ('key', models.CharField(db_index=True, default=get_random_key, help_text='unique key that can be used to download the configuration', max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[^\\s/\\.]+$', 32), code='invalid', message='Key must not contain spaces, dots or slashes.')])), + ( + 'backend', + models.CharField( + choices=[ + ('netjsonconfig.OpenWrt', 'OpenWRT'), + ('netjsonconfig.OpenWisp', 'OpenWISP'), + ], + help_text='Select netjsonconfig backend', + max_length=128, + verbose_name='backend', + ), + ), + ( + 'config', + jsonfield.fields.JSONField( + default=dict, + dump_kwargs={'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='configuration', + ), + ), + ( + 'key', + models.CharField( + db_index=True, + default=get_random_key, + help_text='unique key that can be used to download the configuration', + max_length=64, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^[^\\s/\\.]+$', 32), + code='invalid', + message='Key must not contain spaces, dots or slashes.', + ) + ], + ), + ), ], options={ 'verbose_name_plural': 'configurations', @@ -39,20 +97,66 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Template', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ('name', models.CharField(max_length=63)), - ('backend', models.CharField(choices=[('netjsonconfig.OpenWrt', 'OpenWRT'), ('netjsonconfig.OpenWisp', 'OpenWISP')], help_text='Select netjsonconfig backend', max_length=128, verbose_name='backend')), - ('config', jsonfield.fields.JSONField(default=dict, dump_kwargs={'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='configuration')), + ( + 'backend', + models.CharField( + choices=[ + ('netjsonconfig.OpenWrt', 'OpenWRT'), + ('netjsonconfig.OpenWisp', 'OpenWISP'), + ], + help_text='Select netjsonconfig backend', + max_length=128, + verbose_name='backend', + ), + ), + ( + 'config', + jsonfield.fields.JSONField( + default=dict, + dump_kwargs={'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='configuration', + ), + ), ], - options={ - 'abstract': False, - }, + options={'abstract': False,}, ), migrations.AddField( model_name='config', name='templates', - field=sortedm2m.fields.SortedManyToManyField(blank=True, help_text='configuration templates, applied fromfirst to last', related_name='config_relations', to='django_netjsonconfig.Template', verbose_name='templates'), + field=sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text='configuration templates, applied fromfirst to last', + related_name='config_relations', + to='django_netjsonconfig.Template', + verbose_name='templates', + ), ), ] diff --git a/django_netjsonconfig/migrations/0002_config_status.py b/django_netjsonconfig/migrations/0002_config_status.py index 31c619c..984cfb6 100644 --- a/django_netjsonconfig/migrations/0002_config_status.py +++ b/django_netjsonconfig/migrations/0002_config_status.py @@ -15,6 +15,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='config', name='status', - field=model_utils.fields.StatusField(default='modified', help_text='modified means the configuration is not applied yet; running means applied and running; error means the configuration caused issues and it was rolledback', max_length=100, no_check_for_status=True), + field=model_utils.fields.StatusField( + default='modified', + help_text='modified means the configuration is not applied yet; running means applied and running; error means the configuration caused issues and it was rolledback', + max_length=100, + no_check_for_status=True, + ), ), ] diff --git a/django_netjsonconfig/migrations/0003_config_last_ip.py b/django_netjsonconfig/migrations/0003_config_last_ip.py index c0100a7..12990f4 100644 --- a/django_netjsonconfig/migrations/0003_config_last_ip.py +++ b/django_netjsonconfig/migrations/0003_config_last_ip.py @@ -14,6 +14,10 @@ class Migration(migrations.Migration): migrations.AddField( model_name='config', name='last_ip', - field=models.GenericIPAddressField(blank=True, help_text='indicates the last ip from which the configuration was downloaded from (except downloads from this page)', null=True), + field=models.GenericIPAddressField( + blank=True, + help_text='indicates the last ip from which the configuration was downloaded from (except downloads from this page)', + null=True, + ), ), ] diff --git a/django_netjsonconfig/migrations/0004_config_allow_blank.py b/django_netjsonconfig/migrations/0004_config_allow_blank.py index 904fb89..09686d9 100644 --- a/django_netjsonconfig/migrations/0004_config_allow_blank.py +++ b/django_netjsonconfig/migrations/0004_config_allow_blank.py @@ -16,6 +16,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='config', - field=jsonfield.fields.JSONField(blank=True, default=dict, dump_kwargs={'ensure_ascii': False, 'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='configuration'), + field=jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={'ensure_ascii': False, 'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='configuration', + ), ), ] diff --git a/django_netjsonconfig/migrations/0005_template_default.py b/django_netjsonconfig/migrations/0005_template_default.py index 9383a01..eb3f0bb 100644 --- a/django_netjsonconfig/migrations/0005_template_default.py +++ b/django_netjsonconfig/migrations/0005_template_default.py @@ -14,6 +14,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='template', name='default', - field=models.BooleanField(db_index=True, default=False, help_text='whether new configurations will have this template enabled by default', verbose_name='enabled by default'), + field=models.BooleanField( + db_index=True, + default=False, + help_text='whether new configurations will have this template enabled by default', + verbose_name='enabled by default', + ), ), ] diff --git a/django_netjsonconfig/migrations/0008_vpn_integration.py b/django_netjsonconfig/migrations/0008_vpn_integration.py index c58ed39..5b0519c 100644 --- a/django_netjsonconfig/migrations/0008_vpn_integration.py +++ b/django_netjsonconfig/migrations/0008_vpn_integration.py @@ -23,16 +23,80 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Vpn', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ('name', models.CharField(max_length=64, unique=True)), - ('host', models.CharField(max_length=64, help_text='VPN server hostname or ip address')), + ( + 'host', + models.CharField( + max_length=64, help_text='VPN server hostname or ip address' + ), + ), ('notes', models.TextField(blank=True)), - ('backend', models.CharField(choices=[('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN')], help_text='Select VPN configuration backend', max_length=128, verbose_name='VPN backend')), - ('config', jsonfield.fields.JSONField(blank=True, default=dict, dump_kwargs={'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='server configuration')), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_x509.Ca', verbose_name='CA')), - ('cert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_x509.Cert', verbose_name='x509 Certificate', help_text='leave blank to create automatically')), + ( + 'backend', + models.CharField( + choices=[ + ('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN') + ], + help_text='Select VPN configuration backend', + max_length=128, + verbose_name='VPN backend', + ), + ), + ( + 'config', + jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='server configuration', + ), + ), + ( + 'ca', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='django_x509.Ca', + verbose_name='CA', + ), + ), + ( + 'cert', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='django_x509.Cert', + verbose_name='x509 Certificate', + help_text='leave blank to create automatically', + ), + ), ], options={ 'verbose_name_plural': 'VPN Servers', @@ -42,35 +106,87 @@ class Migration(migrations.Migration): migrations.CreateModel( name='VpnClient', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('auto_cert', models.BooleanField(default=False)), - ('cert', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_x509.Cert')), - ('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_netjsonconfig.Config')), - ('vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_netjsonconfig.Vpn')), + ( + 'cert', + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='django_x509.Cert', + ), + ), + ( + 'config', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='django_netjsonconfig.Config', + ), + ), + ( + 'vpn', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='django_netjsonconfig.Vpn', + ), + ), ], ), migrations.AddField( model_name='template', name='auto_cert', - field=models.BooleanField(db_index=True, default=django_netjsonconfig.base.template.default_auto_cert, help_text='whether x509 client certificates should be automatically managed behind the scenes for each configuration using this template, valid only for the VPN type', verbose_name='auto certificate'), + field=models.BooleanField( + db_index=True, + default=django_netjsonconfig.base.template.default_auto_cert, + help_text='whether x509 client certificates should be automatically managed behind the scenes for each configuration using this template, valid only for the VPN type', + verbose_name='auto certificate', + ), ), migrations.AddField( model_name='template', name='type', - field=models.CharField(choices=[('generic', 'Generic'), ('vpn', 'VPN-client')], db_index=True, default='generic', help_text='template type, determines which features are available', max_length=16, verbose_name='type'), + field=models.CharField( + choices=[('generic', 'Generic'), ('vpn', 'VPN-client')], + db_index=True, + default='generic', + help_text='template type, determines which features are available', + max_length=16, + verbose_name='type', + ), ), migrations.AddField( model_name='config', name='vpn', - field=models.ManyToManyField(blank=True, help_text='Automated VPN configurations', related_name='vpn_relations', through='django_netjsonconfig.VpnClient', to='django_netjsonconfig.Vpn', verbose_name='VPN'), + field=models.ManyToManyField( + blank=True, + help_text='Automated VPN configurations', + related_name='vpn_relations', + through='django_netjsonconfig.VpnClient', + to='django_netjsonconfig.Vpn', + verbose_name='VPN', + ), ), migrations.AddField( model_name='template', name='vpn', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_netjsonconfig.Vpn', verbose_name='VPN'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='django_netjsonconfig.Vpn', + verbose_name='VPN', + ), ), migrations.AlterUniqueTogether( - name='vpnclient', - unique_together=set([('config', 'vpn')]), + name='vpnclient', unique_together=set([('config', 'vpn')]), ), ] diff --git a/django_netjsonconfig/migrations/0010_basemodel_reorganization.py b/django_netjsonconfig/migrations/0010_basemodel_reorganization.py index e7b9e2a..2c36d16 100644 --- a/django_netjsonconfig/migrations/0010_basemodel_reorganization.py +++ b/django_netjsonconfig/migrations/0010_basemodel_reorganization.py @@ -14,18 +14,20 @@ class Migration(migrations.Migration): operations = [ migrations.AlterField( - model_name='config', - name='name', - field=models.CharField(max_length=64), + model_name='config', name='name', field=models.CharField(max_length=64), ), migrations.AlterField( - model_name='template', - name='name', - field=models.CharField(max_length=64), + model_name='template', name='name', field=models.CharField(max_length=64), ), migrations.AlterField( model_name='vpn', name='config', - field=jsonfield.fields.JSONField(default=dict, dump_kwargs={'ensure_ascii': False, 'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='configuration'), + field=jsonfield.fields.JSONField( + default=dict, + dump_kwargs={'ensure_ascii': False, 'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='configuration', + ), ), ] diff --git a/django_netjsonconfig/migrations/0011_template_config_blank.py b/django_netjsonconfig/migrations/0011_template_config_blank.py index e9a1468..b11f53d 100644 --- a/django_netjsonconfig/migrations/0011_template_config_blank.py +++ b/django_netjsonconfig/migrations/0011_template_config_blank.py @@ -17,6 +17,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='template', name='config', - field=jsonfield.fields.JSONField(blank=True, default=dict, dump_kwargs={'ensure_ascii': False, 'indent': 4}, help_text='configuration in NetJSON DeviceConfiguration format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='configuration'), + field=jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={'ensure_ascii': False, 'indent': 4}, + help_text='configuration in NetJSON DeviceConfiguration format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='configuration', + ), ), ] diff --git a/django_netjsonconfig/migrations/0013_config_mac_address.py b/django_netjsonconfig/migrations/0013_config_mac_address.py index e60742e..3ba9a5a 100644 --- a/django_netjsonconfig/migrations/0013_config_mac_address.py +++ b/django_netjsonconfig/migrations/0013_config_mac_address.py @@ -17,6 +17,16 @@ class Migration(migrations.Migration): migrations.AddField( model_name='config', name='mac_address', - field=models.CharField(max_length=17, null=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), code='invalid', message='Must be a valid mac address.')]), + field=models.CharField( + max_length=17, + null=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), ), ] diff --git a/django_netjsonconfig/migrations/0014_randomize_mac_address.py b/django_netjsonconfig/migrations/0014_randomize_mac_address.py index 734501b..5d1f162 100644 --- a/django_netjsonconfig/migrations/0014_randomize_mac_address.py +++ b/django_netjsonconfig/migrations/0014_randomize_mac_address.py @@ -19,5 +19,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(randomize_mac_address, reverse_code=migrations.RunPython.noop), + migrations.RunPython( + randomize_mac_address, reverse_code=migrations.RunPython.noop + ), ] diff --git a/django_netjsonconfig/migrations/0015_config_mac_address_unique.py b/django_netjsonconfig/migrations/0015_config_mac_address_unique.py index ef80b30..f869baa 100644 --- a/django_netjsonconfig/migrations/0015_config_mac_address_unique.py +++ b/django_netjsonconfig/migrations/0015_config_mac_address_unique.py @@ -17,6 +17,16 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='mac_address', - field=models.CharField(max_length=17, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), code='invalid', message='Must be a valid mac address.')]), + field=models.CharField( + max_length=17, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), ), ] diff --git a/django_netjsonconfig/migrations/0016_vpn_dh.py b/django_netjsonconfig/migrations/0016_vpn_dh.py index 4e4d366..41716b1 100644 --- a/django_netjsonconfig/migrations/0016_vpn_dh.py +++ b/django_netjsonconfig/migrations/0016_vpn_dh.py @@ -12,8 +12,6 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( - model_name='vpn', - name='dh', - field=models.TextField(blank=True), + model_name='vpn', name='dh', field=models.TextField(blank=True), ), ] diff --git a/django_netjsonconfig/migrations/0019_cleanup_model_options.py b/django_netjsonconfig/migrations/0019_cleanup_model_options.py index 0e2e821..66e012a 100644 --- a/django_netjsonconfig/migrations/0019_cleanup_model_options.py +++ b/django_netjsonconfig/migrations/0019_cleanup_model_options.py @@ -17,15 +17,26 @@ class Migration(migrations.Migration): ), migrations.AlterModelOptions( name='vpn', - options={'verbose_name': 'VPN server', 'verbose_name_plural': 'VPN servers'}, + options={ + 'verbose_name': 'VPN server', + 'verbose_name_plural': 'VPN servers', + }, ), migrations.AlterModelOptions( name='vpnclient', - options={'verbose_name': 'VPN client', 'verbose_name_plural': 'VPN clients'}, + options={ + 'verbose_name': 'VPN client', + 'verbose_name_plural': 'VPN clients', + }, ), migrations.AlterField( model_name='config', name='vpn', - field=models.ManyToManyField(blank=True, related_name='vpn_relations', through='django_netjsonconfig.VpnClient', to='django_netjsonconfig.Vpn'), + field=models.ManyToManyField( + blank=True, + related_name='vpn_relations', + through='django_netjsonconfig.VpnClient', + to='django_netjsonconfig.Vpn', + ), ), ] diff --git a/django_netjsonconfig/migrations/0020_openvpn_resolv_retry.py b/django_netjsonconfig/migrations/0020_openvpn_resolv_retry.py index 1d4b7a9..b1de903 100644 --- a/django_netjsonconfig/migrations/0020_openvpn_resolv_retry.py +++ b/django_netjsonconfig/migrations/0020_openvpn_resolv_retry.py @@ -17,8 +17,9 @@ def forward(apps, schema_editor): Vpn = apps.get_model('django_netjsonconfig', 'Vpn') for model in [Config, Template, Vpn]: # find objects which have OpenVPN configurations containing the "resolv_retry" attribute - queryset = model.objects.filter(config__contains='"openvpn"')\ - .filter(config__contains='"resolv_retry"') + queryset = model.objects.filter(config__contains='"openvpn"').filter( + config__contains='"resolv_retry"' + ) for obj in queryset: for vpn in obj.config['openvpn']: if 'resolv_retry' in vpn: diff --git a/django_netjsonconfig/migrations/0021_netjsonconfig_label.py b/django_netjsonconfig/migrations/0021_netjsonconfig_label.py index 819e85e..8a6e8a5 100644 --- a/django_netjsonconfig/migrations/0021_netjsonconfig_label.py +++ b/django_netjsonconfig/migrations/0021_netjsonconfig_label.py @@ -14,11 +14,27 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='backend', - field=models.CharField(choices=[('netjsonconfig.OpenWrt', 'OpenWRT'), ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x')], help_text='Select netjsonconfig backend', max_length=128, verbose_name='backend'), + field=models.CharField( + choices=[ + ('netjsonconfig.OpenWrt', 'OpenWRT'), + ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), + ], + help_text='Select netjsonconfig backend', + max_length=128, + verbose_name='backend', + ), ), migrations.AlterField( model_name='template', name='backend', - field=models.CharField(choices=[('netjsonconfig.OpenWrt', 'OpenWRT'), ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x')], help_text='Select netjsonconfig backend', max_length=128, verbose_name='backend'), + field=models.CharField( + choices=[ + ('netjsonconfig.OpenWrt', 'OpenWRT'), + ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), + ], + help_text='Select netjsonconfig backend', + max_length=128, + verbose_name='backend', + ), ), ] diff --git a/django_netjsonconfig/migrations/0022_update_model_labels.py b/django_netjsonconfig/migrations/0022_update_model_labels.py index 17bc5b2..302543d 100644 --- a/django_netjsonconfig/migrations/0022_update_model_labels.py +++ b/django_netjsonconfig/migrations/0022_update_model_labels.py @@ -18,11 +18,28 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='mac_address', - field=models.CharField(help_text='primary mac address', max_length=17, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), code='invalid', message='Must be a valid mac address.')]), + field=models.CharField( + help_text='primary mac address', + max_length=17, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), ), migrations.AlterField( model_name='config', name='templates', - field=sortedm2m.fields.SortedManyToManyField(blank=True, help_text='configuration templates, applied from first to last', related_name='config_relations', to='django_netjsonconfig.Template', verbose_name='templates'), + field=sortedm2m.fields.SortedManyToManyField( + blank=True, + help_text='configuration templates, applied from first to last', + related_name='config_relations', + to='django_netjsonconfig.Template', + verbose_name='templates', + ), ), ] diff --git a/django_netjsonconfig/migrations/0023_template_tags.py b/django_netjsonconfig/migrations/0023_template_tags.py index 5c49985..b7146dd 100644 --- a/django_netjsonconfig/migrations/0023_template_tags.py +++ b/django_netjsonconfig/migrations/0023_template_tags.py @@ -19,9 +19,28 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TaggedTemplate', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.UUIDField(db_index=True, verbose_name='Object id')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='django_netjsonconfig_taggedtemplate_tagged_items', to='contenttypes.ContentType', verbose_name='Content type')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'object_id', + models.UUIDField(db_index=True, verbose_name='Object id'), + ), + ( + 'content_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='django_netjsonconfig_taggedtemplate_tagged_items', + to='contenttypes.ContentType', + verbose_name='Content type', + ), + ), ], options={ 'verbose_name_plural': 'Tags', @@ -32,9 +51,23 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TemplateTag', fields=[ - ('name', models.CharField(max_length=100, unique=True, verbose_name='Name')), - ('slug', models.SlugField(max_length=100, unique=True, verbose_name='Slug')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + 'name', + models.CharField(max_length=100, unique=True, verbose_name='Name'), + ), + ( + 'slug', + models.SlugField(max_length=100, unique=True, verbose_name='Slug'), + ), + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), ], options={ 'verbose_name_plural': 'Tags', @@ -45,11 +78,21 @@ class Migration(migrations.Migration): migrations.AddField( model_name='taggedtemplate', name='tag', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='django_netjsonconfig_taggedtemplate_items', to='django_netjsonconfig.TemplateTag'), + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='django_netjsonconfig_taggedtemplate_items', + to='django_netjsonconfig.TemplateTag', + ), ), migrations.AddField( model_name='template', name='tags', - field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of template tags, may be used to ease auto configuration with specific settings (eg: 4G, mesh, WDS, VPN, ecc.)', through='django_netjsonconfig.TaggedTemplate', to='django_netjsonconfig.TemplateTag', verbose_name='Tags'), + field=taggit.managers.TaggableManager( + blank=True, + help_text='A comma-separated list of template tags, may be used to ease auto configuration with specific settings (eg: 4G, mesh, WDS, VPN, ecc.)', + through='django_netjsonconfig.TaggedTemplate', + to='django_netjsonconfig.TemplateTag', + verbose_name='Tags', + ), ), ] diff --git a/django_netjsonconfig/migrations/0024_add_device_model.py b/django_netjsonconfig/migrations/0024_add_device_model.py index 3850c67..76909ce 100644 --- a/django_netjsonconfig/migrations/0024_add_device_model.py +++ b/django_netjsonconfig/migrations/0024_add_device_model.py @@ -23,23 +23,95 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Device', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + 'id', + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), ('name', models.CharField(max_length=64, unique=True)), - ('mac_address', models.CharField(help_text='primary mac address', max_length=17, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), code='invalid', message='Must be a valid mac address.')])), - ('key', openwisp_utils.base.KeyField(db_index=True, default=openwisp_utils.utils.get_random_key, help_text='unique device key', max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[^\\s/\\.]+$'), code='invalid', message='This value must not contain spaces, dots or slashes.')])), - ('model', models.CharField(blank=True, help_text='device model and manufacturer', max_length=64)), - ('os', models.CharField(blank=True, help_text='operating system identifier', max_length=128, verbose_name='operating system')), + ( + 'mac_address', + models.CharField( + help_text='primary mac address', + max_length=17, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile( + '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32 + ), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), + ), + ( + 'key', + openwisp_utils.base.KeyField( + db_index=True, + default=openwisp_utils.utils.get_random_key, + help_text='unique device key', + max_length=64, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^[^\\s/\\.]+$'), + code='invalid', + message='This value must not contain spaces, dots or slashes.', + ) + ], + ), + ), + ( + 'model', + models.CharField( + blank=True, + help_text='device model and manufacturer', + max_length=64, + ), + ), + ( + 'os', + models.CharField( + blank=True, + help_text='operating system identifier', + max_length=128, + verbose_name='operating system', + ), + ), ('notes', models.TextField(blank=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ], - options={ - 'abstract': False, - }, + options={'abstract': False,}, ), migrations.AddField( model_name='config', name='device', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_netjsonconfig.Device'), + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='django_netjsonconfig.Device', + ), ), ] diff --git a/django_netjsonconfig/migrations/0025_populate_device.py b/django_netjsonconfig/migrations/0025_populate_device.py index c75fd99..66d5eb5 100644 --- a/django_netjsonconfig/migrations/0025_populate_device.py +++ b/django_netjsonconfig/migrations/0025_populate_device.py @@ -14,12 +14,14 @@ def forward(apps, schema_editor): Config = apps.get_model('django_netjsonconfig', 'Config') for config in Config.objects.all(): - device = Device(id=config.id, - name=config.name, - mac_address=config.mac_address, - key=config.key, - created=config.created, - modified=config.modified) + device = Device( + id=config.id, + name=config.name, + mac_address=config.mac_address, + key=config.key, + created=config.created, + modified=config.modified, + ) device.full_clean() device.save() config.device = device diff --git a/django_netjsonconfig/migrations/0026_config_device_not_null.py b/django_netjsonconfig/migrations/0026_config_device_not_null.py index b9b7a1b..e8c52c7 100644 --- a/django_netjsonconfig/migrations/0026_config_device_not_null.py +++ b/django_netjsonconfig/migrations/0026_config_device_not_null.py @@ -15,6 +15,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='device', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='django_netjsonconfig.Device'), + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to='django_netjsonconfig.Device', + ), ), ] diff --git a/django_netjsonconfig/migrations/0027_simplify_config.py b/django_netjsonconfig/migrations/0027_simplify_config.py index 30233e1..4abe99e 100644 --- a/django_netjsonconfig/migrations/0027_simplify_config.py +++ b/django_netjsonconfig/migrations/0027_simplify_config.py @@ -11,16 +11,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='config', - name='name', - ), - migrations.RemoveField( - model_name='config', - name='key', - ), - migrations.RemoveField( - model_name='config', - name='mac_address', - ), + migrations.RemoveField(model_name='config', name='name',), + migrations.RemoveField(model_name='config', name='key',), + migrations.RemoveField(model_name='config', name='mac_address',), ] diff --git a/django_netjsonconfig/migrations/0028_device_indexes.py b/django_netjsonconfig/migrations/0028_device_indexes.py index d4bc97f..db69a41 100644 --- a/django_netjsonconfig/migrations/0028_device_indexes.py +++ b/django_netjsonconfig/migrations/0028_device_indexes.py @@ -14,11 +14,22 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='model', - field=models.CharField(blank=True, db_index=True, help_text='device model and manufacturer', max_length=64), + field=models.CharField( + blank=True, + db_index=True, + help_text='device model and manufacturer', + max_length=64, + ), ), migrations.AlterField( model_name='device', name='os', - field=models.CharField(blank=True, db_index=True, help_text='operating system identifier', max_length=128, verbose_name='operating system'), + field=models.CharField( + blank=True, + db_index=True, + help_text='operating system identifier', + max_length=128, + verbose_name='operating system', + ), ), ] diff --git a/django_netjsonconfig/migrations/0029_explicit_indexes.py b/django_netjsonconfig/migrations/0029_explicit_indexes.py index 40f656a..9131bb5 100644 --- a/django_netjsonconfig/migrations/0029_explicit_indexes.py +++ b/django_netjsonconfig/migrations/0029_explicit_indexes.py @@ -17,7 +17,19 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='mac_address', - field=models.CharField(db_index=True, help_text='primary mac address', max_length=17, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), code='invalid', message='Must be a valid mac address.')]), + field=models.CharField( + db_index=True, + help_text='primary mac address', + max_length=17, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), ), migrations.AlterField( model_name='device', diff --git a/django_netjsonconfig/migrations/0030_device_system.py b/django_netjsonconfig/migrations/0030_device_system.py index e90c02f..4f6043a 100644 --- a/django_netjsonconfig/migrations/0030_device_system.py +++ b/django_netjsonconfig/migrations/0030_device_system.py @@ -14,6 +14,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='device', name='system', - field=models.CharField(blank=True, db_index=True, help_text='system on chip or CPU info', max_length=128, verbose_name='SOC / CPU'), + field=models.CharField( + blank=True, + db_index=True, + help_text='system on chip or CPU info', + max_length=128, + verbose_name='SOC / CPU', + ), ), ] diff --git a/django_netjsonconfig/migrations/0031_updated_mac_address_validator.py b/django_netjsonconfig/migrations/0031_updated_mac_address_validator.py index 80f3e0a..440af49 100644 --- a/django_netjsonconfig/migrations/0031_updated_mac_address_validator.py +++ b/django_netjsonconfig/migrations/0031_updated_mac_address_validator.py @@ -17,6 +17,18 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='mac_address', - field=models.CharField(db_index=True, help_text='primary mac address', max_length=17, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', 32), code='invalid', message='Must be a valid mac address.')]), + field=models.CharField( + db_index=True, + help_text='primary mac address', + max_length=17, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', 32), + code='invalid', + message='Must be a valid mac address.', + ) + ], + ), ), ] diff --git a/django_netjsonconfig/migrations/0033_migrate_last_ip.py b/django_netjsonconfig/migrations/0033_migrate_last_ip.py index 89858f2..a79e0b5 100644 --- a/django_netjsonconfig/migrations/0033_migrate_last_ip.py +++ b/django_netjsonconfig/migrations/0033_migrate_last_ip.py @@ -33,11 +33,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='device', name='last_ip', - field=models.GenericIPAddressField(blank=True, help_text='indicates the IP address logged from the last request coming from the device', null=True), + field=models.GenericIPAddressField( + blank=True, + help_text='indicates the IP address logged from the last request coming from the device', + null=True, + ), ), migrations.RunPython(forward, backward), - migrations.RemoveField( - model_name='config', - name='last_ip' - ), + migrations.RemoveField(model_name='config', name='last_ip'), ] diff --git a/django_netjsonconfig/migrations/0034_device_management_ip.py b/django_netjsonconfig/migrations/0034_device_management_ip.py index d841df6..3f145ce 100644 --- a/django_netjsonconfig/migrations/0034_device_management_ip.py +++ b/django_netjsonconfig/migrations/0034_device_management_ip.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): migrations.AddField( model_name='device', name='management_ip', - field=models.GenericIPAddressField(blank=True, help_text='ip address of the management interface, if available', null=True), + field=models.GenericIPAddressField( + blank=True, + help_text='ip address of the management interface, if available', + null=True, + ), ), ] diff --git a/django_netjsonconfig/migrations/0035_renamed_status_choices.py b/django_netjsonconfig/migrations/0035_renamed_status_choices.py index b67811d..7a03b79 100644 --- a/django_netjsonconfig/migrations/0035_renamed_status_choices.py +++ b/django_netjsonconfig/migrations/0035_renamed_status_choices.py @@ -32,6 +32,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='status', - field=model_utils.fields.StatusField(choices=[(0, 'dummy')], default='modified', help_text='"modified" means the configuration is not applied yet; \n"applied" means the configuration is applied successfully; \n"error" means the configuration caused issues and it was rolled back;', max_length=100, no_check_for_status=True, verbose_name='configuration status'), + field=model_utils.fields.StatusField( + choices=[(0, 'dummy')], + default='modified', + help_text='"modified" means the configuration is not applied yet; \n"applied" means the configuration is applied successfully; \n"error" means the configuration caused issues and it was rolled back;', + max_length=100, + no_check_for_status=True, + verbose_name='configuration status', + ), ), ] diff --git a/django_netjsonconfig/migrations/0037_config_context.py b/django_netjsonconfig/migrations/0037_config_context.py index c48e01c..b5e00c1 100644 --- a/django_netjsonconfig/migrations/0037_config_context.py +++ b/django_netjsonconfig/migrations/0037_config_context.py @@ -15,6 +15,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='config', name='context', - field=jsonfield.fields.JSONField(blank=True, dump_kwargs={'ensure_ascii': False, 'indent': 4}, help_text='Additional context (configuration variables) in JSON format', load_kwargs={'object_pairs_hook': collections.OrderedDict}, null=True), + field=jsonfield.fields.JSONField( + blank=True, + dump_kwargs={'ensure_ascii': False, 'indent': 4}, + help_text='Additional context (configuration variables) in JSON format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + null=True, + ), ), ] diff --git a/django_netjsonconfig/migrations/0038_vpn_key.py b/django_netjsonconfig/migrations/0038_vpn_key.py index 0282ccf..98235b9 100644 --- a/django_netjsonconfig/migrations/0038_vpn_key.py +++ b/django_netjsonconfig/migrations/0038_vpn_key.py @@ -16,6 +16,18 @@ class Migration(migrations.Migration): migrations.AddField( model_name='vpn', name='key', - field=openwisp_utils.base.KeyField(db_index=True, default=openwisp_utils.utils.get_random_key, help_text=None, max_length=64, validators=[django.core.validators.RegexValidator(re.compile('^[^\\s/\\.]+$'), code='invalid', message='This value must not contain spaces, dots or slashes.')]), + field=openwisp_utils.base.KeyField( + db_index=True, + default=openwisp_utils.utils.get_random_key, + help_text=None, + max_length=64, + validators=[ + django.core.validators.RegexValidator( + re.compile('^[^\\s/\\.]+$'), + code='invalid', + message='This value must not contain spaces, dots or slashes.', + ) + ], + ), ), ] diff --git a/django_netjsonconfig/migrations/0040_update_context.py b/django_netjsonconfig/migrations/0040_update_context.py index fdb0196..df69b69 100644 --- a/django_netjsonconfig/migrations/0040_update_context.py +++ b/django_netjsonconfig/migrations/0040_update_context.py @@ -15,6 +15,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='config', name='context', - field=jsonfield.fields.JSONField(blank=True, default=dict, dump_kwargs={'ensure_ascii': False, 'indent': 4}, help_text='Additional context (configuration variables) in JSON format', load_kwargs={'object_pairs_hook': collections.OrderedDict}), + field=jsonfield.fields.JSONField( + blank=True, + default=dict, + dump_kwargs={'ensure_ascii': False, 'indent': 4}, + help_text='Additional context (configuration variables) in JSON format', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + ), ), ] diff --git a/django_netjsonconfig/migrations/0042_device_key_none.py b/django_netjsonconfig/migrations/0042_device_key_none.py index 9443927..82fbd6f 100644 --- a/django_netjsonconfig/migrations/0042_device_key_none.py +++ b/django_netjsonconfig/migrations/0042_device_key_none.py @@ -16,6 +16,20 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='key', - field=openwisp_utils.base.KeyField(blank=True, db_index=True, default=None, help_text='unique device key', max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[^\\s/\\.]+$'), code='invalid', message='This value must not contain spaces, dots or slashes.')]), + field=openwisp_utils.base.KeyField( + blank=True, + db_index=True, + default=None, + help_text='unique device key', + max_length=64, + unique=True, + validators=[ + django.core.validators.RegexValidator( + re.compile('^[^\\s/\\.]+$'), + code='invalid', + message='This value must not contain spaces, dots or slashes.', + ) + ], + ), ), ] diff --git a/django_netjsonconfig/migrations/0043_add_indexes_on_ip_fields.py b/django_netjsonconfig/migrations/0043_add_indexes_on_ip_fields.py index 9391e4b..4083eb1 100644 --- a/django_netjsonconfig/migrations/0043_add_indexes_on_ip_fields.py +++ b/django_netjsonconfig/migrations/0043_add_indexes_on_ip_fields.py @@ -13,11 +13,21 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='last_ip', - field=models.GenericIPAddressField(blank=True, db_index=True, help_text='indicates the IP address logged from the last request coming from the device', null=True), + field=models.GenericIPAddressField( + blank=True, + db_index=True, + help_text='indicates the IP address logged from the last request coming from the device', + null=True, + ), ), migrations.AlterField( model_name='device', name='management_ip', - field=models.GenericIPAddressField(blank=True, db_index=True, help_text='ip address of the management interface, if available', null=True), + field=models.GenericIPAddressField( + blank=True, + db_index=True, + help_text='ip address of the management interface, if available', + null=True, + ), ), ] diff --git a/django_netjsonconfig/models.py b/django_netjsonconfig/models.py index fa21312..77e4263 100644 --- a/django_netjsonconfig/models.py +++ b/django_netjsonconfig/models.py @@ -9,6 +9,7 @@ class Config(TemplatesVpnMixin, AbstractConfig): """ Concrete Config model """ + class Meta(AbstractConfig.Meta): abstract = False @@ -17,6 +18,7 @@ class Device(AbstractDevice): """ Concrete device model """ + class Meta(AbstractDevice.Meta): abstract = False @@ -25,6 +27,7 @@ class TemplateTag(AbstractTemplateTag): """ Concrete template tag model """ + class Meta(AbstractTemplateTag.Meta): abstract = False @@ -33,6 +36,7 @@ class TaggedTemplate(AbstractTaggedTemplate): """ tagged item model with support for UUID primary keys """ + class Meta(AbstractTaggedTemplate.Meta): abstract = False @@ -41,6 +45,7 @@ class Template(AbstractTemplate): """ Concrete Template model """ + class Meta(AbstractTemplate.Meta): abstract = False @@ -49,6 +54,7 @@ class VpnClient(AbstractVpnClient): """ Concrete VpnClient model """ + class Meta(AbstractVpnClient.Meta): abstract = False @@ -57,5 +63,6 @@ class Vpn(AbstractVpn): """ Concrete VPN model """ + class Meta(AbstractVpn.Meta): abstract = False diff --git a/django_netjsonconfig/settings.py b/django_netjsonconfig/settings.py index 852e97e..5cb2e06 100644 --- a/django_netjsonconfig/settings.py +++ b/django_netjsonconfig/settings.py @@ -1,25 +1,41 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -BACKENDS = getattr(settings, 'NETJSONCONFIG_BACKENDS', ( - ('netjsonconfig.OpenWrt', 'OpenWRT'), - ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), -)) -VPN_BACKENDS = getattr(settings, 'NETJSONCONFIG_VPN_BACKENDS', ( - ('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN'), -)) +BACKENDS = getattr( + settings, + 'NETJSONCONFIG_BACKENDS', + ( + ('netjsonconfig.OpenWrt', 'OpenWRT'), + ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), + ), +) +VPN_BACKENDS = getattr( + settings, + 'NETJSONCONFIG_VPN_BACKENDS', + (('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN'),), +) DEFAULT_BACKEND = getattr(settings, 'NETJSONCONFIG_DEFAULT_BACKEND', BACKENDS[0][0]) -DEFAULT_VPN_BACKEND = getattr(settings, 'NETJSONCONFIG_DEFAULT_VPN_BACKEND', VPN_BACKENDS[0][0]) +DEFAULT_VPN_BACKEND = getattr( + settings, 'NETJSONCONFIG_DEFAULT_VPN_BACKEND', VPN_BACKENDS[0][0] +) REGISTRATION_ENABLED = getattr(settings, 'NETJSONCONFIG_REGISTRATION_ENABLED', True) -CONSISTENT_REGISTRATION = getattr(settings, 'NETJSONCONFIG_CONSISTENT_REGISTRATION', True) -REGISTRATION_SELF_CREATION = getattr(settings, 'NETJSONCONFIG_REGISTRATION_SELF_CREATION', True) +CONSISTENT_REGISTRATION = getattr( + settings, 'NETJSONCONFIG_CONSISTENT_REGISTRATION', True +) +REGISTRATION_SELF_CREATION = getattr( + settings, 'NETJSONCONFIG_REGISTRATION_SELF_CREATION', True +) SHARED_SECRET = getattr(settings, 'NETJSONCONFIG_SHARED_SECRET', '') CONTEXT = getattr(settings, 'NETJSONCONFIG_CONTEXT', {}) assert isinstance(CONTEXT, dict), 'NETJSONCONFIG_CONTEXT must be a dictionary' DEFAULT_AUTO_CERT = getattr(settings, 'NETJSONCONFIG_DEFAULT_AUTO_CERT', True) CERT_PATH = getattr(settings, 'NETJSONCONFIG_CERT_PATH', '/etc/x509') -COMMON_NAME_FORMAT = getattr(settings, 'NETJSONCONFIG_COMMON_NAME_FORMAT', '{mac_address}-{name}') -MANAGEMENT_IP_DEVICE_LIST = getattr(settings, 'NETJSONCONFIG_MANAGEMENT_IP_DEVICE_LIST', True) +COMMON_NAME_FORMAT = getattr( + settings, 'NETJSONCONFIG_COMMON_NAME_FORMAT', '{mac_address}-{name}' +) +MANAGEMENT_IP_DEVICE_LIST = getattr( + settings, 'NETJSONCONFIG_MANAGEMENT_IP_DEVICE_LIST', True +) BACKEND_DEVICE_LIST = getattr(settings, 'NETJSONCONFIG_BACKEND_DEVICE_LIST', True) HARDWARE_ID_ENABLED = getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_ENABLED', False) @@ -29,7 +45,7 @@ 'max_length': 32, 'unique': True, 'verbose_name': _('Serial number'), - 'help_text': _('Serial number of this device') + 'help_text': _('Serial number of this device'), } HARDWARE_ID_OPTIONS.update(getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_OPTIONS', {})) HARDWARE_ID_AS_NAME = getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_AS_NAME', True) diff --git a/django_netjsonconfig/tests/__init__.py b/django_netjsonconfig/tests/__init__.py index 6d39bdb..7a2d55f 100644 --- a/django_netjsonconfig/tests/__init__.py +++ b/django_netjsonconfig/tests/__init__.py @@ -12,11 +12,13 @@ class CreateDeviceMixin(object): TEST_MAC_ADDRESS = '00:11:22:33:44:55' def _create_device(self, **kwargs): - options = dict(name='test-device', - mac_address=self.TEST_MAC_ADDRESS, - hardware_id=str(uuid4().hex), - model='TP-Link TL-WDR4300 v1', - os='LEDE Reboot 17.01-SNAPSHOT r3313-c2999ef') + options = dict( + name='test-device', + mac_address=self.TEST_MAC_ADDRESS, + hardware_id=str(uuid4().hex), + model='TP-Link TL-WDR4300 v1', + os='LEDE Reboot 17.01-SNAPSHOT r3313-c2999ef', + ) options.update(kwargs) d = self.device_model(**options) d.full_clean() @@ -37,8 +39,7 @@ class CreateConfigMixin(CreateDeviceMixin): TEST_KEY = 'w1gwJxKaHcamUw62TQIPgYchwLKn3AA0' def _create_config(self, **kwargs): - options = dict(backend='netjsonconfig.OpenWrt', - config={'general': {}}) + options = dict(backend='netjsonconfig.OpenWrt', config={'general': {}}) options.update(kwargs) if 'device' not in kwargs: options['device'] = self._create_device(name='test-device') @@ -53,14 +54,7 @@ def _create_template(self, **kwargs): model_kwargs = { "name": "test-template", "backend": "netjsonconfig.OpenWrt", - "config": { - "interfaces": [ - { - "name": "eth0", - "type": "ethernet" - } - ] - } + "config": {"interfaces": [{"name": "eth0", "type": "ethernet"}]}, } model_kwargs.update(kwargs) t = self.template_model(**model_kwargs) @@ -87,18 +81,20 @@ class CreateVpnMixin(object): "mode": "server", "name": "example-vpn", "proto": "udp", - "tls_server": True + "tls_server": True, } ] } def _create_vpn(self, ca_options={}, **kwargs): - options = dict(name='test', - host='vpn1.test.com', - ca=None, - backend='django_netjsonconfig.vpn_backends.OpenVpn', - config=self._vpn_config, - dh=self._dh) + options = dict( + name='test', + host='vpn1.test.com', + ca=None, + backend='django_netjsonconfig.vpn_backends.OpenVpn', + config=self._vpn_config, + dh=self._dh, + ) options.update(**kwargs) if not options['ca']: options['ca'] = self._create_ca(**ca_options) diff --git a/django_netjsonconfig/tests/test_admin.py b/django_netjsonconfig/tests/test_admin.py index 7f221eb..a2968ce 100644 --- a/django_netjsonconfig/tests/test_admin.py +++ b/django_netjsonconfig/tests/test_admin.py @@ -17,6 +17,7 @@ class TestAdmin(TestVpnX509Mixin, CreateConfigMixin, CreateTemplateMixin, TestCa """ tests for Config model """ + fixtures = ['test_templates'] maxDiff = None ca_model = Ca @@ -27,9 +28,9 @@ class TestAdmin(TestVpnX509Mixin, CreateConfigMixin, CreateTemplateMixin, TestCa template_model = Template def setUp(self): - User.objects.create_superuser(username='admin', - password='tester', - email='admin@admin.com') + User.objects.create_superuser( + username='admin', password='tester', email='admin@admin.com' + ) self.client.login(username='admin', password='tester') def _get_device_params(self): @@ -59,13 +60,15 @@ def test_change_device_clean_templates(self): c = self._create_config(device=d, backend=t.backend, config=t.config) path = reverse('admin:django_netjsonconfig_device_change', args=[d.pk]) params = self._get_device_params() - params.update({ - 'name': 'test-change-device', - 'config-0-id': str(c.pk), - 'config-0-device': str(d.pk), - 'config-0-templates': str(t.pk), - 'config-INITIAL_FORMS': 1 - }) + params.update( + { + 'name': 'test-change-device', + 'config-0-id': str(c.pk), + 'config-0-device': str(d.pk), + 'config-0-templates': str(t.pk), + 'config-INITIAL_FORMS': 1, + } + ) # ensure it fails with error response = self.client.post(path, params) self.assertContains(response, 'errors field-templates') @@ -78,10 +81,7 @@ def test_add_device(self): t = Template.objects.first() path = reverse('admin:django_netjsonconfig_device_add') params = self._get_device_params() - params.update({ - 'name': 'test-add-config', - 'config-0-templates': str(t.pk) - }) + params.update({'name': 'test-add-config', 'config-0-templates': str(t.pk)}) response = self.client.post(path, params) self.assertEqual(response.status_code, 302) self.assertEqual(Device.objects.filter(name=params['name']).count(), 1) @@ -103,25 +103,25 @@ def test_download_device_config_404(self): def test_preview_device_config(self): templates = Template.objects.all() path = reverse('admin:django_netjsonconfig_device_preview') - config = json.dumps({ - 'general': { - 'description': '{{hardware_id}}' - }, - 'interfaces': [ - { - 'name': 'lo0', - 'type': 'loopback', - 'addresses': [ - { - 'family': 'ipv4', - 'proto': 'static', - 'address': '127.0.0.1', - 'mask': 8 - } - ] - } - ] - }) + config = json.dumps( + { + 'general': {'description': '{{hardware_id}}'}, + 'interfaces': [ + { + 'name': 'lo0', + 'type': 'loopback', + 'addresses': [ + { + 'family': 'ipv4', + 'proto': 'static', + 'address': '127.0.0.1', + 'mask': 8, + } + ], + } + ], + } + ) data = { 'name': 'test-device', 'hardware_id': 'SERIAL012345', @@ -130,7 +130,7 @@ def test_preview_device_config(self): 'config': config, 'context': '', 'csrfmiddlewaretoken': 'test', - 'templates': ','.join([str(t.pk) for t in templates]) + 'templates': ','.join([str(t.pk) for t in templates]), } response = self.client.post(path, data) self.assertContains(response, '
= 3.0
-                "cid '{0}'".format(str(d.id)) in response_html,
-            ])
+            any(
+                [
+                    "cid '{0}'".format(str(d.id)) in response_html,
+                    # django >= 3.0
+                    "cid '{0}'".format(str(d.id)) in response_html,
+                ]
+            )
         )
         self.assertTrue(
-            any([
-                "ckey '{0}'".format(str(d.key)) in response_html,
-                # django >= 3.0
-                "ckey '{0}'".format(str(d.key)) in response_html,
-            ])
+            any(
+                [
+                    "ckey '{0}'".format(str(d.key)) in response_html,
+                    # django >= 3.0
+                    "ckey '{0}'".format(str(d.key)) in response_html,
+                ]
+            )
         )
         self.assertTrue(
-            any([
-                "cname '{0}'".format(str(d.name)) in response_html,
-                # django >= 3.0
-                "cname '{0}'".format(str(d.name)) in response_html,
-            ])
+            any(
+                [
+                    "cname '{0}'".format(str(d.name)) in response_html,
+                    # django >= 3.0
+                    "cname '{0}'".format(str(d.name)) in response_html,
+                ]
+            )
         )
 
     def test_download_vpn_config(self):
@@ -386,7 +398,7 @@ def test_preview_vpn(self):
             'ca': v.ca_id,
             'cert': v.cert_id,
             'config': json.dumps(v.config),
-            'csrfmiddlewaretoken': 'test'
+            'csrfmiddlewaretoken': 'test',
         }
         response = self.client.post(path, data)
         self.assertContains(response, '
= 3.0
-                "option interval '60'" in response_html
-            ])
+            any(
+                [
+                    "option interval '60'" in response_html,
+                    # django >= 3.0
+                    "option interval '60'" in response_html,
+                ]
+            )
         )
 
     def test_context_device(self):
diff --git a/django_netjsonconfig/tests/test_config.py b/django_netjsonconfig/tests/test_config.py
index 6149499..697e438 100644
--- a/django_netjsonconfig/tests/test_config.py
+++ b/django_netjsonconfig/tests/test_config.py
@@ -15,11 +15,11 @@
 from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin
 
 
-class TestConfig(CreateConfigMixin, CreateTemplateMixin,
-                 TestVpnX509Mixin, TestCase):
+class TestConfig(CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin, TestCase):
     """
     tests for Config model
     """
+
     fixtures = ['test_templates']
     maxDiff = None
     ca_model = Ca
@@ -35,9 +35,9 @@ def test_str(self):
         self.assertEqual(str(c), 'test')
 
     def test_config_not_none(self):
-        c = Config(device=self._create_device(),
-                   backend='netjsonconfig.OpenWrt',
-                   config=None)
+        c = Config(
+            device=self._create_device(), backend='netjsonconfig.OpenWrt', config=None
+        )
         c.full_clean()
         self.assertEqual(c.config, {})
 
@@ -47,15 +47,14 @@ def test_backend_class(self):
 
     def test_backend_instance(self):
         config = {'general': {'hostname': 'config'}}
-        c = Config(backend='netjsonconfig.OpenWrt',
-                   config=config)
+        c = Config(backend='netjsonconfig.OpenWrt', config=config)
         self.assertIsInstance(c.backend_instance, OpenWrt)
 
     def test_netjson_validation(self):
         config = {'interfaces': {'invalid': True}}
-        c = Config(device=self._create_device(),
-                   backend='netjsonconfig.OpenWrt',
-                   config=config)
+        c = Config(
+            device=self._create_device(), backend='netjsonconfig.OpenWrt', config=config
+        )
         # ensure django ValidationError is raised
         try:
             c.full_clean()
@@ -71,19 +70,12 @@ def test_json(self):
         c.templates.add(dhcp)
         c.templates.add(radio)
         full_config = {
-            'general': {
-                'hostname': 'json-test'
-            },
+            'general': {'hostname': 'json-test'},
             "interfaces": [
                 {
                     "name": "eth0",
                     "type": "ethernet",
-                    "addresses": [
-                        {
-                            "proto": "dhcp",
-                            "family": "ipv4"
-                        }
-                    ]
+                    "addresses": [{"proto": "dhcp", "family": "ipv4"}],
                 }
             ],
             "radios": [
@@ -95,9 +87,9 @@ def test_json(self):
                     "channel": 11,
                     "channel_width": 20,
                     "tx_power": 8,
-                    "country": "IT"
+                    "country": "IT",
                 }
-            ]
+            ],
         }
         del c.backend_instance
         self.assertDictEqual(c.json(dict=True), full_config)
@@ -110,19 +102,9 @@ def test_m2m_validation(self):
         # if config and template have a conflicting non-unique item
         # that violates the schema, the system should not allow
         # the assignment and raise an exception
-        config = {
-            "files": [
-                {
-                    "path": "/test",
-                    "mode": "0644",
-                    "contents": "test"
-                }
-            ]
-        }
+        config = {"files": [{"path": "/test", "mode": "0644", "contents": "test"}]}
         config_copy = deepcopy(config)
-        t = Template(name='files',
-                     backend='netjsonconfig.OpenWrt',
-                     config=config)
+        t = Template(name='files', backend='netjsonconfig.OpenWrt', config=config)
         t.full_clean()
         t.save()
         c = self._create_config(config=config_copy)
@@ -202,12 +184,14 @@ def test_config_context(self):
                 'id': '{{ id }}',
                 'key': '{{ key }}',
                 'name': '{{ name }}',
-                'mac_address': '{{ mac_address }}'
+                'mac_address': '{{ mac_address }}',
             }
         }
-        c = Config(device=self._create_device(name='context-test'),
-                   backend='netjsonconfig.OpenWrt',
-                   config=config)
+        c = Config(
+            device=self._create_device(name='context-test'),
+            backend='netjsonconfig.OpenWrt',
+            config=config,
+        )
         output = c.backend_instance.render()
         self.assertIn(str(c.device.id), output)
         self.assertIn(c.device.key, output)
@@ -215,14 +199,10 @@ def test_config_context(self):
         self.assertIn(c.device.mac_address, output)
 
     def test_context_setting(self):
-        config = {
-            'general': {
-                'vpnserver1': '{{ vpnserver1 }}'
-            }
-        }
-        c = Config(device=self._create_device(),
-                   backend='netjsonconfig.OpenWrt',
-                   config=config)
+        config = {'general': {'vpnserver1': '{{ vpnserver1 }}'}}
+        c = Config(
+            device=self._create_device(), backend='netjsonconfig.OpenWrt', config=config
+        )
         output = c.backend_instance.render()
         vpnserver1 = settings.NETJSONCONFIG_CONTEXT['vpnserver1']
         self.assertIn(vpnserver1, output)
@@ -233,9 +213,7 @@ def test_mac_address_as_hostname(self):
 
     def test_create_vpnclient(self):
         vpn = self._create_vpn()
-        t = self._create_template(name='test-network',
-                                  type='vpn',
-                                  vpn=vpn)
+        t = self._create_template(name='test-network', type='vpn', vpn=vpn)
         c = self._create_config(device=self._create_device(name='test-create-cert'))
         c.templates.add(t)
         c.save()
@@ -266,10 +244,9 @@ def test_clear_vpnclient(self):
 
     def test_create_cert(self):
         vpn = self._create_vpn()
-        t = self._create_template(name='test-create-cert',
-                                  type='vpn',
-                                  vpn=vpn,
-                                  auto_cert=True)
+        t = self._create_template(
+            name='test-create-cert', type='vpn', vpn=vpn, auto_cert=True
+        )
         c = self._create_config(device=self._create_device(name='test-create-cert'))
         c.templates.add(t)
         vpnclient = c.vpnclient_set.first()
@@ -427,30 +404,36 @@ def test_get_template_model_bound(self):
     def test_remove_duplicate_files(self):
         template1 = self._create_template(
             name='test-vpn-1',
-            config={'files': [
-                {
-                    'path': '/etc/vpnserver1',
-                    'mode': '0644',
-                    'contents': '{{ name }}\n{{ vpnserver1 }}\n'
-                }
-            ]}
+            config={
+                'files': [
+                    {
+                        'path': '/etc/vpnserver1',
+                        'mode': '0644',
+                        'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                    }
+                ]
+            },
         )
         template2 = self._create_template(
             name='test-vpn-2',
-            config={'files': [
-                {
-                    'path': '/etc/vpnserver1',
-                    'mode': '0644',
-                    'contents': '{{ name }}\n{{ vpnserver1 }}\n'
-                }
-            ]}
+            config={
+                'files': [
+                    {
+                        'path': '/etc/vpnserver1',
+                        'mode': '0644',
+                        'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                    }
+                ]
+            },
         )
         config = self._create_config()
         config.templates.add(template1)
         config.templates.add(template2)
         config.refresh_from_db()
         try:
-            result = config.get_backend_instance(template_instances=[template1, template2]).render()
+            result = config.get_backend_instance(
+                template_instances=[template1, template2]
+            ).render()
         except ValidationError:
             self.fail('ValidationError raised!')
         else:
@@ -458,18 +441,22 @@ def test_remove_duplicate_files(self):
 
     def test_duplicated_files_in_config(self):
         try:
-            self._create_config(config={'files': [
-                {
-                    'path': '/etc/vpnserver1',
-                    'mode': '0644',
-                    'contents': '{{ name }}\n{{ vpnserver1 }}\n'
-                },
-                {
-                    'path': '/etc/vpnserver1',
-                    'mode': '0644',
-                    'contents': '{{ name }}\n{{ vpnserver1 }}\n'
+            self._create_config(
+                config={
+                    'files': [
+                        {
+                            'path': '/etc/vpnserver1',
+                            'mode': '0644',
+                            'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                        },
+                        {
+                            'path': '/etc/vpnserver1',
+                            'mode': '0644',
+                            'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                        },
+                    ]
                 }
-            ]})
+            )
         except ValidationError as e:
             self.assertIn('Invalid configuration triggered by "#/files"', str(e))
         else:
@@ -495,9 +482,7 @@ def test_config_status_changed_modified(self):
         with catch_signal(config_status_changed) as handler:
             c.save()
             handler.assert_called_once_with(
-                sender=Config,
-                signal=config_status_changed,
-                instance=c,
+                sender=Config, signal=config_status_changed, instance=c,
             )
             self.assertEqual(c.status, 'modified')
 
@@ -527,7 +512,7 @@ def test_config_modified_sent(self):
                 signal=config_modified,
                 instance=c,
                 device=c.device,
-                config=c
+                config=c,
             )
             self.assertEqual(c.status, 'modified')
 
diff --git a/django_netjsonconfig/tests/test_controller.py b/django_netjsonconfig/tests/test_controller.py
index 0e00cbc..d3aeb30 100644
--- a/django_netjsonconfig/tests/test_controller.py
+++ b/django_netjsonconfig/tests/test_controller.py
@@ -10,7 +10,11 @@
 from openwisp_utils.tests import catch_signal
 
 from ..models import Config, Device, Template, Vpn
-from ..signals import checksum_requested, config_download_requested, config_status_changed
+from ..signals import (
+    checksum_requested,
+    config_download_requested,
+    config_status_changed,
+)
 from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin
 
 TEST_MACADDR = '00:11:22:33:44:55'
@@ -19,10 +23,13 @@
 REGISTER_URL = reverse('controller:device_register')
 
 
-class TestController(CreateConfigMixin, CreateTemplateMixin, TestCase, TestVpnX509Mixin):
+class TestController(
+    CreateConfigMixin, CreateTemplateMixin, TestCase, TestVpnX509Mixin
+):
     """
     tests for django_netjsonconfig.controller
     """
+
     config_model = Config
     device_model = Device
     template_model = Template
@@ -57,13 +64,15 @@ def test_device_checksum_requested_signal_is_emitted(self):
                 sender=Device,
                 signal=checksum_requested,
                 instance=d,
-                request=response.wsgi_request
+                request=response.wsgi_request,
             )
 
     def test_device_checksum_bad_uuid(self):
         d = self._create_device_config()
         pk = '{}-wrong'.format(d.pk)
-        response = self.client.get(reverse('controller:device_checksum', args=[pk]), {'key': d.key})
+        response = self.client.get(
+            reverse('controller:device_checksum', args=[pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_checksum_400(self):
@@ -74,20 +83,26 @@ def test_device_checksum_400(self):
 
     def test_device_checksum_403(self):
         d = self._create_device_config()
-        response = self.client.get(reverse('controller:device_checksum', args=[d.pk]), {'key': 'wrong'})
+        response = self.client.get(
+            reverse('controller:device_checksum', args=[d.pk]), {'key': 'wrong'}
+        )
         self.assertEqual(response.status_code, 403)
         self._check_header(response)
 
     def test_device_checksum_405(self):
         d = self._create_device_config()
-        response = self.client.post(reverse('controller:device_checksum', args=[d.pk]), {'key': d.key})
+        response = self.client.post(
+            reverse('controller:device_checksum', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_device_download_config(self):
         d = self._create_device_config()
         url = reverse('controller:device_download_config', args=[d.pk])
         response = self.client.get(url, {'key': d.key, 'management_ip': '10.0.0.2'})
-        self.assertEqual(response['Content-Disposition'], 'attachment; filename=test.tar.gz')
+        self.assertEqual(
+            response['Content-Disposition'], 'attachment; filename=test.tar.gz'
+        )
         self._check_header(response)
         d.refresh_from_db()
         self.assertIsNotNone(d.last_ip)
@@ -107,18 +122,22 @@ def test_device_config_download_requested_signal_is_emitted(self):
                 sender=Device,
                 signal=config_download_requested,
                 instance=d,
-                request=response.wsgi_request
+                request=response.wsgi_request,
             )
 
     def test_device_download_config_bad_uuid(self):
         d = self._create_device_config()
         pk = '{}-wrong'.format(d.pk)
-        response = self.client.get(reverse('controller:device_download_config', args=[pk]), {'key': d.key})
+        response = self.client.get(
+            reverse('controller:device_download_config', args=[pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_download_config_400(self):
         d = self._create_device_config()
-        response = self.client.get(reverse('controller:device_download_config', args=[d.pk]))
+        response = self.client.get(
+            reverse('controller:device_download_config', args=[d.pk])
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
 
@@ -131,7 +150,9 @@ def test_device_download_config_403(self):
 
     def test_device_download_config_405(self):
         d = self._create_device_config()
-        response = self.client.post(reverse('controller:device_download_config', args=[d.pk]), {'key': d.key})
+        response = self.client.post(
+            reverse('controller:device_download_config', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_vpn_checksum(self):
@@ -150,13 +171,15 @@ def test_vpn_checksum_requested_signal_is_emitted(self):
                 sender=Vpn,
                 signal=checksum_requested,
                 instance=v,
-                request=response.wsgi_request
+                request=response.wsgi_request,
             )
 
     def test_vpn_checksum_bad_uuid(self):
         v = self._create_vpn()
         pk = '{}-wrong'.format(v.pk)
-        response = self.client.get(reverse('controller:vpn_checksum', args=[pk]), {'key': v.key})
+        response = self.client.get(
+            reverse('controller:vpn_checksum', args=[pk]), {'key': v.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_vpn_checksum_400(self):
@@ -167,20 +190,26 @@ def test_vpn_checksum_400(self):
 
     def test_vpn_checksum_403(self):
         v = self._create_vpn()
-        response = self.client.get(reverse('controller:vpn_checksum', args=[v.pk]), {'key': 'wrong'})
+        response = self.client.get(
+            reverse('controller:vpn_checksum', args=[v.pk]), {'key': 'wrong'}
+        )
         self.assertEqual(response.status_code, 403)
         self._check_header(response)
 
     def test_vpn_checksum_405(self):
         v = self._create_vpn()
-        response = self.client.post(reverse('controller:vpn_checksum', args=[v.pk]), {'key': v.key})
+        response = self.client.post(
+            reverse('controller:vpn_checksum', args=[v.pk]), {'key': v.key}
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_vpn_download_config(self):
         v = self._create_vpn()
         url = reverse('controller:vpn_download_config', args=[v.pk])
         response = self.client.get(url, {'key': v.key})
-        self.assertEqual(response['Content-Disposition'], 'attachment; filename=test.tar.gz')
+        self.assertEqual(
+            response['Content-Disposition'], 'attachment; filename=test.tar.gz'
+        )
         self._check_header(response)
 
     def test_vpn_config_download_requested_signal_is_emitted(self):
@@ -192,18 +221,22 @@ def test_vpn_config_download_requested_signal_is_emitted(self):
                 sender=Vpn,
                 signal=config_download_requested,
                 instance=v,
-                request=response.wsgi_request
+                request=response.wsgi_request,
             )
 
     def test_vpn_download_config_bad_uuid(self):
         v = self._create_vpn()
         pk = '{}-wrong'.format(v.pk)
-        response = self.client.get(reverse('controller:vpn_download_config', args=[pk]), {'key': v.key})
+        response = self.client.get(
+            reverse('controller:vpn_download_config', args=[pk]), {'key': v.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_vpn_download_config_400(self):
         v = self._create_vpn()
-        response = self.client.get(reverse('controller:vpn_download_config', args=[v.pk]))
+        response = self.client.get(
+            reverse('controller:vpn_download_config', args=[v.pk])
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
 
@@ -216,7 +249,9 @@ def test_vpn_download_config_403(self):
 
     def test_vpn_download_config_405(self):
         v = self._create_vpn()
-        response = self.client.post(reverse('controller:vpn_download_config', args=[v.pk]), {'key': v.key})
+        response = self.client.post(
+            reverse('controller:vpn_download_config', args=[v.pk]), {'key': v.key}
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_register(self, **kwargs):
@@ -225,7 +260,7 @@ def test_register(self, **kwargs):
             'name': TEST_MACADDR,
             'mac_address': TEST_MACADDR,
             'hardware_id': '1234',
-            'backend': 'netjsonconfig.OpenWrt'
+            'backend': 'netjsonconfig.OpenWrt',
         }
         options.update(kwargs)
         response = self.client.post(REGISTER_URL, options)
@@ -270,71 +305,95 @@ def test_register_device_info(self):
 
     def test_register_400(self):
         # missing secret
-        response = self.client.post(REGISTER_URL, {
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertContains(response, 'secret', status_code=400)
         # missing name
-        response = self.client.post(REGISTER_URL, {
-            'mac_address': TEST_MACADDR,
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'mac_address': TEST_MACADDR,
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertContains(response, 'name', status_code=400)
         # missing backend
-        response = self.client.post(REGISTER_URL, {
-            'mac_address': TEST_MACADDR,
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'mac_address': TEST_MACADDR,
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+            },
+        )
         self.assertContains(response, 'backend', status_code=400)
         # missing mac_address
-        response = self.client.post(REGISTER_URL, {
-            'backend': 'netjsonconfig.OpenWrt',
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'backend': 'netjsonconfig.OpenWrt',
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+            },
+        )
         self.assertContains(response, 'mac_address', status_code=400)
         self._check_header(response)
 
     def test_register_failed_creation(self):
         self.test_register()
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertContains(response, 'already exists', status_code=400)
 
     def test_register_failed_creation_wrong_backend(self):
         self.test_register()
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'netjsonconfig.CLEARLYWRONG'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'netjsonconfig.CLEARLYWRONG',
+            },
+        )
         self.assertContains(response, 'backend', status_code=403)
 
     def test_register_403(self):
         # wrong secret
-        response = self.client.post(REGISTER_URL, {
-            'secret': 'WRONG',
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': 'WRONG',
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertContains(response, 'wrong secret', status_code=403)
         # wrong backend
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'wrong'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'wrong',
+            },
+        )
         self.assertContains(response, 'wrong backend', status_code=403)
         self._check_header(response)
 
@@ -343,14 +402,17 @@ def test_register_405(self):
         self.assertEqual(response.status_code, 405)
 
     def test_consistent_registration_new(self):
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'key': TEST_CONSISTENT_KEY,
-            'mac_address': TEST_MACADDR,
-            'hardware_id': '1234',
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'key': TEST_CONSISTENT_KEY,
+                'mac_address': TEST_MACADDR,
+                'hardware_id': '1234',
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertEqual(response.status_code, 201)
         lines = response.content.decode().split('\n')
         self.assertEqual(lines[0], 'registration-result: success')
@@ -368,13 +430,16 @@ def test_device_consistent_registration_existing(self):
         d = self._create_device_config()
         d.key = TEST_CONSISTENT_KEY
         d.save()
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'key': TEST_CONSISTENT_KEY,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'key': TEST_CONSISTENT_KEY,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertEqual(response.status_code, 201)
         lines = response.content.decode().split('\n')
         self.assertEqual(lines[0], 'registration-result: success')
@@ -391,13 +456,16 @@ def test_device_consistent_registration_exists_no_config(self):
         d = self._create_device()
         d.key = TEST_CONSISTENT_KEY
         d.save()
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'key': TEST_CONSISTENT_KEY,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'key': TEST_CONSISTENT_KEY,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertEqual(response.status_code, 201)
         lines = response.content.decode().split('\n')
         self.assertEqual(lines[0], 'registration-result: success')
@@ -424,7 +492,7 @@ def test_device_registration_update_hw_info(self):
             'backend': 'netjsonconfig.OpenWrt',
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
         self.assertNotEqual(d.os, params['os'])
         self.assertNotEqual(d.system, params['system'])
@@ -448,7 +516,7 @@ def test_device_registration_update_hw_info_no_config(self):
             'backend': 'netjsonconfig.OpenWrt',
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
         self.assertNotEqual(d.os, params['os'])
         self.assertNotEqual(d.system, params['system'])
@@ -466,8 +534,10 @@ def test_device_report_status_running(self):
         # TODO: remove in stable version 1.0
         """
         d = self._create_device_config()
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]),
-                                    {'key': d.key, 'status': 'running'})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]),
+            {'key': d.key, 'status': 'running'},
+        )
         self._check_header(response)
         d.config.refresh_from_db()
         self.assertEqual(d.config.status, 'applied')
@@ -477,13 +547,11 @@ def test_device_report_status_applied(self):
         with catch_signal(config_status_changed) as handler:
             response = self.client.post(
                 reverse('controller:device_report_status', args=[d.pk]),
-                {'key': d.key, 'status': 'applied'}
+                {'key': d.key, 'status': 'applied'},
             )
             d.config.refresh_from_db()
             handler.assert_called_once_with(
-                sender=Config,
-                signal=config_status_changed,
-                instance=d.config,
+                sender=Config, signal=config_status_changed, instance=d.config,
             )
         self._check_header(response)
         d.config.refresh_from_db()
@@ -494,13 +562,11 @@ def test_device_report_status_error(self):
         with catch_signal(config_status_changed) as handler:
             response = self.client.post(
                 reverse('controller:device_report_status', args=[d.pk]),
-                {'key': d.key, 'status': 'error'}
+                {'key': d.key, 'status': 'error'},
             )
             d.config.refresh_from_db()
             handler.assert_called_once_with(
-                sender=Config,
-                signal=config_status_changed,
-                instance=d.config,
+                sender=Config, signal=config_status_changed, instance=d.config,
             )
         self._check_header(response)
         self.assertEqual(d.config.status, 'error')
@@ -508,37 +574,49 @@ def test_device_report_status_error(self):
     def test_device_report_status_bad_uuid(self):
         d = self._create_device_config()
         pk = '{}-wrong'.format(d.pk)
-        response = self.client.post(reverse('controller:device_report_status', args=[pk]), {'key': d.key})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_report_status_400(self):
         d = self._create_device_config()
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]))
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk])
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]),
-                                    {'key': d.key})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]),
-                                    {'key': d.key})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
 
     def test_device_report_status_403(self):
         d = self._create_device_config()
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]), {'key': 'wrong'})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]), {'key': 'wrong'}
+        )
         self.assertEqual(response.status_code, 403)
         self._check_header(response)
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]),
-                                    {'key': d.key, 'status': 'madeup'})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]),
+            {'key': d.key, 'status': 'madeup'},
+        )
         self.assertEqual(response.status_code, 403)
         self._check_header(response)
 
     def test_device_report_status_405(self):
         d = self._create_device_config()
-        response = self.client.get(reverse('controller:device_report_status', args=[d.pk]),
-                                   {'key': d.key, 'status': 'running'})
+        response = self.client.get(
+            reverse('controller:device_report_status', args=[d.pk]),
+            {'key': d.key, 'status': 'running'},
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_device_update_info(self):
@@ -547,12 +625,14 @@ def test_device_update_info(self):
             'key': d.key,
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
         self.assertNotEqual(d.os, params['os'])
         self.assertNotEqual(d.system, params['system'])
         self.assertNotEqual(d.model, params['model'])
-        response = self.client.post(reverse('controller:device_update_info', args=[d.pk]), params)
+        response = self.client.post(
+            reverse('controller:device_update_info', args=[d.pk]), params
+        )
         self.assertEqual(response.status_code, 200)
         self._check_header(response)
         d.refresh_from_db()
@@ -567,9 +647,11 @@ def test_device_update_info_bad_uuid(self):
             'key': d.key,
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
-        response = self.client.post(reverse('controller:device_update_info', args=[pk]), params)
+        response = self.client.post(
+            reverse('controller:device_update_info', args=[pk]), params
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_update_info_400(self):
@@ -578,9 +660,11 @@ def test_device_update_info_400(self):
             'key': d.key,
             'model': 'TP-Link TL-WDR4300 v2 this model name is longer than 64 characters',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
-        response = self.client.post(reverse('controller:device_update_info', args=[d.pk]), params)
+        response = self.client.post(
+            reverse('controller:device_update_info', args=[d.pk]), params
+        )
         self.assertEqual(response.status_code, 400)
         self._check_header(response)
 
@@ -590,9 +674,11 @@ def test_device_update_info_403(self):
             'key': 'wrong',
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
-        response = self.client.post(reverse('controller:device_update_info', args=[d.pk]), params)
+        response = self.client.post(
+            reverse('controller:device_update_info', args=[d.pk]), params
+        )
         self.assertEqual(response.status_code, 403)
         self._check_header(response)
 
@@ -602,25 +688,33 @@ def test_device_update_info_405(self):
             'key': d.key,
             'model': 'TP-Link TL-WDR4300 v2',
             'os': 'OpenWrt 18.06-SNAPSHOT r7312-e60be11330',
-            'system': 'Atheros AR9344 rev 3'
+            'system': 'Atheros AR9344 rev 3',
         }
-        response = self.client.get(reverse('controller:device_update_info', args=[d.pk]), params)
+        response = self.client.get(
+            reverse('controller:device_update_info', args=[d.pk]), params
+        )
         self.assertEqual(response.status_code, 405)
 
     def test_device_checksum_no_config(self):
         d = self._create_device()
-        response = self.client.get(reverse('controller:device_checksum', args=[d.pk]), {'key': d.key})
+        response = self.client.get(
+            reverse('controller:device_checksum', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_download_no_config(self):
         d = self._create_device()
-        response = self.client.get(reverse('controller:device_download_config', args=[d.pk]), {'key': d.key})
+        response = self.client.get(
+            reverse('controller:device_download_config', args=[d.pk]), {'key': d.key}
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_device_report_status_no_config(self):
         d = self._create_device()
-        response = self.client.post(reverse('controller:device_report_status', args=[d.pk]),
-                                    {'key': d.key, 'status': 'running'})
+        response = self.client.post(
+            reverse('controller:device_report_status', args=[d.pk]),
+            {'key': d.key, 'status': 'running'},
+        )
         self.assertEqual(response.status_code, 404)
 
     def test_register_failed_rollback(self):
@@ -631,7 +725,7 @@ def test_register_failed_rollback(self):
                 'name': TEST_MACADDR,
                 'mac_address': TEST_MACADDR,
                 'hardware_id': '1234',
-                'backend': 'netjsonconfig.OpenWrt'
+                'backend': 'netjsonconfig.OpenWrt',
             }
             response = self.client.post(REGISTER_URL, options)
             self.assertEqual(response.status_code, 400)
@@ -639,14 +733,17 @@ def test_register_failed_rollback(self):
 
     @patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', False)
     def test_consistent_registration_disabled(self):
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'key': TEST_CONSISTENT_KEY,
-            'mac_address': TEST_MACADDR,
-            'hardware_id': '1234',
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'key': TEST_CONSISTENT_KEY,
+                'mac_address': TEST_MACADDR,
+                'hardware_id': '1234',
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertEqual(response.status_code, 201)
         lines = response.content.decode().split('\n')
         self.assertEqual(lines[0], 'registration-result: success')
@@ -659,12 +756,15 @@ def test_consistent_registration_disabled(self):
 
     @patch('django_netjsonconfig.settings.REGISTRATION_ENABLED', False)
     def test_registration_disabled(self):
-        response = self.client.post(REGISTER_URL, {
-            'secret': settings.NETJSONCONFIG_SHARED_SECRET,
-            'name': TEST_MACADDR,
-            'mac_address': TEST_MACADDR,
-            'backend': 'netjsonconfig.OpenWrt'
-        })
+        response = self.client.post(
+            REGISTER_URL,
+            {
+                'secret': settings.NETJSONCONFIG_SHARED_SECRET,
+                'name': TEST_MACADDR,
+                'mac_address': TEST_MACADDR,
+                'backend': 'netjsonconfig.OpenWrt',
+            },
+        )
         self.assertEqual(response.status_code, 403)
 
     @patch('django_netjsonconfig.settings.REGISTRATION_SELF_CREATION', False)
@@ -675,15 +775,17 @@ def test_self_creation_disabled(self):
             'mac_address': TEST_MACADDR,
             'hardware_id': '1234',
             'backend': 'netjsonconfig.OpenWrt',
-            'key': 'c09164172a9d178735f21d2fd92001fa'
+            'key': 'c09164172a9d178735f21d2fd92001fa',
         }
         # first attempt fails because device is not present in DB
         response = self.client.post(REGISTER_URL, options)
         self.assertEqual(response.status_code, 404)
         # once the device is created, everything works normally
-        device = self._create_device(name=options['name'],
-                                     mac_address=options['mac_address'],
-                                     hardware_id=options['hardware_id'])
+        device = self._create_device(
+            name=options['name'],
+            mac_address=options['mac_address'],
+            hardware_id=options['hardware_id'],
+        )
         self.assertEqual(device.key, options['key'])
         response = self.client.post(REGISTER_URL, options)
         self.assertEqual(response.status_code, 201)
diff --git a/django_netjsonconfig/tests/test_device.py b/django_netjsonconfig/tests/test_device.py
index 46a53c4..37be9b2 100644
--- a/django_netjsonconfig/tests/test_device.py
+++ b/django_netjsonconfig/tests/test_device.py
@@ -14,6 +14,7 @@ class TestDevice(CreateConfigMixin, TestCase):
     """
     tests for Device model
     """
+
     config_model = Config
     device_model = Device
 
@@ -28,12 +29,11 @@ def test_str_hardware_id(self):
         self.assertEqual(str(d), '123')
 
     def test_mac_address_validator(self):
-        d = Device(name='test',
-                   key=self.TEST_KEY)
+        d = Device(name='test', key=self.TEST_KEY)
         bad_mac_addresses_list = [
             '{0}:BB:CC'.format(self.TEST_MAC_ADDRESS),
             'AA:BB:CC:11:22033',
-            'AA BB CC 11 22 33'
+            'AA BB CC 11 22 33',
         ]
         for mac_address in bad_mac_addresses_list:
             d.mac_address = mac_address
@@ -41,14 +41,14 @@ def test_mac_address_validator(self):
                 d.full_clean()
             except ValidationError as e:
                 self.assertIn('mac_address', e.message_dict)
-                self.assertEqual(mac_address_validator.message,
-                                 e.message_dict['mac_address'][0])
+                self.assertEqual(
+                    mac_address_validator.message, e.message_dict['mac_address'][0]
+                )
             else:
                 self.fail('ValidationError not raised for "{0}"'.format(mac_address))
 
     def test_config_status_modified(self):
-        c = self._create_config(device=self._create_device(),
-                                status='applied')
+        c = self._create_config(device=self._create_device(), status='applied')
         self.assertEqual(c.status, 'applied')
         c.device.name = 'test-status-modified'
         c.device.full_clean()
@@ -57,9 +57,7 @@ def test_config_status_modified(self):
         self.assertEqual(c.status, 'modified')
 
     def test_key_validator(self):
-        d = Device(name='test',
-                   mac_address=self.TEST_MAC_ADDRESS,
-                   hardware_id='1234')
+        d = Device(name='test', mac_address=self.TEST_MAC_ADDRESS, hardware_id='1234')
         d.key = 'key/key'
         with self.assertRaises(ValidationError):
             d.full_clean()
@@ -95,11 +93,13 @@ def test_config_model_static(self):
 
     def test_get_default_templates(self):
         d = self._create_device()
-        self.assertEqual(d.get_default_templates().count(),
-                         Config().get_default_templates().count())
+        self.assertEqual(
+            d.get_default_templates().count(), Config().get_default_templates().count()
+        )
         self._create_config(device=d)
-        self.assertEqual(d.get_default_templates().count(),
-                         Config().get_default_templates().count())
+        self.assertEqual(
+            d.get_default_templates().count(), Config().get_default_templates().count()
+        )
 
     def test_bad_hostnames(self):
         bad_host_name_list = [
@@ -107,38 +107,40 @@ def test_bad_hostnames(self):
             'openwisp..mydomain.com',
             'openwisp,mydomain.test',
             '{0}:BB:CC'.format(self.TEST_MAC_ADDRESS),
-            'AA:BB:CC:11:22033'
+            'AA:BB:CC:11:22033',
         ]
         for host in bad_host_name_list:
             try:
                 self._create_device(name=host)
             except ValidationError as e:
                 self.assertIn('name', e.message_dict)
-                self.assertEqual(device_name_validator.message,
-                                 e.message_dict['name'][0])
+                self.assertEqual(
+                    device_name_validator.message, e.message_dict['name'][0]
+                )
             else:
                 self.fail('ValidationError not raised for "{0}"'.format(host))
 
     def test_add_device_with_context(self):
         d = self._create_device()
         d.save()
-        c = self._create_config(device=d, config={
-            "openwisp": [
-                {
-                    "config_name": "controller",
-                    "config_value": "http",
-                    "url": "http://controller.examplewifiservice.com",
-                    "interval": "{{ interval }}",
-                    "verify_ssl": "1",
-                    "uuid": "UUID",
-                    "key": self.TEST_KEY
-                }
-            ]
-        }, context={
-            'interval': '60'
-        })
-        self.assertEqual(c.json(dict=True)['openwisp'][0]['interval'],
-                         '60')
+        c = self._create_config(
+            device=d,
+            config={
+                "openwisp": [
+                    {
+                        "config_name": "controller",
+                        "config_value": "http",
+                        "url": "http://controller.examplewifiservice.com",
+                        "interval": "{{ interval }}",
+                        "verify_ssl": "1",
+                        "uuid": "UUID",
+                        "key": self.TEST_KEY,
+                    }
+                ]
+            },
+            context={'interval': '60'},
+        )
+        self.assertEqual(c.json(dict=True)['openwisp'][0]['interval'], '60')
 
     def test_get_context_with_config(self):
         d = self._create_device()
@@ -151,20 +153,22 @@ def test_get_context_without_config(self):
 
     @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', False)
     def test_generate_random_key(self):
-        d = self.device_model(name='test_generate_key',
-                              mac_address='00:11:22:33:44:55')
+        d = self.device_model(name='test_generate_key', mac_address='00:11:22:33:44:55')
         self.assertIsNone(d.key)
         # generating key twice shall not yield same result
-        self.assertNotEqual(d.generate_key(app_settings.SHARED_SECRET),
-                            d.generate_key(app_settings.SHARED_SECRET))
+        self.assertNotEqual(
+            d.generate_key(app_settings.SHARED_SECRET),
+            d.generate_key(app_settings.SHARED_SECRET),
+        )
 
     @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', True)
     @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_ENABLED', False)
     def test_generate_consistent_key_mac_address(self):
-        d = self.device_model(name='test_generate_key',
-                              mac_address='00:11:22:33:44:55')
+        d = self.device_model(name='test_generate_key', mac_address='00:11:22:33:44:55')
         self.assertIsNone(d.key)
-        string = '{}+{}'.format(d.mac_address, app_settings.SHARED_SECRET).encode('utf-8')
+        string = '{}+{}'.format(d.mac_address, app_settings.SHARED_SECRET).encode(
+            'utf-8'
+        )
         expected = md5(string).hexdigest()
         key = d.generate_key(app_settings.SHARED_SECRET)
         self.assertEqual(key, expected)
@@ -173,11 +177,15 @@ def test_generate_consistent_key_mac_address(self):
     @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', True)
     @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_ENABLED', True)
     def test_generate_consistent_key_mac_hardware_id(self):
-        d = self.device_model(name='test_generate_key',
-                              mac_address='00:11:22:33:44:55',
-                              hardware_id='1234')
+        d = self.device_model(
+            name='test_generate_key',
+            mac_address='00:11:22:33:44:55',
+            hardware_id='1234',
+        )
         self.assertIsNone(d.key)
-        string = '{}+{}'.format(d.hardware_id, app_settings.SHARED_SECRET).encode('utf-8')
+        string = '{}+{}'.format(d.hardware_id, app_settings.SHARED_SECRET).encode(
+            'utf-8'
+        )
         expected = md5(string).hexdigest()
         key = d.generate_key(app_settings.SHARED_SECRET)
         self.assertEqual(key, expected)
diff --git a/django_netjsonconfig/tests/test_tag.py b/django_netjsonconfig/tests/test_tag.py
index 1e0418c..0c59a1b 100644
--- a/django_netjsonconfig/tests/test_tag.py
+++ b/django_netjsonconfig/tests/test_tag.py
@@ -8,6 +8,7 @@ class TestTag(CreateTemplateMixin, TestCase):
     """
     tests for Tag model
     """
+
     template_model = Template
 
     def test_tag(self):
diff --git a/django_netjsonconfig/tests/test_template.py b/django_netjsonconfig/tests/test_template.py
index ba935bf..3ffca88 100644
--- a/django_netjsonconfig/tests/test_template.py
+++ b/django_netjsonconfig/tests/test_template.py
@@ -12,11 +12,11 @@
 from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin
 
 
-class TestTemplate(CreateConfigMixin, CreateTemplateMixin,
-                   TestVpnX509Mixin, TestCase):
+class TestTemplate(CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin, TestCase):
     """
     tests for Template model
     """
+
     ca_model = Ca
     config_model = Config
     device_model = Device
@@ -63,9 +63,7 @@ def test_config_status_modified_after_change(self):
             t.save()
             c.refresh_from_db()
             handler.assert_called_once_with(
-                sender=Config,
-                signal=config_status_changed,
-                instance=c,
+                sender=Config, signal=config_status_changed, instance=c,
             )
             self.assertEqual(c.status, 'modified')
 
@@ -89,9 +87,7 @@ def test_config_status_modified_after_template_added(self):
             c.templates.add(t)
             c.refresh_from_db()
             handler.assert_called_once_with(
-                sender=Config,
-                signal=config_status_changed,
-                instance=c,
+                sender=Config, signal=config_status_changed, instance=c,
             )
 
     def test_config_modified_signal_always_sent(self):
@@ -106,7 +102,7 @@ def test_config_modified_signal_always_sent(self):
                 signal=config_modified,
                 instance=c,
                 device=c.device,
-                config=c
+                config=c,
             )
 
         c.status = 'applied'
@@ -144,18 +140,20 @@ def test_default_template(self):
         self.assertEqual(c.templates.count(), 0)
         c.device.delete()
         # create default templates for different backends
-        t1 = self._create_template(name='default-openwrt',
-                                   backend='netjsonconfig.OpenWrt',
-                                   default=True)
-        t2 = self._create_template(name='default-openwisp',
-                                   backend='netjsonconfig.OpenWisp',
-                                   default=True)
-        c1 = self._create_config(device=self._create_device(name='test-openwrt'),
-                                 backend='netjsonconfig.OpenWrt')
-        d2 = self._create_device(name='test-openwisp',
-                                 mac_address=self.TEST_MAC_ADDRESS.replace('55', '56'))
-        c2 = self._create_config(device=d2,
-                                 backend='netjsonconfig.OpenWisp')
+        t1 = self._create_template(
+            name='default-openwrt', backend='netjsonconfig.OpenWrt', default=True
+        )
+        t2 = self._create_template(
+            name='default-openwisp', backend='netjsonconfig.OpenWisp', default=True
+        )
+        c1 = self._create_config(
+            device=self._create_device(name='test-openwrt'),
+            backend='netjsonconfig.OpenWrt',
+        )
+        d2 = self._create_device(
+            name='test-openwisp', mac_address=self.TEST_MAC_ADDRESS.replace('55', '56')
+        )
+        c2 = self._create_config(device=d2, backend='netjsonconfig.OpenWisp')
         # ensure OpenWRT device has only the default OpenWRT backend
         self.assertEqual(c1.templates.count(), 1)
         self.assertEqual(c1.templates.first().id, t1.id)
@@ -182,21 +180,17 @@ def test_generic_has_create_cert_false(self):
 
     def test_auto_client_template(self):
         vpn = self._create_vpn()
-        t = self._create_template(name='autoclient',
-                                  type='vpn',
-                                  auto_cert=True,
-                                  vpn=vpn,
-                                  config={})
+        t = self._create_template(
+            name='autoclient', type='vpn', auto_cert=True, vpn=vpn, config={}
+        )
         control = t.vpn.auto_client()
         self.assertDictEqual(t.config, control)
 
     def test_auto_client_template_auto_cert_False(self):
         vpn = self._create_vpn()
-        t = self._create_template(name='autoclient',
-                                  type='vpn',
-                                  auto_cert=False,
-                                  vpn=vpn,
-                                  config={})
+        t = self._create_template(
+            name='autoclient', type='vpn', auto_cert=False, vpn=vpn, config={}
+        )
         vpn = t.config['openvpn'][0]
         self.assertEqual(vpn['cert'], 'cert.pem')
         self.assertEqual(vpn['key'], 'key.pem')
@@ -204,13 +198,17 @@ def test_auto_client_template_auto_cert_False(self):
         self.assertIn('ca_path', t.config['files'][0]['path'])
 
     def test_template_context_var(self):
-        t = self._create_template(config={'files': [
-            {
-                'path': '/etc/vpnserver1',
-                'mode': '0644',
-                'contents': '{{ name }}\n{{ vpnserver1 }}\n'
+        t = self._create_template(
+            config={
+                'files': [
+                    {
+                        'path': '/etc/vpnserver1',
+                        'mode': '0644',
+                        'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                    }
+                ]
             }
-        ]})
+        )
         c = self._create_config()
         c.templates.add(t)
         # clear cache
@@ -231,9 +229,9 @@ def test_get_context(self):
     def test_tamplates_clone(self):
         t = self._create_template(default=True)
         t.save()
-        user = User.objects.create_superuser(username='admin',
-                                             password='tester',
-                                             email='admin@admin.com')
+        user = User.objects.create_superuser(
+            username='admin', password='tester', email='admin@admin.com'
+        )
         c = t.clone(user)
         c.full_clean()
         c.save()
@@ -246,18 +244,20 @@ def test_duplicate_files_in_template(self):
         try:
             self._create_template(
                 name='test-vpn-1',
-                config={'files': [
-                    {
-                        'path': '/etc/vpnserver1',
-                        'mode': '0644',
-                        'contents': '{{ name }}\n{{ vpnserver1 }}\n'
-                    },
-                    {
-                        'path': '/etc/vpnserver1',
-                        'mode': '0644',
-                        'contents': '{{ name }}\n{{ vpnserver1 }}\n'
-                    }
-                ]}
+                config={
+                    'files': [
+                        {
+                            'path': '/etc/vpnserver1',
+                            'mode': '0644',
+                            'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                        },
+                        {
+                            'path': '/etc/vpnserver1',
+                            'mode': '0644',
+                            'contents': '{{ name }}\n{{ vpnserver1 }}\n',
+                        },
+                    ]
+                },
             )
         except ValidationError as e:
             self.assertIn('Invalid configuration triggered by "#/files"', str(e))
diff --git a/django_netjsonconfig/tests/test_views.py b/django_netjsonconfig/tests/test_views.py
index 421c6d2..82cfab9 100644
--- a/django_netjsonconfig/tests/test_views.py
+++ b/django_netjsonconfig/tests/test_views.py
@@ -11,9 +11,9 @@ class TestViews(TestCase):
     """
 
     def setUp(self):
-        User.objects.create_superuser(username='admin',
-                                      password='tester',
-                                      email='admin@admin.com')
+        User.objects.create_superuser(
+            username='admin', password='tester', email='admin@admin.com'
+        )
 
     def test_schema_403(self):
         response = self.client.get(reverse('admin:schema'))
@@ -28,6 +28,7 @@ def test_schema_200(self):
 
     def test_schema_hostname_hidden(self):
         from ..views import available_schemas
+
         for key, schema in available_schemas.items():
             if 'general' not in schema['properties']:
                 continue
diff --git a/django_netjsonconfig/tests/test_vpn.py b/django_netjsonconfig/tests/test_vpn.py
index 9b28c91..e9bcc36 100644
--- a/django_netjsonconfig/tests/test_vpn.py
+++ b/django_netjsonconfig/tests/test_vpn.py
@@ -8,11 +8,11 @@
 from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin
 
 
-class TestVpn(TestVpnX509Mixin, CreateConfigMixin,
-              CreateTemplateMixin, TestCase):
+class TestVpn(TestVpnX509Mixin, CreateConfigMixin, CreateTemplateMixin, TestCase):
     """
     tests for Vpn model
     """
+
     maxDiff = None
     ca_model = Ca
     config_model = Config
@@ -21,12 +21,14 @@ class TestVpn(TestVpnX509Mixin, CreateConfigMixin,
     vpn_model = Vpn
 
     def test_config_not_none(self):
-        v = Vpn(name='test',
-                host='vpn1.test.com',
-                ca=self._create_ca(),
-                backend='django_netjsonconfig.vpn_backends.OpenVpn',
-                config=None,
-                dh=self._dh)
+        v = Vpn(
+            name='test',
+            host='vpn1.test.com',
+            ca=self._create_ca(),
+            backend='django_netjsonconfig.vpn_backends.OpenVpn',
+            config=None,
+            dh=self._dh,
+        )
         try:
             v.full_clean()
         except ValidationError:
@@ -34,27 +36,33 @@ def test_config_not_none(self):
         self.assertEqual(v.config, {})
 
     def test_backend_class(self):
-        v = Vpn(name='test',
-                host='vpn1.test.com',
-                ca=self._create_ca(),
-                backend='django_netjsonconfig.vpn_backends.OpenVpn')
+        v = Vpn(
+            name='test',
+            host='vpn1.test.com',
+            ca=self._create_ca(),
+            backend='django_netjsonconfig.vpn_backends.OpenVpn',
+        )
         self.assertIs(v.backend_class, OpenVpn)
 
     def test_backend_instance(self):
-        v = Vpn(name='test',
-                host='vpn1.test.com',
-                ca=self._create_ca(),
-                backend='django_netjsonconfig.vpn_backends.OpenVpn',
-                config={})
+        v = Vpn(
+            name='test',
+            host='vpn1.test.com',
+            ca=self._create_ca(),
+            backend='django_netjsonconfig.vpn_backends.OpenVpn',
+            config={},
+        )
         self.assertIsInstance(v.backend_instance, OpenVpn)
 
     def test_validation(self):
         config = {'openvpn': {'invalid': True}}
-        v = Vpn(name='test',
-                host='vpn1.test.com',
-                ca=self._create_ca(),
-                backend='django_netjsonconfig.vpn_backends.OpenVpn',
-                config=config)
+        v = Vpn(
+            name='test',
+            host='vpn1.test.com',
+            ca=self._create_ca(),
+            backend='django_netjsonconfig.vpn_backends.OpenVpn',
+            config=config,
+        )
         # ensure django ValidationError is raised
         with self.assertRaises(ValidationError):
             v.full_clean()
@@ -67,11 +75,7 @@ def test_automatic_cert_creation(self):
         vpn = self._create_vpn()
         self.assertIsNotNone(vpn.cert)
         server_extensions = [
-            {
-                "name": "nsCertType",
-                "value": "server",
-                "critical": False
-            }
+            {"name": "nsCertType", "value": "server", "critical": False}
         ]
         self.assertEqual(vpn.cert.extensions, server_extensions)
 
@@ -86,17 +90,15 @@ def test_vpn_client_unique_together(self):
         try:
             client.full_clean()
         except ValidationError as e:
-            self.assertIn('with this Config and Vpn already exists',
-                          e.message_dict['__all__'][0])
+            self.assertIn(
+                'with this Config and Vpn already exists', e.message_dict['__all__'][0]
+            )
         else:
             self.fail('unique_together clause not triggered')
 
     def test_vpn_client_auto_cert_deletes_cert(self):
         vpn = self._create_vpn()
-        t = self._create_template(name='vpn-test',
-                                  type='vpn',
-                                  vpn=vpn,
-                                  auto_cert=True)
+        t = self._create_template(name='vpn-test', type='vpn', vpn=vpn, auto_cert=True)
         c = self._create_config()
         c.templates.add(t)
         vpnclient = c.vpnclient_set.first()
@@ -109,30 +111,33 @@ def test_vpn_client_auto_cert_deletes_cert(self):
     def test_vpn_cert_and_ca_mismatch(self):
         ca = self._create_ca()
         different_ca = self._create_ca()
-        cert = Cert(name='test-cert-vpn',
-                    ca=ca,
-                    key_length='2048',
-                    digest='sha256',
-                    country_code='IT',
-                    state='RM',
-                    city='Rome',
-                    organization_name='OpenWISP',
-                    email='test@test.com',
-                    common_name='openwisp.org')
+        cert = Cert(
+            name='test-cert-vpn',
+            ca=ca,
+            key_length='2048',
+            digest='sha256',
+            country_code='IT',
+            state='RM',
+            city='Rome',
+            organization_name='OpenWISP',
+            email='test@test.com',
+            common_name='openwisp.org',
+        )
         cert.full_clean()
         cert.save()
-        vpn = Vpn(name='test',
-                  host='vpn1.test.com',
-                  ca=different_ca,
-                  cert=cert,
-                  backend='django_netjsonconfig.vpn_backends.OpenVpn')
+        vpn = Vpn(
+            name='test',
+            host='vpn1.test.com',
+            ca=different_ca,
+            cert=cert,
+            backend='django_netjsonconfig.vpn_backends.OpenVpn',
+        )
         try:
             vpn.full_clean()
         except ValidationError as e:
             self.assertIn('cert', e.message_dict)
         else:
-            self.fail('Mismatch between ca and cert but '
-                      'ValidationError not raised')
+            self.fail('Mismatch between ca and cert but ' 'ValidationError not raised')
 
     def test_auto_client(self):
         vpn = self._create_vpn()
@@ -140,25 +145,25 @@ def test_auto_client(self):
         context_keys = vpn._get_auto_context_keys()
         for key in context_keys.keys():
             context_keys[key] = '{{%s}}' % context_keys[key]
-        control = vpn.backend_class.auto_client(host=vpn.host,
-                                                server=self._vpn_config['openvpn'][0],
-                                                **context_keys)
+        control = vpn.backend_class.auto_client(
+            host=vpn.host, server=self._vpn_config['openvpn'][0], **context_keys
+        )
         control['files'] = [
             {
                 'path': context_keys['ca_path'],
                 'mode': '0600',
-                'contents': context_keys['ca_contents']
+                'contents': context_keys['ca_contents'],
             },
             {
                 'path': context_keys['cert_path'],
                 'mode': '0600',
-                'contents': context_keys['cert_contents']
+                'contents': context_keys['cert_contents'],
             },
             {
                 'path': context_keys['key_path'],
                 'mode': '0600',
-                'contents': context_keys['key_contents']
-            }
+                'contents': context_keys['key_contents'],
+            },
         ]
         self.assertDictEqual(auto, control)
 
@@ -170,14 +175,14 @@ def test_auto_client_auto_cert_False(self):
             context_keys[key] = '{{%s}}' % context_keys[key]
         for key in ['cert_path', 'cert_contents', 'key_path', 'key_contents']:
             del context_keys[key]
-        control = vpn.backend_class.auto_client(host=vpn.host,
-                                                server=self._vpn_config['openvpn'][0],
-                                                **context_keys)
+        control = vpn.backend_class.auto_client(
+            host=vpn.host, server=self._vpn_config['openvpn'][0], **context_keys
+        )
         control['files'] = [
             {
                 'path': context_keys['ca_path'],
                 'mode': '0600',
-                'contents': context_keys['ca_contents']
+                'contents': context_keys['ca_contents'],
             }
         ]
         self.assertDictEqual(auto, control)
@@ -187,7 +192,9 @@ def test_vpn_client_get_common_name(self):
         d = self._create_device()
         c = self._create_config(device=d)
         client = VpnClient(vpn=vpn, config=c, auto_cert=True)
-        self.assertEqual(client._get_common_name(), '{mac_address}-{name}'.format(**d.__dict__))
+        self.assertEqual(
+            client._get_common_name(), '{mac_address}-{name}'.format(**d.__dict__)
+        )
         d.name = d.mac_address
         self.assertEqual(client._get_common_name(), d.mac_address)
 
@@ -211,7 +218,7 @@ def test_get_context(self):
             'ca': v.ca.certificate,
             'cert': v.cert.certificate,
             'key': v.cert.private_key,
-            'dh': v.dh
+            'dh': v.dh,
         }
         expected.update(settings.NETJSONCONFIG_CONTEXT)
         self.assertEqual(v.get_context(), expected)
diff --git a/django_netjsonconfig/utils.py b/django_netjsonconfig/utils.py
index 944f714..6f69199 100644
--- a/django_netjsonconfig/utils.py
+++ b/django_netjsonconfig/utils.py
@@ -24,6 +24,7 @@ class ControllerResponse(HttpResponse):
     """
     extends ``django.http.HttpResponse`` by adding a custom HTTP header
     """
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self['X-Openwisp-Controller'] = 'true'
@@ -44,8 +45,9 @@ def send_device_config(config, request):
     which includes the configuration tar.gz as attachment
     """
     update_last_ip(config.device, request)
-    return send_file(filename='{0}.tar.gz'.format(config.name),
-                     contents=config.generate().getvalue())
+    return send_file(
+        filename='{0}.tar.gz'.format(config.name), contents=config.generate().getvalue()
+    )
 
 
 def send_vpn_config(vpn, request):
@@ -53,8 +55,9 @@ def send_vpn_config(vpn, request):
     returns a ``ControllerResponse``which includes the configuration
     tar.gz as attachment
     """
-    return send_file(filename='{0}.tar.gz'.format(vpn.name),
-                     contents=vpn.generate().getvalue())
+    return send_file(
+        filename='{0}.tar.gz'.format(vpn.name), contents=vpn.generate().getvalue()
+    )
 
 
 def update_last_ip(device, request):
@@ -108,42 +111,66 @@ def get_controller_urls(views_module):
     used by third party apps to reduce boilerplate
     """
     urls = [
-        url(r'^controller/device/checksum/(?P[^/]+)/$',
+        url(
+            r'^controller/device/checksum/(?P[^/]+)/$',
             views_module.device_checksum,
-            name='device_checksum'),
-        url(r'^controller/device/download-config/(?P[^/]+)/$',
+            name='device_checksum',
+        ),
+        url(
+            r'^controller/device/download-config/(?P[^/]+)/$',
             views_module.device_download_config,
-            name='device_download_config'),
-        url(r'^controller/device/update-info/(?P[^/]+)/$',
+            name='device_download_config',
+        ),
+        url(
+            r'^controller/device/update-info/(?P[^/]+)/$',
             views_module.device_update_info,
-            name='device_update_info'),
-        url(r'^controller/device/report-status/(?P[^/]+)/$',
+            name='device_update_info',
+        ),
+        url(
+            r'^controller/device/report-status/(?P[^/]+)/$',
             views_module.device_report_status,
-            name='device_report_status'),
-        url(r'^controller/device/register/$',
+            name='device_report_status',
+        ),
+        url(
+            r'^controller/device/register/$',
             views_module.device_register,
-            name='device_register'),
-        url(r'^controller/vpn/checksum/(?P[^/]+)/$',
+            name='device_register',
+        ),
+        url(
+            r'^controller/vpn/checksum/(?P[^/]+)/$',
             views_module.vpn_checksum,
-            name='vpn_checksum'),
-        url(r'^controller/vpn/download-config/(?P[^/]+)/$',
+            name='vpn_checksum',
+        ),
+        url(
+            r'^controller/vpn/download-config/(?P[^/]+)/$',
             views_module.vpn_download_config,
-            name='vpn_download_config'),
+            name='vpn_download_config',
+        ),
         # legacy URLs
-        url(r'^controller/checksum/(?P[^/]+)/$',
+        url(
+            r'^controller/checksum/(?P[^/]+)/$',
             views_module.device_checksum,
-            name='checksum_legacy'),
-        url(r'^controller/download-config/(?P[^/]+)/$',
+            name='checksum_legacy',
+        ),
+        url(
+            r'^controller/download-config/(?P[^/]+)/$',
             views_module.device_download_config,
-            name='download_config_legacy'),
-        url(r'^controller/update-info/(?P[^/]+)/$',
+            name='download_config_legacy',
+        ),
+        url(
+            r'^controller/update-info/(?P[^/]+)/$',
             views_module.device_update_info,
-            name='update_info_legacy'),
-        url(r'^controller/report-status/(?P[^/]+)/$',
+            name='update_info_legacy',
+        ),
+        url(
+            r'^controller/report-status/(?P[^/]+)/$',
             views_module.device_report_status,
-            name='report_status_legacy'),
-        url(r'^controller/register/$',
+            name='report_status_legacy',
+        ),
+        url(
+            r'^controller/register/$',
             views_module.device_register,
-            name='register_legacy'),
+            name='register_legacy',
+        ),
     ]
     return urls
diff --git a/django_netjsonconfig/validators.py b/django_netjsonconfig/validators.py
index 8cfb1fb..d627115 100644
--- a/django_netjsonconfig/validators.py
+++ b/django_netjsonconfig/validators.py
@@ -15,11 +15,13 @@
 )
 
 # device name must either be a hostname or a valid mac address
-hostname_regex = '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}' \
-                 '[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9]' \
-                 '[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$'
+hostname_regex = (
+    '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}'
+    '[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9]'
+    '[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$'
+)
 device_name_validator = RegexValidator(
     _lazy_re_compile('{0}|{1}'.format(hostname_regex, mac_address_regex)),
     message=_('Must be either a valid hostname or mac address.'),
-    code='invalid'
+    code='invalid',
 )
diff --git a/django_netjsonconfig/vpn_backends.py b/django_netjsonconfig/vpn_backends.py
index 1a10c4a..9fe002e 100644
--- a/django_netjsonconfig/vpn_backends.py
+++ b/django_netjsonconfig/vpn_backends.py
@@ -4,19 +4,19 @@
 
 # adapt OpenVPN schema in order to limit it to 1 item only
 limited_schema = deepcopy(BaseOpenVpn.schema)
-limited_schema['properties']['openvpn'].update({
-    "additionalItems": False,
-    "minItems": 1,
-    "maxItems": 1
-})
+limited_schema['properties']['openvpn'].update(
+    {"additionalItems": False, "minItems": 1, "maxItems": 1}
+)
 # server mode only
-limited_schema['properties']['openvpn']['items'].update({
-    "oneOf": [
-        {"$ref": "#/definitions/server_bridged"},
-        {"$ref": "#/definitions/server_routed"},
-        {"$ref": "#/definitions/server_manual"}
-    ]
-})
+limited_schema['properties']['openvpn']['items'].update(
+    {
+        "oneOf": [
+            {"$ref": "#/definitions/server_bridged"},
+            {"$ref": "#/definitions/server_routed"},
+            {"$ref": "#/definitions/server_manual"},
+        ]
+    }
+)
 
 # default values for ca, cert and key
 limited_schema['definitions']['tunnel']['properties']['ca']['default'] = 'ca.pem'
@@ -24,26 +24,10 @@
 limited_schema['definitions']['tunnel']['properties']['key']['default'] = 'key.pem'
 limited_schema['definitions']['server']['properties']['dh']['default'] = 'dh.pem'
 limited_schema['properties']['files']['default'] = [
-    {
-        "path": "ca.pem",
-        "mode": "0600",
-        "contents": "{{ ca }}"
-    },
-    {
-        "path": "cert.pem",
-        "mode": "0600",
-        "contents": "{{ cert }}"
-    },
-    {
-        "path": "key.pem",
-        "mode": "0600",
-        "contents": "{{ key }}"
-    },
-    {
-        "path": "dh.pem",
-        "mode": "0600",
-        "contents": "{{ dh }}"
-    }
+    {"path": "ca.pem", "mode": "0600", "contents": "{{ ca }}"},
+    {"path": "cert.pem", "mode": "0600", "contents": "{{ cert }}"},
+    {"path": "key.pem", "mode": "0600", "contents": "{{ key }}"},
+    {"path": "dh.pem", "mode": "0600", "contents": "{{ dh }}"},
 ]
 
 
@@ -55,4 +39,5 @@ class OpenVpn(BaseOpenVpn):
         * allows only 1 vpn
         * adds default values for ca, cert, key and dh
     """
+
     schema = limited_schema
diff --git a/django_netjsonconfig/widgets.py b/django_netjsonconfig/widgets.py
index e69a338..5fa40a6 100644
--- a/django_netjsonconfig/widgets.py
+++ b/django_netjsonconfig/widgets.py
@@ -9,17 +9,25 @@ class JsonSchemaWidget(AdminTextareaWidget):
     """
     JSON Schema Editor widget
     """
+
     @property
     def media(self):
         prefix = 'django-netjsonconfig'
-        js = [static('{0}/js/{1}'.format(prefix, f))
-              for f in ('lib/advanced-mode.js',
-                        'lib/tomorrow_night_bright.js',
-                        'lib/jsonschema-ui.js',
-                        'widget.js')]
-        css = {'all': [static('{0}/css/{1}'.format(prefix, f))
-                       for f in ('lib/jsonschema-ui.css',
-                                 'lib/advanced-mode.css')]}
+        js = [
+            static('{0}/js/{1}'.format(prefix, f))
+            for f in (
+                'lib/advanced-mode.js',
+                'lib/tomorrow_night_bright.js',
+                'lib/jsonschema-ui.js',
+                'widget.js',
+            )
+        ]
+        css = {
+            'all': [
+                static('{0}/css/{1}'.format(prefix, f))
+                for f in ('lib/jsonschema-ui.css', 'lib/advanced-mode.css')
+            ]
+        }
         return forms.Media(js=js, css=css)
 
     def render(self, name, value, attrs={}, renderer=None):
@@ -33,7 +41,6 @@ def render(self, name, value, attrs={}, renderer=None):
        target="_blank">netjsonconfig documentation.
 
 """
-        html = html.format(_('Advanced mode (raw JSON)'),
-                           reverse('admin:schema'))
+        html = html.format(_('Advanced mode (raw JSON)'), reverse('admin:schema'))
         html += super().render(name, value, attrs, renderer)
         return html
diff --git a/requirements-test.txt b/requirements-test.txt
index dd7240b..0b3938a 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,2 +1,2 @@
 coveralls
-openwisp-utils[qa]>=0.4.1
+openwisp-utils[qa]>=0.5.0
diff --git a/requirements.txt b/requirements.txt
index ebf4256..276ee34 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,4 +5,4 @@ django-sortedm2m>=3.0.0,<3.1.0
 django-reversion>=3.0.5,<3.1.0
 django-x509>=0.6.2,<0.7.0
 django-taggit>=0.24.0,<1.3.0
-openwisp-utils>=0.4.4,<0.5.0
+openwisp-utils>=0.5.0,<0.6.0
diff --git a/run-qa-checks b/run-qa-checks
new file mode 100755
index 0000000..e9091f9
--- /dev/null
+++ b/run-qa-checks
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+openwisp-qa-check \
+  --migration-path ./django_netjsonconfig/migrations \
+  --migration-module django_netjsonconfig
+
+jslint django_netjsonconfig/static/django-netjsonconfig/js/*.js
diff --git a/runtests.py b/runtests.py
index 2af7ecb..a032c6d 100755
--- a/runtests.py
+++ b/runtests.py
@@ -9,6 +9,7 @@
 
 if __name__ == "__main__":
     from django.core.management import execute_from_command_line
+
     args = sys.argv
     args.insert(1, "test")
     args.insert(2, "django_netjsonconfig")
diff --git a/setup.cfg b/setup.cfg
index 42fc402..eefb17d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,13 +1,6 @@
 [bdist_wheel]
 universal=1
 
-[isort]
-known_third_party = django
-known_first_party = netjsonconfig,openwisp_utils
-line_length=110
-default_section = THIRDPARTY
-skip = migrations
-
 [flake8]
 exclude = *migrations*,
           ./tests/*settings*.py
@@ -16,3 +9,14 @@ max-line-length = 110
 # W504: line break after or after operator
 # W605: invalid escape sequence
 ignore = W605, W503, W504
+
+[isort]
+known_third_party = django
+known_first_party = netjsonconfig,openwisp_utils
+line_length=88
+default_section = THIRDPARTY
+skip = migrations
+multi_line_output=3
+use_parentheses=True
+include_trailing_comma=True
+force_grid_wrap=0
diff --git a/setup.py b/setup.py
index 1483f4a..15127e3 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,12 @@ def get_install_requires():
     requirements = []
     for line in open('requirements.txt').readlines():
         # skip to next iteration if comment or empty line
-        if line.startswith('#') or line == '' or line.startswith('http') or line.startswith('git'):
+        if (
+            line.startswith('#')
+            or line == ''
+            or line.startswith('http')
+            or line.startswith('git')
+        ):
             continue
         # add line to requirements
         requirements.append(line)
@@ -59,5 +64,5 @@ def get_install_requires():
         'Framework :: Django',
         'Topic :: System :: Networking',
         'Programming Language :: Python :: 3',
-    ]
+    ],
 )
diff --git a/tests/local_settings.example.py b/tests/local_settings.example.py
index 602f896..949008b 100644
--- a/tests/local_settings.example.py
+++ b/tests/local_settings.example.py
@@ -1,7 +1,7 @@
 # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS
 # BUT DO NOT COMMIT
 
-#DATABASES = {
+# DATABASES = {
 #    'default': {
 #        'ENGINE': 'django.db.backends.sqlite3',
 #        'NAME': 'netjsonconfig.db',
@@ -10,4 +10,4 @@
 #        'HOST': '',
 #        'PORT': ''
 #    },
-#}
+# }
diff --git a/tests/settings.py b/tests/settings.py
index 4f5d688..ba9232e 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -26,7 +26,7 @@
     'django.contrib.admin',
     'sortedm2m',
     'reversion',
-    'django_x509'
+    'django_x509',
 ]
 
 STATICFILES_FINDERS = [
@@ -72,9 +72,7 @@
 ]
 
 NETJSONCONFIG_SHARED_SECRET = 't3st1ng'
-NETJSONCONFIG_CONTEXT = {
-    'vpnserver1': 'vpn.testdomain.com'
-}
+NETJSONCONFIG_CONTEXT = {'vpnserver1': 'vpn.testdomain.com'}
 
 NETJSONCONFIG_HARDWARE_ID_ENABLED = True
 
diff --git a/tests/urls.py b/tests/urls.py
index 5fd9e4c..f630d78 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -16,4 +16,5 @@
 
 if 'debug_toolbar' in settings.INSTALLED_APPS and settings.DEBUG:
     import debug_toolbar
+
     urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))]