├── .coveragerc ├── .gitignore ├── .jslintrc ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_netjsonconfig ├── __init__.py ├── admin.py ├── apps.py ├── base │ ├── __init__.py │ ├── admin.py │ ├── base.py │ ├── config.py │ ├── device.py │ ├── tag.py │ ├── template.py │ └── vpn.py ├── controller │ ├── __init__.py │ ├── generics.py │ ├── urls.py │ └── views.py ├── fixtures │ └── test_templates.json ├── migrations │ ├── 0001_initial.py │ ├── 0002_config_status.py │ ├── 0003_config_last_ip.py │ ├── 0004_config_allow_blank.py │ ├── 0005_template_default.py │ ├── 0006_utc_data_migration.py │ ├── 0007_file_data_migration.py │ ├── 0008_vpn_integration.py │ ├── 0009_openvpn_data_migration.py │ ├── 0010_basemodel_reorganization.py │ ├── 0011_template_config_blank.py │ ├── 0012_name_unique.py │ ├── 0013_config_mac_address.py │ ├── 0014_randomize_mac_address.py │ ├── 0015_config_mac_address_unique.py │ ├── 0016_vpn_dh.py │ ├── 0017_openvpn_data_migration.py │ ├── 0018_openvpn_disabled_attr.py │ ├── 0019_cleanup_model_options.py │ ├── 0020_openvpn_resolv_retry.py │ ├── 0021_netjsonconfig_label.py │ ├── 0022_update_model_labels.py │ ├── 0023_template_tags.py │ ├── 0024_add_device_model.py │ ├── 0025_populate_device.py │ ├── 0026_config_device_not_null.py │ ├── 0027_simplify_config.py │ ├── 0028_device_indexes.py │ ├── 0029_explicit_indexes.py │ ├── 0030_device_system.py │ ├── 0031_updated_mac_address_validator.py │ ├── 0032_internal_notes_label.py │ ├── 0033_migrate_last_ip.py │ ├── 0034_device_management_ip.py │ ├── 0035_renamed_status_choices.py │ ├── 0036_device_hardware_id.py │ ├── 0037_config_context.py │ ├── 0038_vpn_key.py │ ├── 0039_vpn_format_dh.py │ ├── 0040_update_context.py │ ├── 0041_update_context_data.py │ ├── 0042_device_key_none.py │ ├── 0043_add_indexes_on_ip_fields.py │ ├── 0044_template_default_values.py │ └── __init__.py ├── models.py ├── settings.py ├── signals.py ├── static │ └── django-netjsonconfig │ │ ├── css │ │ ├── admin.css │ │ └── lib │ │ │ ├── advanced-mode.css │ │ │ ├── img │ │ │ └── jsoneditor-icons.svg │ │ │ └── jsonschema-ui.css │ │ └── js │ │ ├── lib │ │ ├── advanced-mode.js │ │ ├── jsonschema-ui.js │ │ ├── tomorrow_night_bright.js │ │ └── utils.js │ │ ├── preview.js │ │ ├── switcher.js │ │ ├── tabs.js │ │ ├── unsaved_changes.js │ │ └── widget.js ├── templates │ └── admin │ │ └── django_netjsonconfig │ │ ├── change_form.html │ │ └── preview.html ├── tests │ ├── __init__.py │ ├── test_admin.py │ ├── test_config.py │ ├── test_controller.py │ ├── test_device.py │ ├── test_tag.py │ ├── test_template.py │ ├── test_views.py │ └── test_vpn.py ├── urls.py ├── utils.py ├── validators.py ├── views.py ├── vpn_backends.py └── widgets.py ├── docs └── images │ ├── adhoc-interface.png │ ├── bridge.png │ ├── configuration-ui.png │ ├── device-advanced.png │ ├── device-context.png │ ├── preview.png │ ├── radio.png │ ├── template-advanced.png │ ├── template-default-values.png │ └── wpa-enterprise.png ├── requirements-test.txt ├── requirements.txt ├── run-qa-checks ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── local_settings.example.py ├── manage.py ├── settings.py └── urls.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | /*/test* 4 | /tests 5 | /*/__init__.py 6 | /setup.py 7 | /*/migrations/* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # PyInstaller 24 | # Usually these files are written by a python script from a template 25 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 26 | *.manifest 27 | *.spec 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .coverage 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | 41 | # Translations 42 | *.mo 43 | *.pot 44 | 45 | # Django stuff: 46 | *.log 47 | 48 | # Sphinx documentation 49 | docs/_build/ 50 | 51 | # PyBuilder 52 | target/ 53 | 54 | # editors 55 | *.komodoproject 56 | .vscode/ 57 | 58 | # other 59 | *.DS_Store* 60 | *~ 61 | ._* 62 | local_settings.py 63 | *.db 64 | *.tar.gz 65 | -------------------------------------------------------------------------------- /.jslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "navigator", 5 | "console", 6 | "window", 7 | "django", 8 | "$", 9 | "gettext", 10 | "alert", 11 | "closePreview", 12 | "objectIsEqual", 13 | "editor", 14 | "advancedJSONEditor", 15 | "JSONEditor" 16 | ], 17 | 18 | "browser": true, 19 | "eqeq": true, 20 | "white": true, 21 | "unparam": true, 22 | "undef": true, 23 | "sub": true, 24 | "asi": true, 25 | "sloppy": true, 26 | "vars": true, 27 | "forin": true, 28 | "nomen": true, 29 | "continue": true, 30 | "plusplus": true, 31 | "newcap": true, 32 | "indent": 4, 33 | "maxerr": 10000 34 | } 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | dist: bionic 4 | 5 | python: 6 | - "3.6" 7 | - "3.7" 8 | 9 | env: 10 | - DJANGO="django>=2.2,<3.0" 11 | - DJANGO="django>=3.0,<3.1" 12 | 13 | branches: 14 | only: 15 | - master 16 | - gsoc2019 17 | 18 | before_install: 19 | - pip install -U pip wheel setuptools 20 | - pip install --no-cache-dir -U -r requirements-test.txt 21 | - npm install -g jslint 22 | 23 | install: 24 | - pip install $DJANGO 25 | - python setup.py -q develop 26 | 27 | script: 28 | - ./run-qa-checks 29 | - coverage run --source=django_netjsonconfig runtests.py 30 | 31 | after_success: 32 | coveralls 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Please refer to the `Contribution Guidelines `_. 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | include requirements.txt 5 | recursive-include django_netjsonconfig * 6 | recursive-exclude * *.pyc 7 | recursive-exclude * *.swp 8 | recursive-exclude * __pycache__ 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-netjsonconfig 2 | ==================== 3 | 4 | **WARNING**: The development of this project has moved to `openwisp-controller `_, we advise all users of django-netjsonconfig to migrate. 5 | 6 | **The development of django-netjsonconfig is discontinued and this repository is archived.** 7 | -------------------------------------------------------------------------------- /django_netjsonconfig/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 12, 0, 'final') 2 | __version__ = VERSION # alias 3 | 4 | 5 | def get_version(): 6 | version = '%s.%s' % (VERSION[0], VERSION[1]) 7 | if VERSION[2]: 8 | version = '%s.%s' % (version, VERSION[2]) 9 | if VERSION[3:] == ('alpha', 0): 10 | version = '%s pre-alpha' % version 11 | else: 12 | if VERSION[3] != 'final': 13 | try: 14 | rev = VERSION[4] 15 | except IndexError: 16 | rev = 0 17 | version = '%s%s%s' % (version, VERSION[3][0:1], rev) 18 | return version 19 | 20 | 21 | default_app_config = 'django_netjsonconfig.apps.DjangoNetjsonconfigApp' 22 | -------------------------------------------------------------------------------- /django_netjsonconfig/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .base.admin import ( 4 | AbstractConfigForm, 5 | AbstractConfigInline, 6 | AbstractDeviceAdmin, 7 | AbstractTemplateAdmin, 8 | AbstractVpnAdmin, 9 | AbstractVpnForm, 10 | BaseForm, 11 | ) 12 | from .models import Config, Device, Template, Vpn 13 | 14 | 15 | class ConfigForm(AbstractConfigForm): 16 | class Meta(AbstractConfigForm.Meta): 17 | model = Config 18 | 19 | 20 | class TemplateForm(BaseForm): 21 | class Meta(BaseForm.Meta): 22 | model = Template 23 | 24 | 25 | class TemplateAdmin(AbstractTemplateAdmin): 26 | form = TemplateForm 27 | 28 | 29 | class VpnForm(AbstractVpnForm): 30 | class Meta(AbstractVpnForm.Meta): 31 | model = Vpn 32 | 33 | 34 | class VpnAdmin(AbstractVpnAdmin): 35 | form = VpnForm 36 | 37 | 38 | class ConfigInline(AbstractConfigInline): 39 | model = Config 40 | form = ConfigForm 41 | extra = 0 42 | 43 | 44 | class DeviceAdmin(AbstractDeviceAdmin): 45 | inlines = [ConfigInline] 46 | 47 | 48 | admin.site.register(Device, DeviceAdmin) 49 | admin.site.register(Template, TemplateAdmin) 50 | admin.site.register(Vpn, VpnAdmin) 51 | -------------------------------------------------------------------------------- /django_netjsonconfig/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.conf import settings 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.db.models.signals import m2m_changed, post_delete 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from .settings import REGISTRATION_ENABLED, SHARED_SECRET 8 | 9 | 10 | class DjangoNetjsonconfigApp(AppConfig): 11 | name = 'django_netjsonconfig' 12 | label = 'django_netjsonconfig' 13 | verbose_name = _('Network Configuration') 14 | 15 | def __setmodels__(self): 16 | """ 17 | This method allows third party apps to set their own custom models 18 | """ 19 | from .models import Config, VpnClient 20 | 21 | self.config_model = Config 22 | self.vpnclient_model = VpnClient 23 | 24 | def connect_signals(self): 25 | """ 26 | * m2m validation before templates are added/removed to a config 27 | * automatic vpn client management on m2m_changed 28 | * automatic vpn client removal 29 | """ 30 | m2m_changed.connect( 31 | self.config_model.clean_templates, 32 | sender=self.config_model.templates.through, 33 | ) 34 | m2m_changed.connect( 35 | self.config_model.templates_changed, 36 | sender=self.config_model.templates.through, 37 | ) 38 | m2m_changed.connect( 39 | self.config_model.manage_vpn_clients, 40 | sender=self.config_model.templates.through, 41 | ) 42 | post_delete.connect( 43 | self.vpnclient_model.post_delete, sender=self.vpnclient_model 44 | ) 45 | 46 | def check_settings(self): 47 | if ( 48 | settings.DEBUG is False and REGISTRATION_ENABLED and not SHARED_SECRET 49 | ): # pragma: nocover 50 | raise ImproperlyConfigured( 51 | 'Security error: NETJSONCONFIG_SHARED_SECRET is not set. ' 52 | 'Please set it or disable auto-registration by setting ' 53 | 'NETJSONCONFIG_REGISTRATION_ENABLED to False' 54 | ) 55 | 56 | def ready(self): 57 | self.__setmodels__() 58 | self.check_settings() 59 | self.connect_signals() 60 | -------------------------------------------------------------------------------- /django_netjsonconfig/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/django_netjsonconfig/base/__init__.py -------------------------------------------------------------------------------- /django_netjsonconfig/base/base.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import hashlib 3 | import json 4 | from copy import deepcopy 5 | 6 | from django.core.exceptions import ValidationError 7 | from django.db import models 8 | from django.utils.functional import cached_property 9 | from django.utils.module_loading import import_string 10 | from django.utils.translation import ugettext_lazy as _ 11 | from jsonfield import JSONField 12 | 13 | from netjsonconfig.exceptions import ValidationError as SchemaError 14 | from openwisp_utils.base import TimeStampedEditableModel 15 | 16 | from .. import settings as app_settings 17 | 18 | 19 | class BaseModel(TimeStampedEditableModel): 20 | """ 21 | Shared logic 22 | """ 23 | 24 | name = models.CharField(max_length=64, unique=True, db_index=True) 25 | 26 | class Meta: 27 | abstract = True 28 | 29 | def __str__(self): 30 | return self.name 31 | 32 | 33 | class BaseConfig(BaseModel): 34 | """ 35 | Base configuration management model logic shared between models 36 | """ 37 | 38 | backend = models.CharField( 39 | _('backend'), 40 | choices=app_settings.BACKENDS, 41 | max_length=128, 42 | help_text=_( 43 | 'Select netjsonconfig backend' 45 | ), 46 | ) 47 | config = JSONField( 48 | _('configuration'), 49 | default=dict, 50 | help_text=_('configuration in NetJSON DeviceConfiguration format'), 51 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 52 | dump_kwargs={'indent': 4}, 53 | ) 54 | 55 | __template__ = False 56 | __vpn__ = False 57 | 58 | class Meta: 59 | abstract = True 60 | 61 | def clean(self): 62 | """ 63 | * ensures config is not ``None`` 64 | * performs netjsonconfig backend validation 65 | """ 66 | if self.config is None: 67 | self.config = {} 68 | if not isinstance(self.config, dict): 69 | raise ValidationError({'config': _('Unexpected configuration format.')}) 70 | # perform validation only if backend is defined, otherwise 71 | # django will take care of notifying blank field error 72 | if not self.backend: 73 | return 74 | try: 75 | backend = self.backend_instance 76 | except ImportError as e: 77 | message = 'Error while importing "{0}": {1}'.format(self.backend, e) 78 | raise ValidationError({'backend': message}) 79 | else: 80 | self.clean_netjsonconfig_backend(backend) 81 | 82 | def get_config(self): 83 | """ 84 | config preprocessing (skipped for templates): 85 | * inserts hostname automatically if not present in config 86 | """ 87 | config = self.config or {} # might be ``None`` in some corner cases 88 | if self.__template__: 89 | return config 90 | c = deepcopy(config) 91 | is_config = not any([self.__template__, self.__vpn__]) 92 | if 'hostname' not in c.get('general', {}) and is_config: 93 | c.setdefault('general', {}) 94 | c['general']['hostname'] = self.name.replace(':', '-') 95 | return c 96 | 97 | def get_context(self): 98 | return app_settings.CONTEXT 99 | 100 | @classmethod 101 | def validate_netjsonconfig_backend(cls, backend): 102 | """ 103 | calls ``validate`` method of netjsonconfig backend 104 | might trigger SchemaError 105 | """ 106 | # the following line is a trick needed to avoid cluttering 107 | # an eventual ``ValidationError`` message with ``OrderedDict`` 108 | # which would make the error message hard to read 109 | backend.config = json.loads(json.dumps(backend.config)) 110 | backend.validate() 111 | 112 | @classmethod 113 | def clean_netjsonconfig_backend(cls, backend): 114 | """ 115 | catches any ``SchemaError`` which will be redirected 116 | to ``django.core.exceptions.ValdiationError`` 117 | """ 118 | try: 119 | cls.validate_netjsonconfig_backend(backend) 120 | except SchemaError as e: 121 | path = [str(el) for el in e.details.path] 122 | trigger = '/'.join(path) 123 | error = e.details.message 124 | message = ( 125 | 'Invalid configuration triggered by "#/{0}", ' 126 | 'validator says:\n\n{1}'.format(trigger, error) 127 | ) 128 | raise ValidationError(message) 129 | 130 | @cached_property 131 | def backend_class(self): 132 | """ 133 | returns netjsonconfig backend class 134 | """ 135 | return import_string(self.backend) 136 | 137 | @cached_property 138 | def backend_instance(self): 139 | """ 140 | returns netjsonconfig backend instance 141 | """ 142 | return self.get_backend_instance() 143 | 144 | def get_backend_instance(self, template_instances=None): 145 | """ 146 | allows overriding config and templates 147 | needed for pre validation of m2m 148 | """ 149 | backend = self.backend_class 150 | kwargs = {'config': self.get_config()} 151 | context = {} 152 | # determine if we can pass templates 153 | # expecting a many2many relationship 154 | if hasattr(self, 'templates'): 155 | if template_instances is None: 156 | template_instances = self.templates.all() 157 | templates_list = list() 158 | for t in template_instances: 159 | templates_list.append(t.config) 160 | context.update(t.get_context()) 161 | kwargs['templates'] = templates_list 162 | # pass context to backend if get_context method is defined 163 | if hasattr(self, 'get_context'): 164 | context.update(self.get_context()) 165 | kwargs['context'] = context 166 | backend_instance = backend(**kwargs) 167 | # remove accidentally duplicated files when combining config and templates 168 | # this may happen if a device uses multiple VPN client templates 169 | # which share the same Certification Authority, hence the CA 170 | # is defined twice, which would raise ValidationError 171 | if template_instances: 172 | self._remove_duplicated_files(backend_instance) 173 | return backend_instance 174 | 175 | @classmethod 176 | def _remove_duplicated_files(cls, backend_instance): 177 | if 'files' not in backend_instance.config: 178 | return 179 | unique_files = [] 180 | for file in backend_instance.config['files']: 181 | if file not in unique_files: 182 | unique_files.append(file) 183 | backend_instance.config['files'] = unique_files 184 | 185 | def generate(self): 186 | """ 187 | shortcut for self.backend_instance.generate() 188 | """ 189 | return self.backend_instance.generate() 190 | 191 | @property 192 | def checksum(self): 193 | """ 194 | returns checksum of configuration 195 | """ 196 | config = self.generate().getvalue() 197 | return hashlib.md5(config).hexdigest() 198 | 199 | def json(self, dict=False, **kwargs): 200 | """ 201 | returns JSON representation of object 202 | """ 203 | config = self.backend_instance.config 204 | if dict: 205 | return config 206 | return json.dumps(config, **kwargs) 207 | -------------------------------------------------------------------------------- /django_netjsonconfig/base/device.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | from django.db import models 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from openwisp_utils.base import KeyField 7 | 8 | from .. import settings as app_settings 9 | from ..validators import device_name_validator, mac_address_validator 10 | from .base import BaseModel 11 | 12 | 13 | class AbstractDevice(BaseModel): 14 | """ 15 | Base device model 16 | Stores information related to the 17 | physical properties of a network device 18 | """ 19 | 20 | mac_address = models.CharField( 21 | max_length=17, 22 | unique=True, 23 | db_index=True, 24 | validators=[mac_address_validator], 25 | help_text=_('primary mac address'), 26 | ) 27 | key = KeyField( 28 | unique=True, 29 | blank=True, 30 | default=None, 31 | db_index=True, 32 | help_text=_('unique device key'), 33 | ) 34 | model = models.CharField( 35 | max_length=64, 36 | blank=True, 37 | db_index=True, 38 | help_text=_('device model and manufacturer'), 39 | ) 40 | os = models.CharField( 41 | _('operating system'), 42 | blank=True, 43 | db_index=True, 44 | max_length=128, 45 | help_text=_('operating system identifier'), 46 | ) 47 | system = models.CharField( 48 | _('SOC / CPU'), 49 | blank=True, 50 | db_index=True, 51 | max_length=128, 52 | help_text=_('system on chip or CPU info'), 53 | ) 54 | notes = models.TextField(blank=True, help_text=_('internal notes')) 55 | # these fields are filled automatically 56 | # with data received from devices 57 | last_ip = models.GenericIPAddressField( 58 | blank=True, 59 | null=True, 60 | db_index=True, 61 | help_text=_( 62 | 'indicates the IP address logged from ' 63 | 'the last request coming from the device' 64 | ), 65 | ) 66 | management_ip = models.GenericIPAddressField( 67 | blank=True, 68 | null=True, 69 | db_index=True, 70 | help_text=_('ip address of the management interface, ' 'if available'), 71 | ) 72 | hardware_id = models.CharField(**(app_settings.HARDWARE_ID_OPTIONS)) 73 | 74 | class Meta: 75 | abstract = True 76 | 77 | def __str__(self): 78 | return ( 79 | self.hardware_id 80 | if (app_settings.HARDWARE_ID_ENABLED and app_settings.HARDWARE_ID_AS_NAME) 81 | else self.name 82 | ) 83 | 84 | def clean(self): 85 | """ 86 | modifies related config status if name 87 | attribute is changed (queries the database) 88 | """ 89 | super().clean() 90 | if self._state.adding: 91 | return 92 | current = self.__class__.objects.get(pk=self.pk) 93 | if self.name != current.name and self._has_config(): 94 | self.config.set_status_modified() 95 | 96 | def _has_config(self): 97 | return hasattr(self, 'config') 98 | 99 | def _get_config_attr(self, attr): 100 | """ 101 | gets property or calls method of related config object 102 | without rasing an exception if config is not set 103 | """ 104 | if not self._has_config(): 105 | return None 106 | attr = getattr(self.config, attr) 107 | return attr() if callable(attr) else attr 108 | 109 | def get_context(self): 110 | if self._has_config(): 111 | config = self.config 112 | else: 113 | config = self.get_config_model()(device=self) 114 | return config.get_context() 115 | 116 | def generate_key(self, shared_secret): 117 | if app_settings.CONSISTENT_REGISTRATION: 118 | keybase = ( 119 | self.hardware_id 120 | if app_settings.HARDWARE_ID_ENABLED 121 | else self.mac_address 122 | ) 123 | hash = md5('{}+{}'.format(keybase, shared_secret).encode('utf-8')) 124 | return hash.hexdigest() 125 | else: 126 | return KeyField.default_callable() 127 | 128 | def save(self, *args, **kwargs): 129 | if not self.key: 130 | self.key = self.generate_key(app_settings.SHARED_SECRET) 131 | super().save(*args, **kwargs) 132 | 133 | @property 134 | def backend(self): 135 | """ 136 | Used as a shortcut for display purposes 137 | (eg: admin site) 138 | """ 139 | return self._get_config_attr('get_backend_display') 140 | 141 | @property 142 | def status(self): 143 | """ 144 | Used as a shortcut for display purposes 145 | (eg: admin site) 146 | """ 147 | return self._get_config_attr('get_status_display') 148 | 149 | def get_default_templates(self): 150 | """ 151 | calls `get_default_templates` of related 152 | config object (or new config instance) 153 | """ 154 | if self._has_config(): 155 | c = self.config 156 | else: 157 | c = self.get_temp_config_instance() 158 | return c.get_default_templates() 159 | 160 | @classmethod 161 | def get_config_model(cls): 162 | return cls._meta.get_field('config').related_model 163 | 164 | def get_temp_config_instance(self, **options): 165 | return self.get_config_model()(**options) 166 | 167 | 168 | # Create a copy of the validators 169 | # (to avoid modifying parent classes) 170 | # and add device_name_validator 171 | name_field = AbstractDevice._meta.get_field('name') 172 | name_field.validators = name_field.validators[:] + [device_name_validator] 173 | -------------------------------------------------------------------------------- /django_netjsonconfig/base/tag.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | from taggit.models import GenericUUIDTaggedItemBase, TagBase, TaggedItemBase 4 | 5 | from openwisp_utils.base import UUIDModel 6 | 7 | 8 | class AbstractTemplateTag(TagBase, UUIDModel): 9 | class Meta: 10 | abstract = True 11 | verbose_name = _('Tag') 12 | verbose_name_plural = _('Tags') 13 | 14 | 15 | class AbstractTaggedTemplate(GenericUUIDTaggedItemBase, TaggedItemBase): 16 | tag = models.ForeignKey( 17 | 'django_netjsonconfig.TemplateTag', 18 | related_name='%(app_label)s_%(class)s_items', 19 | on_delete=models.CASCADE, 20 | ) 21 | 22 | class Meta: 23 | abstract = True 24 | verbose_name = _('Tagged item') 25 | verbose_name_plural = _('Tags') 26 | -------------------------------------------------------------------------------- /django_netjsonconfig/base/template.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from copy import copy 3 | 4 | from django.contrib.admin.models import ADDITION, LogEntry 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.core.exceptions import ValidationError 7 | from django.db import models 8 | from django.utils.translation import ugettext_lazy as _ 9 | from jsonfield import JSONField 10 | from taggit.managers import TaggableManager 11 | 12 | from ..settings import DEFAULT_AUTO_CERT 13 | from .base import BaseConfig 14 | 15 | TYPE_CHOICES = ( 16 | ('generic', _('Generic')), 17 | ('vpn', _('VPN-client')), 18 | ) 19 | 20 | 21 | def default_auto_cert(): 22 | """ 23 | returns the default value for auto_cert field 24 | (this avoids to set the exact default value in the database migration) 25 | """ 26 | return DEFAULT_AUTO_CERT 27 | 28 | 29 | class AbstractTemplate(BaseConfig): 30 | """ 31 | Abstract model implementing a 32 | netjsonconfig template 33 | """ 34 | 35 | tags = TaggableManager( 36 | through='django_netjsonconfig.TaggedTemplate', 37 | blank=True, 38 | help_text=_( 39 | 'A comma-separated list of template tags, may be used ' 40 | 'to ease auto configuration with specific settings (eg: ' 41 | '4G, mesh, WDS, VPN, ecc.)' 42 | ), 43 | ) 44 | vpn = models.ForeignKey( 45 | 'django_netjsonconfig.Vpn', 46 | verbose_name=_('VPN'), 47 | blank=True, 48 | null=True, 49 | on_delete=models.CASCADE, 50 | ) 51 | type = models.CharField( 52 | _('type'), 53 | max_length=16, 54 | choices=TYPE_CHOICES, 55 | default='generic', 56 | db_index=True, 57 | help_text=_('template type, determines which ' 'features are available'), 58 | ) 59 | default = models.BooleanField( 60 | _('enabled by default'), 61 | default=False, 62 | db_index=True, 63 | help_text=_( 64 | 'whether new configurations will have ' 'this template enabled by default' 65 | ), 66 | ) 67 | auto_cert = models.BooleanField( 68 | _('auto certificate'), 69 | default=default_auto_cert, 70 | db_index=True, 71 | help_text=_( 72 | 'whether x509 client certificates should ' 73 | 'be automatically managed behind the scenes ' 74 | 'for each configuration using this template, ' 75 | 'valid only for the VPN type' 76 | ), 77 | ) 78 | default_values = JSONField( 79 | _('Default Values'), 80 | default=dict, 81 | blank=True, 82 | help_text=_( 83 | 'A dictionary containing the default ' 84 | 'values for the variables used by this ' 85 | 'template; these default variables will ' 86 | 'be used during schema validation.' 87 | ), 88 | load_kwargs={'object_pairs_hook': OrderedDict}, 89 | dump_kwargs={'indent': 4}, 90 | ) 91 | __template__ = True 92 | 93 | class Meta: 94 | abstract = True 95 | verbose_name = _('template') 96 | verbose_name_plural = _('templates') 97 | 98 | def save(self, *args, **kwargs): 99 | """ 100 | modifies status of related configs 101 | if key attributes have changed (queries the database) 102 | """ 103 | update_related_config_status = False 104 | if not self._state.adding: 105 | current = self.__class__.objects.get(pk=self.pk) 106 | for attr in ['backend', 'config']: 107 | if getattr(self, attr) != getattr(current, attr): 108 | update_related_config_status = True 109 | break 110 | # save current changes 111 | super().save(*args, **kwargs) 112 | # update relations 113 | if update_related_config_status: 114 | self._update_related_config_status() 115 | 116 | def _update_related_config_status(self): 117 | changing_status = list(self.config_relations.exclude(status='modified')) 118 | self.config_relations.update(status='modified') 119 | for config in self.config_relations.all(): 120 | # config modified signal sent regardless 121 | config._send_config_modified_signal() 122 | # config status changed signal sent only if status changed 123 | if config in changing_status: 124 | config._send_config_status_changed_signal() 125 | 126 | def clean(self, *args, **kwargs): 127 | """ 128 | * ensures VPN is selected if type is VPN 129 | * clears VPN specific fields if type is not VPN 130 | * automatically determines configuration if necessary 131 | """ 132 | if self.type == 'vpn' and not self.vpn: 133 | raise ValidationError( 134 | {'vpn': _('A VPN must be selected when template type is "VPN"')} 135 | ) 136 | elif self.type != 'vpn': 137 | self.vpn = None 138 | self.auto_cert = False 139 | if self.type == 'vpn' and not self.config: 140 | self.config = self.vpn.auto_client(auto_cert=self.auto_cert) 141 | super().clean(*args, **kwargs) 142 | 143 | def get_context(self): 144 | context = {} 145 | if self.default_values: 146 | context = copy(self.default_values) 147 | context.update(super().get_context()) 148 | return context 149 | 150 | def clone(self, user): 151 | clone = copy(self) 152 | clone.name = self.__get_clone_name() 153 | clone._state.adding = True 154 | clone.pk = None 155 | # avoid cloned templates to be flagged as default 156 | # to avoid potential unwanted duplications in 157 | # newly registrated devices 158 | clone.default = False 159 | clone.full_clean() 160 | clone.save() 161 | ct = ContentType.objects.get(model='template') 162 | LogEntry.objects.log_action( 163 | user_id=user.id, 164 | content_type_id=ct.pk, 165 | object_id=clone.pk, 166 | object_repr=clone.name, 167 | action_flag=ADDITION, 168 | ) 169 | return clone 170 | 171 | def __get_clone_name(self): 172 | name = '{} (Clone)'.format(self.name) 173 | index = 2 174 | while self.__class__.objects.filter(name=name).count(): 175 | name = '{} (Clone {})'.format(self.name, index) 176 | index += 1 177 | return name 178 | 179 | 180 | AbstractTemplate._meta.get_field('config').blank = True 181 | -------------------------------------------------------------------------------- /django_netjsonconfig/base/vpn.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from django.core.exceptions import ObjectDoesNotExist, ValidationError 4 | from django.db import models 5 | from django.utils.text import slugify 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from openwisp_utils.base import KeyField 9 | 10 | from .. import settings as app_settings 11 | from .base import BaseConfig 12 | 13 | 14 | class AbstractVpn(BaseConfig): 15 | """ 16 | Abstract VPN model 17 | """ 18 | 19 | host = models.CharField( 20 | max_length=64, help_text=_('VPN server hostname or ip address') 21 | ) 22 | ca = models.ForeignKey( 23 | 'django_x509.Ca', verbose_name=_('CA'), on_delete=models.CASCADE 24 | ) 25 | key = KeyField(db_index=True) 26 | cert = models.ForeignKey( 27 | 'django_x509.Cert', 28 | verbose_name=_('x509 Certificate'), 29 | help_text=_('leave blank to create automatically'), 30 | blank=True, 31 | null=True, 32 | on_delete=models.CASCADE, 33 | ) 34 | backend = models.CharField( 35 | _('VPN backend'), 36 | choices=app_settings.VPN_BACKENDS, 37 | max_length=128, 38 | help_text=_('Select VPN configuration backend'), 39 | ) 40 | notes = models.TextField(blank=True) 41 | # diffie hellman parameters are required 42 | # in some VPN solutions (eg: OpenVPN) 43 | dh = models.TextField(blank=True) 44 | 45 | __vpn__ = True 46 | 47 | class Meta: 48 | verbose_name = _('VPN server') 49 | verbose_name_plural = _('VPN servers') 50 | abstract = True 51 | 52 | def clean(self, *args, **kwargs): 53 | """ 54 | * ensure certificate matches CA 55 | """ 56 | super().clean(*args, **kwargs) 57 | # certificate must be related to CA 58 | if self.cert and self.cert.ca.pk != self.ca.pk: 59 | msg = _('The selected certificate must match the selected CA.') 60 | raise ValidationError({'cert': msg}) 61 | 62 | def save(self, *args, **kwargs): 63 | """ 64 | Calls _auto_create_cert() if cert is not set 65 | """ 66 | if not self.cert: 67 | self.cert = self._auto_create_cert() 68 | if not self.dh: 69 | self.dh = self.dhparam(1024) 70 | super().save(*args, **kwargs) 71 | 72 | @classmethod 73 | def dhparam(cls, length): 74 | """ 75 | Returns an automatically generated set of DH parameters in PEM 76 | """ 77 | return subprocess.check_output( 78 | 'openssl dhparam {0} 2> /dev/null'.format(length), shell=True 79 | ).decode('utf-8') 80 | 81 | def _auto_create_cert(self): 82 | """ 83 | Automatically generates server x509 certificate 84 | """ 85 | common_name = slugify(self.name) 86 | server_extensions = [ 87 | {"name": "nsCertType", "value": "server", "critical": False} 88 | ] 89 | cert_model = self.__class__.cert.field.related_model 90 | cert = cert_model( 91 | name=self.name, 92 | ca=self.ca, 93 | key_length=self.ca.key_length, 94 | digest=self.ca.digest, 95 | country_code=self.ca.country_code, 96 | state=self.ca.state, 97 | city=self.ca.city, 98 | organization_name=self.ca.organization_name, 99 | email=self.ca.email, 100 | common_name=common_name, 101 | extensions=server_extensions, 102 | ) 103 | cert = self._auto_create_cert_extra(cert) 104 | cert.save() 105 | return cert 106 | 107 | def _auto_create_cert_extra(self, cert): 108 | """ 109 | this method can be overridden in order to perform 110 | extra operations on a Cert object when auto-creating 111 | certificates for VPN servers 112 | """ 113 | return cert 114 | 115 | def get_context(self): 116 | """ 117 | prepares context for netjsonconfig VPN backend 118 | """ 119 | try: 120 | c = {'ca': self.ca.certificate} 121 | except ObjectDoesNotExist: 122 | c = {} 123 | if self.cert: 124 | c.update({'cert': self.cert.certificate, 'key': self.cert.private_key}) 125 | if self.dh: 126 | c.update({'dh': self.dh}) 127 | c.update(super().get_context()) 128 | return c 129 | 130 | def _get_auto_context_keys(self): 131 | """ 132 | returns a dictionary which indicates the names of 133 | the configuration variables needed to access: 134 | * path to CA file 135 | * CA certificate in PEM format 136 | * path to cert file 137 | * cert in PEM format 138 | * path to key file 139 | * key in PEM format 140 | """ 141 | pk = self.pk.hex 142 | return { 143 | 'ca_path': 'ca_path_{0}'.format(pk), 144 | 'ca_contents': 'ca_contents_{0}'.format(pk), 145 | 'cert_path': 'cert_path_{0}'.format(pk), 146 | 'cert_contents': 'cert_contents_{0}'.format(pk), 147 | 'key_path': 'key_path_{0}'.format(pk), 148 | 'key_contents': 'key_contents_{0}'.format(pk), 149 | } 150 | 151 | def auto_client(self, auto_cert=True): 152 | """ 153 | calls backend ``auto_client`` method and returns a configuration 154 | dictionary that is suitable to be used as a template 155 | if ``auto_cert`` is ``False`` the resulting configuration 156 | won't include autogenerated key and certificate details 157 | """ 158 | config = {} 159 | backend = self.backend_class 160 | if hasattr(backend, 'auto_client'): 161 | context_keys = self._get_auto_context_keys() 162 | # add curly brackets for netjsonconfig context evaluation 163 | for key in context_keys.keys(): 164 | context_keys[key] = '{{%s}}' % context_keys[key] 165 | # do not include cert and key if auto_cert is False 166 | if not auto_cert: 167 | for key in ['cert_path', 'cert_contents', 'key_path', 'key_contents']: 168 | del context_keys[key] 169 | conifg_dict_key = self.backend_class.__name__.lower() 170 | auto = backend.auto_client( 171 | host=self.host, server=self.config[conifg_dict_key][0], **context_keys 172 | ) 173 | config.update(auto) 174 | return config 175 | 176 | 177 | class AbstractVpnClient(models.Model): 178 | """ 179 | m2m through model 180 | """ 181 | 182 | config = models.ForeignKey('django_netjsonconfig.Config', on_delete=models.CASCADE) 183 | vpn = models.ForeignKey('django_netjsonconfig.Vpn', on_delete=models.CASCADE) 184 | cert = models.OneToOneField( 185 | 'django_x509.Cert', on_delete=models.CASCADE, blank=True, null=True 186 | ) 187 | # this flags indicates whether the certificate must be 188 | # automatically managed, which is going to be almost in all cases 189 | auto_cert = models.BooleanField(default=False) 190 | 191 | class Meta: 192 | abstract = True 193 | unique_together = ('config', 'vpn') 194 | verbose_name = _('VPN client') 195 | verbose_name_plural = _('VPN clients') 196 | 197 | def save(self, *args, **kwargs): 198 | """ 199 | automatically creates an x509 certificate when ``auto_cert`` is True 200 | """ 201 | if self.auto_cert: 202 | cn = self._get_common_name() 203 | self._auto_create_cert(name=self.config.device.name, common_name=cn) 204 | super().save(*args, **kwargs) 205 | 206 | def _get_common_name(self): 207 | """ 208 | returns the common name for a new certificate 209 | """ 210 | d = self.config.device 211 | cn_format = app_settings.COMMON_NAME_FORMAT 212 | if cn_format == '{mac_address}-{name}' and d.name == d.mac_address: 213 | cn_format = '{mac_address}' 214 | return cn_format.format(**d.__dict__) 215 | 216 | @classmethod 217 | def post_delete(cls, **kwargs): 218 | """ 219 | class method for ``post_delete`` signal 220 | automatically deletes certificates when ``auto_cert`` is ``True`` 221 | """ 222 | instance = kwargs['instance'] 223 | if instance.auto_cert: 224 | instance.cert.delete() 225 | 226 | def _auto_create_cert(self, name, common_name): 227 | """ 228 | Automatically creates and assigns a client x509 certificate 229 | """ 230 | server_extensions = [ 231 | {"name": "nsCertType", "value": "client", "critical": False} 232 | ] 233 | ca = self.vpn.ca 234 | cert_model = self.__class__.cert.field.related_model 235 | cert = cert_model( 236 | name=name, 237 | ca=ca, 238 | key_length=ca.key_length, 239 | digest=str(ca.digest), 240 | country_code=ca.country_code, 241 | state=ca.state, 242 | city=ca.city, 243 | organization_name=ca.organization_name, 244 | email=ca.email, 245 | common_name=common_name, 246 | extensions=server_extensions, 247 | ) 248 | cert = self._auto_create_cert_extra(cert) 249 | cert.full_clean() 250 | cert.save() 251 | self.cert = cert 252 | return cert 253 | 254 | def _auto_create_cert_extra(self, cert): 255 | """ 256 | this method can be overridden in order to perform 257 | extra operations on a Cert object when auto-creating 258 | certificates for VPN clients 259 | """ 260 | return cert 261 | -------------------------------------------------------------------------------- /django_netjsonconfig/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/django_netjsonconfig/controller/__init__.py -------------------------------------------------------------------------------- /django_netjsonconfig/controller/urls.py: -------------------------------------------------------------------------------- 1 | from ..utils import get_controller_urls 2 | from . import views 3 | 4 | app_name = 'django_netjsonconfig' 5 | 6 | urlpatterns = get_controller_urls(views) 7 | -------------------------------------------------------------------------------- /django_netjsonconfig/controller/views.py: -------------------------------------------------------------------------------- 1 | from ..models import Device, Vpn 2 | from .generics import ( 3 | BaseDeviceChecksumView, 4 | BaseDeviceDownloadConfigView, 5 | BaseDeviceRegisterView, 6 | BaseDeviceReportStatusView, 7 | BaseDeviceUpdateInfoView, 8 | BaseVpnChecksumView, 9 | BaseVpnDownloadConfigView, 10 | ) 11 | 12 | 13 | class DeviceChecksumView(BaseDeviceChecksumView): 14 | model = Device 15 | 16 | 17 | class DeviceDownloadConfigView(BaseDeviceDownloadConfigView): 18 | model = Device 19 | 20 | 21 | class DeviceUpdateInfoView(BaseDeviceUpdateInfoView): 22 | model = Device 23 | 24 | 25 | class DeviceReportStatusView(BaseDeviceReportStatusView): 26 | model = Device 27 | 28 | 29 | class DeviceRegisterView(BaseDeviceRegisterView): 30 | model = Device 31 | 32 | 33 | class VpnChecksumView(BaseVpnChecksumView): 34 | model = Vpn 35 | 36 | 37 | class VpnDownloadConfigView(BaseVpnDownloadConfigView): 38 | model = Vpn 39 | 40 | 41 | device_checksum = DeviceChecksumView.as_view() 42 | device_download_config = DeviceDownloadConfigView.as_view() 43 | device_update_info = DeviceUpdateInfoView.as_view() 44 | device_report_status = DeviceReportStatusView.as_view() 45 | device_register = DeviceRegisterView.as_view() 46 | vpn_checksum = VpnChecksumView.as_view() 47 | vpn_download_config = VpnDownloadConfigView.as_view() 48 | -------------------------------------------------------------------------------- /django_netjsonconfig/fixtures/test_templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk":"d083b494-8e16-4054-9537-fb9eba914861", 4 | "model":"django_netjsonconfig.template", 5 | "fields":{ 6 | "name": "dhcp", 7 | "backend": "netjsonconfig.OpenWrt", 8 | "config": { 9 | "interfaces": [ 10 | { 11 | "name": "eth0", 12 | "type": "ethernet", 13 | "addresses": [ 14 | { 15 | "proto": "dhcp", 16 | "family": "ipv4" 17 | } 18 | ] 19 | } 20 | ] 21 | }, 22 | "created":"2015-05-16T20:02:52.483Z", 23 | "modified":"2015-05-16T19:33:41.621Z" 24 | } 25 | }, 26 | { 27 | "pk":"d083b494-8e16-4054-9537-fb9eba914862", 28 | "model":"django_netjsonconfig.template", 29 | "fields":{ 30 | "name": "radio0", 31 | "backend": "netjsonconfig.OpenWrt", 32 | "config": { 33 | "radios": [ 34 | { 35 | "name": "radio0", 36 | "phy": "phy0", 37 | "driver": "mac80211", 38 | "protocol": "802.11n", 39 | "channel": 11, 40 | "channel_width": 20, 41 | "tx_power": 8, 42 | "country": "IT" 43 | } 44 | ] 45 | }, 46 | "created":"2015-05-16T20:02:52.483Z", 47 | "modified":"2015-05-16T19:33:41.621Z" 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-13 10:10 3 | 4 | import collections 5 | import re 6 | import uuid 7 | 8 | import django.core.validators 9 | import django.utils.timezone 10 | import jsonfield.fields 11 | import model_utils.fields 12 | import sortedm2m.fields 13 | from django.db import migrations, models 14 | 15 | from openwisp_utils.utils import get_random_key 16 | 17 | 18 | class Migration(migrations.Migration): 19 | initial = True 20 | dependencies = [] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='Config', 25 | fields=[ 26 | ( 27 | 'id', 28 | models.UUIDField( 29 | default=uuid.uuid4, 30 | editable=False, 31 | primary_key=True, 32 | serialize=False, 33 | ), 34 | ), 35 | ( 36 | 'created', 37 | model_utils.fields.AutoCreatedField( 38 | default=django.utils.timezone.now, 39 | editable=False, 40 | verbose_name='created', 41 | ), 42 | ), 43 | ( 44 | 'modified', 45 | model_utils.fields.AutoLastModifiedField( 46 | default=django.utils.timezone.now, 47 | editable=False, 48 | verbose_name='modified', 49 | ), 50 | ), 51 | ('name', models.CharField(max_length=63)), 52 | ( 53 | 'backend', 54 | models.CharField( 55 | choices=[ 56 | ('netjsonconfig.OpenWrt', 'OpenWRT'), 57 | ('netjsonconfig.OpenWisp', 'OpenWISP'), 58 | ], 59 | help_text='Select netjsonconfig backend', 60 | max_length=128, 61 | verbose_name='backend', 62 | ), 63 | ), 64 | ( 65 | 'config', 66 | jsonfield.fields.JSONField( 67 | default=dict, 68 | dump_kwargs={'indent': 4}, 69 | help_text='configuration in NetJSON DeviceConfiguration format', 70 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 71 | verbose_name='configuration', 72 | ), 73 | ), 74 | ( 75 | 'key', 76 | models.CharField( 77 | db_index=True, 78 | default=get_random_key, 79 | help_text='unique key that can be used to download the configuration', 80 | max_length=64, 81 | unique=True, 82 | validators=[ 83 | django.core.validators.RegexValidator( 84 | re.compile('^[^\\s/\\.]+$', 32), 85 | code='invalid', 86 | message='Key must not contain spaces, dots or slashes.', 87 | ) 88 | ], 89 | ), 90 | ), 91 | ], 92 | options={ 93 | 'verbose_name_plural': 'configurations', 94 | 'verbose_name': 'configuration', 95 | }, 96 | ), 97 | migrations.CreateModel( 98 | name='Template', 99 | fields=[ 100 | ( 101 | 'id', 102 | models.UUIDField( 103 | default=uuid.uuid4, 104 | editable=False, 105 | primary_key=True, 106 | serialize=False, 107 | ), 108 | ), 109 | ( 110 | 'created', 111 | model_utils.fields.AutoCreatedField( 112 | default=django.utils.timezone.now, 113 | editable=False, 114 | verbose_name='created', 115 | ), 116 | ), 117 | ( 118 | 'modified', 119 | model_utils.fields.AutoLastModifiedField( 120 | default=django.utils.timezone.now, 121 | editable=False, 122 | verbose_name='modified', 123 | ), 124 | ), 125 | ('name', models.CharField(max_length=63)), 126 | ( 127 | 'backend', 128 | models.CharField( 129 | choices=[ 130 | ('netjsonconfig.OpenWrt', 'OpenWRT'), 131 | ('netjsonconfig.OpenWisp', 'OpenWISP'), 132 | ], 133 | help_text='Select netjsonconfig backend', 134 | max_length=128, 135 | verbose_name='backend', 136 | ), 137 | ), 138 | ( 139 | 'config', 140 | jsonfield.fields.JSONField( 141 | default=dict, 142 | dump_kwargs={'indent': 4}, 143 | help_text='configuration in NetJSON DeviceConfiguration format', 144 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 145 | verbose_name='configuration', 146 | ), 147 | ), 148 | ], 149 | options={'abstract': False,}, 150 | ), 151 | migrations.AddField( 152 | model_name='config', 153 | name='templates', 154 | field=sortedm2m.fields.SortedManyToManyField( 155 | blank=True, 156 | help_text='configuration templates, applied fromfirst to last', 157 | related_name='config_relations', 158 | to='django_netjsonconfig.Template', 159 | verbose_name='templates', 160 | ), 161 | ), 162 | ] 163 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0002_config_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-20 14:02 3 | 4 | import model_utils.fields 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='config', 17 | name='status', 18 | field=model_utils.fields.StatusField( 19 | default='modified', 20 | 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', 21 | max_length=100, 22 | no_check_for_status=True, 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0003_config_last_ip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-22 09:41 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0002_config_status'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='config', 16 | name='last_ip', 17 | field=models.GenericIPAddressField( 18 | blank=True, 19 | help_text='indicates the last ip from which the configuration was downloaded from (except downloads from this page)', 20 | null=True, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0004_config_allow_blank.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-11 15:33 3 | 4 | import collections 5 | import jsonfield.fields 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('django_netjsonconfig', '0003_config_last_ip'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='config', 18 | name='config', 19 | field=jsonfield.fields.JSONField( 20 | blank=True, 21 | default=dict, 22 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 23 | help_text='configuration in NetJSON DeviceConfiguration format', 24 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 25 | verbose_name='configuration', 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0005_template_default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-17 15:11 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0004_config_allow_blank'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='template', 16 | name='default', 17 | field=models.BooleanField( 18 | db_index=True, 19 | default=False, 20 | help_text='whether new configurations will have this template enabled by default', 21 | verbose_name='enabled by default', 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0006_utc_data_migration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forwards(apps, schema_editor): 7 | """ 8 | converts "Coordinated Universal Time" to "UTC" 9 | """ 10 | if not schema_editor.connection.alias == 'default': 11 | return 12 | Config = apps.get_model('django_netjsonconfig', 'Config') 13 | Template = apps.get_model('django_netjsonconfig', 'Template') 14 | old_value = 'Coordinated Universal Time' 15 | new_value = 'UTC' 16 | for model in [Config, Template]: 17 | for obj in model.objects.filter(config__contains=old_value): 18 | obj.config['general']['timezone'] = new_value 19 | obj.full_clean() 20 | obj.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | 25 | dependencies = [ 26 | ('django_netjsonconfig', '0005_template_default'), 27 | ] 28 | 29 | operations = [ 30 | migrations.RunPython(forwards), 31 | ] 32 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0007_file_data_migration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | try: 6 | from netjsonconfig.backends.openwrt.schema import DEFAULT_FILE_MODE 7 | # netjsonconfig >= 0.6.0 8 | except ImportError: 9 | from netjsonconfig.schema import DEFAULT_FILE_MODE 10 | 11 | 12 | def forwards(apps, schema_editor): 13 | """ 14 | adds "mode" property to "files" section in configurations, if missing 15 | """ 16 | if not schema_editor.connection.alias == 'default': 17 | return 18 | Config = apps.get_model('django_netjsonconfig', 'Config') 19 | Template = apps.get_model('django_netjsonconfig', 'Template') 20 | for model in [Config, Template]: 21 | for obj in model.objects.filter(config__contains='"files"'): 22 | for f in obj.config['files']: 23 | if 'mode' not in f: 24 | f['mode'] = DEFAULT_FILE_MODE 25 | obj.save() 26 | 27 | 28 | class Migration(migrations.Migration): 29 | 30 | dependencies = [ 31 | ('django_netjsonconfig', '0006_utc_data_migration'), 32 | ] 33 | 34 | operations = [ 35 | migrations.RunPython(forwards), 36 | ] 37 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0008_vpn_integration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-08-24 15:59 3 | 4 | import collections 5 | import uuid 6 | 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | import django_netjsonconfig.base.template 10 | import jsonfield.fields 11 | import model_utils.fields 12 | from django.db import migrations, models 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('django_x509', '0002_certificate'), 19 | ('django_netjsonconfig', '0007_file_data_migration'), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='Vpn', 25 | fields=[ 26 | ( 27 | 'id', 28 | models.UUIDField( 29 | default=uuid.uuid4, 30 | editable=False, 31 | primary_key=True, 32 | serialize=False, 33 | ), 34 | ), 35 | ( 36 | 'created', 37 | model_utils.fields.AutoCreatedField( 38 | default=django.utils.timezone.now, 39 | editable=False, 40 | verbose_name='created', 41 | ), 42 | ), 43 | ( 44 | 'modified', 45 | model_utils.fields.AutoLastModifiedField( 46 | default=django.utils.timezone.now, 47 | editable=False, 48 | verbose_name='modified', 49 | ), 50 | ), 51 | ('name', models.CharField(max_length=64, unique=True)), 52 | ( 53 | 'host', 54 | models.CharField( 55 | max_length=64, help_text='VPN server hostname or ip address' 56 | ), 57 | ), 58 | ('notes', models.TextField(blank=True)), 59 | ( 60 | 'backend', 61 | models.CharField( 62 | choices=[ 63 | ('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN') 64 | ], 65 | help_text='Select VPN configuration backend', 66 | max_length=128, 67 | verbose_name='VPN backend', 68 | ), 69 | ), 70 | ( 71 | 'config', 72 | jsonfield.fields.JSONField( 73 | blank=True, 74 | default=dict, 75 | dump_kwargs={'indent': 4}, 76 | help_text='configuration in NetJSON DeviceConfiguration format', 77 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 78 | verbose_name='server configuration', 79 | ), 80 | ), 81 | ( 82 | 'ca', 83 | models.ForeignKey( 84 | on_delete=django.db.models.deletion.CASCADE, 85 | to='django_x509.Ca', 86 | verbose_name='CA', 87 | ), 88 | ), 89 | ( 90 | 'cert', 91 | models.ForeignKey( 92 | blank=True, 93 | null=True, 94 | on_delete=django.db.models.deletion.CASCADE, 95 | to='django_x509.Cert', 96 | verbose_name='x509 Certificate', 97 | help_text='leave blank to create automatically', 98 | ), 99 | ), 100 | ], 101 | options={ 102 | 'verbose_name_plural': 'VPN Servers', 103 | 'verbose_name': 'VPN Server', 104 | }, 105 | ), 106 | migrations.CreateModel( 107 | name='VpnClient', 108 | fields=[ 109 | ( 110 | 'id', 111 | models.AutoField( 112 | auto_created=True, 113 | primary_key=True, 114 | serialize=False, 115 | verbose_name='ID', 116 | ), 117 | ), 118 | ('auto_cert', models.BooleanField(default=False)), 119 | ( 120 | 'cert', 121 | models.OneToOneField( 122 | blank=True, 123 | null=True, 124 | on_delete=django.db.models.deletion.CASCADE, 125 | to='django_x509.Cert', 126 | ), 127 | ), 128 | ( 129 | 'config', 130 | models.ForeignKey( 131 | on_delete=django.db.models.deletion.CASCADE, 132 | to='django_netjsonconfig.Config', 133 | ), 134 | ), 135 | ( 136 | 'vpn', 137 | models.ForeignKey( 138 | on_delete=django.db.models.deletion.CASCADE, 139 | to='django_netjsonconfig.Vpn', 140 | ), 141 | ), 142 | ], 143 | ), 144 | migrations.AddField( 145 | model_name='template', 146 | name='auto_cert', 147 | field=models.BooleanField( 148 | db_index=True, 149 | default=django_netjsonconfig.base.template.default_auto_cert, 150 | 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', 151 | verbose_name='auto certificate', 152 | ), 153 | ), 154 | migrations.AddField( 155 | model_name='template', 156 | name='type', 157 | field=models.CharField( 158 | choices=[('generic', 'Generic'), ('vpn', 'VPN-client')], 159 | db_index=True, 160 | default='generic', 161 | help_text='template type, determines which features are available', 162 | max_length=16, 163 | verbose_name='type', 164 | ), 165 | ), 166 | migrations.AddField( 167 | model_name='config', 168 | name='vpn', 169 | field=models.ManyToManyField( 170 | blank=True, 171 | help_text='Automated VPN configurations', 172 | related_name='vpn_relations', 173 | through='django_netjsonconfig.VpnClient', 174 | to='django_netjsonconfig.Vpn', 175 | verbose_name='VPN', 176 | ), 177 | ), 178 | migrations.AddField( 179 | model_name='template', 180 | name='vpn', 181 | field=models.ForeignKey( 182 | blank=True, 183 | null=True, 184 | on_delete=django.db.models.deletion.CASCADE, 185 | to='django_netjsonconfig.Vpn', 186 | verbose_name='VPN', 187 | ), 188 | ), 189 | migrations.AlterUniqueTogether( 190 | name='vpnclient', unique_together=set([('config', 'vpn')]), 191 | ), 192 | ] 193 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0009_openvpn_data_migration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | """ 8 | converts openvpn configuration to newer format 9 | """ 10 | if not schema_editor.connection.alias == 'default': 11 | return 12 | Config = apps.get_model('django_netjsonconfig', 'Config') 13 | Template = apps.get_model('django_netjsonconfig', 'Template') 14 | for model in [Config, Template]: 15 | for obj in model.objects.filter(config__contains='"openvpn"'): 16 | for v in obj.config['openvpn']: 17 | if 'config_name' in v: 18 | del v['config_name'] 19 | if 'config_value' in v: 20 | v['name'] = v['config_value'] 21 | del v['config_value'] 22 | v['enabled'] = True 23 | if 'remote' in v and isinstance(v['remote'], str): 24 | parts = v['remote'].split() 25 | v['remote'] = [{'host': parts[0], 'port': int(parts[1])}] 26 | if 'mode' not in v: 27 | if 'client' in v or 'tls_client' in v: 28 | v['mode'] = 'p2p' 29 | else: 30 | v['mode'] = 'server' 31 | if 'proto' not in v: 32 | v['proto'] = 'udp' 33 | if 'dev' not in v: 34 | v['dev'] = v['name'] 35 | for key, value in v.items(): 36 | if value == '1': 37 | v[key] = True 38 | elif value == '0': 39 | v[key] = True 40 | if isinstance(v.get('up_delay'), bool): 41 | v['up_delay'] = int(v['up_delay']) 42 | if isinstance(v.get('down_delay'), bool): 43 | v['down_delay'] = int(v['down_delay']) 44 | if v.get('resolv_retry') == 'infinite': 45 | v['resolv_retry'] = True 46 | elif 'resolv_retry' in v: 47 | v['resolv_retry'] = False 48 | obj.save() 49 | 50 | 51 | def backward(apps, schema_editor): 52 | """ 53 | rolls back to old format 54 | """ 55 | if not schema_editor.connection.alias == 'default': 56 | return 57 | Config = apps.get_model('django_netjsonconfig', 'Config') 58 | Template = apps.get_model('django_netjsonconfig', 'Template') 59 | for model in [Config, Template]: 60 | for obj in model.objects.filter(config__contains='"openvpn"'): 61 | for v in obj.config['openvpn']: 62 | v['config_name'] = 'openvpn' 63 | if 'name' in v: 64 | v['config_value'] = v['name'] 65 | del v['name'] 66 | if 'remote' in v and isinstance(v['remote'], list): 67 | v['remote'] = '{host} {port}'.format(**v['remote'][0]) 68 | obj.save() 69 | 70 | 71 | class Migration(migrations.Migration): 72 | dependencies = [ 73 | ('django_x509', '0002_certificate'), 74 | ('django_netjsonconfig', '0008_vpn_integration'), 75 | ] 76 | 77 | operations = [ 78 | migrations.RunPython(forward, backward), 79 | ] 80 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0010_basemodel_reorganization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-09 14:52 3 | 4 | import collections 5 | import jsonfield.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('django_netjsonconfig', '0009_openvpn_data_migration'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='config', name='name', field=models.CharField(max_length=64), 18 | ), 19 | migrations.AlterField( 20 | model_name='template', name='name', field=models.CharField(max_length=64), 21 | ), 22 | migrations.AlterField( 23 | model_name='vpn', 24 | name='config', 25 | field=jsonfield.fields.JSONField( 26 | default=dict, 27 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 28 | help_text='configuration in NetJSON DeviceConfiguration format', 29 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 30 | verbose_name='configuration', 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0011_template_config_blank.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-12 15:43 3 | 4 | import collections 5 | import jsonfield.fields 6 | from django.db import migrations 7 | import collections 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_netjsonconfig', '0010_basemodel_reorganization'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='template', 19 | name='config', 20 | field=jsonfield.fields.JSONField( 21 | blank=True, 22 | default=dict, 23 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 24 | help_text='configuration in NetJSON DeviceConfiguration format', 25 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 26 | verbose_name='configuration', 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0012_name_unique.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-14 09:18 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0011_template_config_blank'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='config', 16 | name='name', 17 | field=models.CharField(max_length=64, unique=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='template', 21 | name='name', 22 | field=models.CharField(max_length=64, unique=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0013_config_mac_address.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-14 10:17 3 | 4 | import re 5 | 6 | import django.core.validators 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_netjsonconfig', '0012_name_unique'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='config', 19 | name='mac_address', 20 | field=models.CharField( 21 | max_length=17, 22 | null=True, 23 | validators=[ 24 | django.core.validators.RegexValidator( 25 | re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), 26 | code='invalid', 27 | message='Must be a valid mac address.', 28 | ) 29 | ], 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0014_randomize_mac_address.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-14 10:42 3 | 4 | from django.db import migrations 5 | 6 | 7 | def randomize_mac_address(apps, schema_editor): 8 | """ 9 | This data migration is not necessary anymore. 10 | It will be squashed in future versions. 11 | """ 12 | pass 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('django_netjsonconfig', '0013_config_mac_address'), 19 | ] 20 | 21 | operations = [ 22 | migrations.RunPython( 23 | randomize_mac_address, reverse_code=migrations.RunPython.noop 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0015_config_mac_address_unique.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2016-09-14 10:42 3 | 4 | import re 5 | 6 | import django.core.validators 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_netjsonconfig', '0014_randomize_mac_address'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='config', 19 | name='mac_address', 20 | field=models.CharField( 21 | max_length=17, 22 | unique=True, 23 | validators=[ 24 | django.core.validators.RegexValidator( 25 | re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), 26 | code='invalid', 27 | message='Must be a valid mac address.', 28 | ) 29 | ], 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0016_vpn_dh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2016-09-16 16:29 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0015_config_mac_address_unique'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='vpn', name='dh', field=models.TextField(blank=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0017_openvpn_data_migration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | """ 8 | corrects wrong openvpn "mode" 9 | TODO: delete this migration in future releases 10 | """ 11 | if not schema_editor.connection.alias == 'default': 12 | return 13 | Config = apps.get_model('django_netjsonconfig', 'Config') 14 | Template = apps.get_model('django_netjsonconfig', 'Template') 15 | for model in [Config, Template]: 16 | for obj in model.objects.filter(config__contains='"openvpn"'): 17 | for v in obj.config['openvpn']: 18 | if 'mode' in v and v['mode'] == 'client': 19 | v['mode'] = 'p2p' 20 | obj.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | dependencies = [ 25 | ('django_netjsonconfig', '0016_vpn_dh'), 26 | ] 27 | 28 | operations = [ 29 | migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), 30 | ] 31 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0018_openvpn_disabled_attr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | """ 8 | converts "enabled" attribute to "disabled" in OpenVPN configurations, 9 | according to the change introduced in netjsonconfig 0.5.3 10 | (see https://github.com/openwisp/netjsonconfig/commit/7a152a344333665bbc9217011418ae39e8a1af81) 11 | TODO: delete this migration in future releases 12 | """ 13 | if not schema_editor.connection.alias == 'default': 14 | return 15 | Config = apps.get_model('django_netjsonconfig', 'Config') 16 | Template = apps.get_model('django_netjsonconfig', 'Template') 17 | for model in [Config, Template]: 18 | for obj in model.objects.filter(config__contains='"openvpn"'): 19 | for vpn in obj.config['openvpn']: 20 | if 'enabled' in vpn: 21 | vpn['disabled'] = not vpn['enabled'] 22 | del vpn['enabled'] 23 | obj.save() 24 | 25 | 26 | class Migration(migrations.Migration): 27 | dependencies = [ 28 | ('django_netjsonconfig', '0017_openvpn_data_migration'), 29 | ] 30 | 31 | operations = [ 32 | migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), 33 | ] 34 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0019_cleanup_model_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2017-02-08 13:15 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0018_openvpn_disabled_attr'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='template', 16 | options={'verbose_name': 'template', 'verbose_name_plural': 'templates'}, 17 | ), 18 | migrations.AlterModelOptions( 19 | name='vpn', 20 | options={ 21 | 'verbose_name': 'VPN server', 22 | 'verbose_name_plural': 'VPN servers', 23 | }, 24 | ), 25 | migrations.AlterModelOptions( 26 | name='vpnclient', 27 | options={ 28 | 'verbose_name': 'VPN client', 29 | 'verbose_name_plural': 'VPN clients', 30 | }, 31 | ), 32 | migrations.AlterField( 33 | model_name='config', 34 | name='vpn', 35 | field=models.ManyToManyField( 36 | blank=True, 37 | related_name='vpn_relations', 38 | through='django_netjsonconfig.VpnClient', 39 | to='django_netjsonconfig.Vpn', 40 | ), 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0020_openvpn_resolv_retry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | """ 8 | converts "resolv_retry" attribute to string format in OpenVPN configurations, 9 | according to the change introduced in netjsonconfig 0.5.4 10 | (see https://github.com/openwisp/netjsonconfig/commit/904659962832b1cf097e34c4251a56e158a247ae) 11 | TODO: delete this migration in future releases 12 | """ 13 | if not schema_editor.connection.alias == 'default': 14 | return 15 | Config = apps.get_model('django_netjsonconfig', 'Config') 16 | Template = apps.get_model('django_netjsonconfig', 'Template') 17 | Vpn = apps.get_model('django_netjsonconfig', 'Vpn') 18 | for model in [Config, Template, Vpn]: 19 | # find objects which have OpenVPN configurations containing the "resolv_retry" attribute 20 | queryset = model.objects.filter(config__contains='"openvpn"').filter( 21 | config__contains='"resolv_retry"' 22 | ) 23 | for obj in queryset: 24 | for vpn in obj.config['openvpn']: 25 | if 'resolv_retry' in vpn: 26 | vpn['resolv_retry'] = 'infinite' if vpn['resolv_retry'] else '0' 27 | obj.save() 28 | 29 | 30 | class Migration(migrations.Migration): 31 | dependencies = [ 32 | ('django_netjsonconfig', '0019_cleanup_model_options'), 33 | ] 34 | 35 | operations = [ 36 | migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), 37 | ] 38 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0021_netjsonconfig_label.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2017-02-15 13:37 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0020_openvpn_resolv_retry'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='config', 16 | name='backend', 17 | field=models.CharField( 18 | choices=[ 19 | ('netjsonconfig.OpenWrt', 'OpenWRT'), 20 | ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), 21 | ], 22 | help_text='Select netjsonconfig backend', 23 | max_length=128, 24 | verbose_name='backend', 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name='template', 29 | name='backend', 30 | field=models.CharField( 31 | choices=[ 32 | ('netjsonconfig.OpenWrt', 'OpenWRT'), 33 | ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), 34 | ], 35 | help_text='Select netjsonconfig backend', 36 | max_length=128, 37 | verbose_name='backend', 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0022_update_model_labels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-04-12 12:41 3 | 4 | import re 5 | 6 | import django.core.validators 7 | import sortedm2m.fields 8 | from django.db import migrations, models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('django_netjsonconfig', '0021_netjsonconfig_label'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AlterField( 19 | model_name='config', 20 | name='mac_address', 21 | field=models.CharField( 22 | help_text='primary mac address', 23 | max_length=17, 24 | unique=True, 25 | validators=[ 26 | django.core.validators.RegexValidator( 27 | re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), 28 | code='invalid', 29 | message='Must be a valid mac address.', 30 | ) 31 | ], 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name='config', 36 | name='templates', 37 | field=sortedm2m.fields.SortedManyToManyField( 38 | blank=True, 39 | help_text='configuration templates, applied from first to last', 40 | related_name='config_relations', 41 | to='django_netjsonconfig.Template', 42 | verbose_name='templates', 43 | ), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0023_template_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-04-14 14:01 3 | 4 | import uuid 5 | 6 | import django.db.models.deletion 7 | import taggit.managers 8 | from django.db import migrations, models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('contenttypes', '0002_remove_content_type_name'), 15 | ('django_netjsonconfig', '0022_update_model_labels'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='TaggedTemplate', 21 | fields=[ 22 | ( 23 | 'id', 24 | models.AutoField( 25 | auto_created=True, 26 | primary_key=True, 27 | serialize=False, 28 | verbose_name='ID', 29 | ), 30 | ), 31 | ( 32 | 'object_id', 33 | models.UUIDField(db_index=True, verbose_name='Object id'), 34 | ), 35 | ( 36 | 'content_type', 37 | models.ForeignKey( 38 | on_delete=django.db.models.deletion.CASCADE, 39 | related_name='django_netjsonconfig_taggedtemplate_tagged_items', 40 | to='contenttypes.ContentType', 41 | verbose_name='Content type', 42 | ), 43 | ), 44 | ], 45 | options={ 46 | 'verbose_name_plural': 'Tags', 47 | 'abstract': False, 48 | 'verbose_name': 'Tagged item', 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name='TemplateTag', 53 | fields=[ 54 | ( 55 | 'name', 56 | models.CharField(max_length=100, unique=True, verbose_name='Name'), 57 | ), 58 | ( 59 | 'slug', 60 | models.SlugField(max_length=100, unique=True, verbose_name='Slug'), 61 | ), 62 | ( 63 | 'id', 64 | models.UUIDField( 65 | default=uuid.uuid4, 66 | editable=False, 67 | primary_key=True, 68 | serialize=False, 69 | ), 70 | ), 71 | ], 72 | options={ 73 | 'verbose_name_plural': 'Tags', 74 | 'abstract': False, 75 | 'verbose_name': 'Tag', 76 | }, 77 | ), 78 | migrations.AddField( 79 | model_name='taggedtemplate', 80 | name='tag', 81 | field=models.ForeignKey( 82 | on_delete=django.db.models.deletion.CASCADE, 83 | related_name='django_netjsonconfig_taggedtemplate_items', 84 | to='django_netjsonconfig.TemplateTag', 85 | ), 86 | ), 87 | migrations.AddField( 88 | model_name='template', 89 | name='tags', 90 | field=taggit.managers.TaggableManager( 91 | blank=True, 92 | 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.)', 93 | through='django_netjsonconfig.TaggedTemplate', 94 | to='django_netjsonconfig.TemplateTag', 95 | verbose_name='Tags', 96 | ), 97 | ), 98 | ] 99 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0024_add_device_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-04-27 10:20 3 | 4 | import re 5 | import uuid 6 | 7 | import django.core.validators 8 | import django.db.models.deletion 9 | import django.utils.timezone 10 | import model_utils.fields 11 | import openwisp_utils.base 12 | import openwisp_utils.utils 13 | from django.db import migrations, models 14 | 15 | 16 | class Migration(migrations.Migration): 17 | 18 | dependencies = [ 19 | ('django_netjsonconfig', '0023_template_tags'), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='Device', 25 | fields=[ 26 | ( 27 | 'id', 28 | models.UUIDField( 29 | default=uuid.uuid4, 30 | editable=False, 31 | primary_key=True, 32 | serialize=False, 33 | ), 34 | ), 35 | ('name', models.CharField(max_length=64, unique=True)), 36 | ( 37 | 'mac_address', 38 | models.CharField( 39 | help_text='primary mac address', 40 | max_length=17, 41 | unique=True, 42 | validators=[ 43 | django.core.validators.RegexValidator( 44 | re.compile( 45 | '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32 46 | ), 47 | code='invalid', 48 | message='Must be a valid mac address.', 49 | ) 50 | ], 51 | ), 52 | ), 53 | ( 54 | 'key', 55 | openwisp_utils.base.KeyField( 56 | db_index=True, 57 | default=openwisp_utils.utils.get_random_key, 58 | help_text='unique device key', 59 | max_length=64, 60 | unique=True, 61 | validators=[ 62 | django.core.validators.RegexValidator( 63 | re.compile('^[^\\s/\\.]+$'), 64 | code='invalid', 65 | message='This value must not contain spaces, dots or slashes.', 66 | ) 67 | ], 68 | ), 69 | ), 70 | ( 71 | 'model', 72 | models.CharField( 73 | blank=True, 74 | help_text='device model and manufacturer', 75 | max_length=64, 76 | ), 77 | ), 78 | ( 79 | 'os', 80 | models.CharField( 81 | blank=True, 82 | help_text='operating system identifier', 83 | max_length=128, 84 | verbose_name='operating system', 85 | ), 86 | ), 87 | ('notes', models.TextField(blank=True)), 88 | ( 89 | 'created', 90 | model_utils.fields.AutoCreatedField( 91 | default=django.utils.timezone.now, 92 | editable=False, 93 | verbose_name='created', 94 | ), 95 | ), 96 | ( 97 | 'modified', 98 | model_utils.fields.AutoLastModifiedField( 99 | default=django.utils.timezone.now, 100 | editable=False, 101 | verbose_name='modified', 102 | ), 103 | ), 104 | ], 105 | options={'abstract': False,}, 106 | ), 107 | migrations.AddField( 108 | model_name='config', 109 | name='device', 110 | field=models.OneToOneField( 111 | blank=True, 112 | null=True, 113 | on_delete=django.db.models.deletion.CASCADE, 114 | to='django_netjsonconfig.Device', 115 | ), 116 | ), 117 | ] 118 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0025_populate_device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | """ 8 | Creates a Device record for each existing Config 9 | TODO: delete this migration in future releases 10 | """ 11 | if not schema_editor.connection.alias == 'default': 12 | return 13 | Device = apps.get_model('django_netjsonconfig', 'Device') 14 | Config = apps.get_model('django_netjsonconfig', 'Config') 15 | 16 | for config in Config.objects.all(): 17 | device = Device( 18 | id=config.id, 19 | name=config.name, 20 | mac_address=config.mac_address, 21 | key=config.key, 22 | created=config.created, 23 | modified=config.modified, 24 | ) 25 | device.full_clean() 26 | device.save() 27 | config.device = device 28 | config.save() 29 | 30 | 31 | class Migration(migrations.Migration): 32 | dependencies = [ 33 | ('django_netjsonconfig', '0024_add_device_model'), 34 | ] 35 | 36 | operations = [ 37 | migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), 38 | ] 39 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0026_config_device_not_null.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-04-27 13:59 3 | 4 | import django.db.models.deletion 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0025_populate_device'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='config', 17 | name='device', 18 | field=models.OneToOneField( 19 | on_delete=django.db.models.deletion.CASCADE, 20 | to='django_netjsonconfig.Device', 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0027_simplify_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-05-09 14:55 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0026_config_device_not_null'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField(model_name='config', name='name',), 15 | migrations.RemoveField(model_name='config', name='key',), 16 | migrations.RemoveField(model_name='config', name='mac_address',), 17 | ] 18 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0028_device_indexes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-05-15 16:28 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0027_simplify_config'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='device', 16 | name='model', 17 | field=models.CharField( 18 | blank=True, 19 | db_index=True, 20 | help_text='device model and manufacturer', 21 | max_length=64, 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name='device', 26 | name='os', 27 | field=models.CharField( 28 | blank=True, 29 | db_index=True, 30 | help_text='operating system identifier', 31 | max_length=128, 32 | verbose_name='operating system', 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0029_explicit_indexes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-05-15 16:33 3 | 4 | import re 5 | 6 | import django.core.validators 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_netjsonconfig', '0028_device_indexes'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='device', 19 | name='mac_address', 20 | field=models.CharField( 21 | db_index=True, 22 | help_text='primary mac address', 23 | max_length=17, 24 | unique=True, 25 | validators=[ 26 | django.core.validators.RegexValidator( 27 | re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', 32), 28 | code='invalid', 29 | message='Must be a valid mac address.', 30 | ) 31 | ], 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name='device', 36 | name='name', 37 | field=models.CharField(db_index=True, max_length=64, unique=True), 38 | ), 39 | migrations.AlterField( 40 | model_name='template', 41 | name='name', 42 | field=models.CharField(db_index=True, max_length=64, unique=True), 43 | ), 44 | migrations.AlterField( 45 | model_name='vpn', 46 | name='name', 47 | field=models.CharField(db_index=True, max_length=64, unique=True), 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0030_device_system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-06-01 15:57 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_netjsonconfig', '0029_explicit_indexes'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='device', 16 | name='system', 17 | field=models.CharField( 18 | blank=True, 19 | db_index=True, 20 | help_text='system on chip or CPU info', 21 | max_length=128, 22 | verbose_name='SOC / CPU', 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0031_updated_mac_address_validator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2018-01-12 14:04 3 | 4 | import re 5 | 6 | import django.core.validators 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('django_netjsonconfig', '0030_device_system'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='device', 19 | name='mac_address', 20 | field=models.CharField( 21 | db_index=True, 22 | help_text='primary mac address', 23 | max_length=17, 24 | unique=True, 25 | validators=[ 26 | django.core.validators.RegexValidator( 27 | re.compile('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', 32), 28 | code='invalid', 29 | message='Must be a valid mac address.', 30 | ) 31 | ], 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0032_internal_notes_label.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-07-11 10:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_netjsonconfig', '0031_updated_mac_address_validator'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='device', 15 | name='notes', 16 | field=models.TextField(blank=True, help_text='internal notes'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0033_migrate_last_ip.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-07-11 11:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def forward(apps, schema_editor): 7 | device_model = apps.get_model('django_netjsonconfig', 'Device') 8 | devices = device_model.objects.all().select_related('config') 9 | for device in devices: 10 | if not hasattr(device, 'config'): 11 | continue 12 | device.last_ip = device.config.last_ip 13 | device.save() 14 | 15 | 16 | def backward(apps, schema_editor): 17 | device_model = apps.get_model('django_netjsonconfig', 'Device') 18 | devices = device_model.objects.all().select_related('config') 19 | for device in devices: 20 | if not hasattr(device, 'config'): 21 | continue 22 | device.config.last_ip = device.last_ip 23 | device.config.save() 24 | 25 | 26 | class Migration(migrations.Migration): 27 | 28 | dependencies = [ 29 | ('django_netjsonconfig', '0032_internal_notes_label'), 30 | ] 31 | 32 | operations = [ 33 | migrations.AddField( 34 | model_name='device', 35 | name='last_ip', 36 | field=models.GenericIPAddressField( 37 | blank=True, 38 | help_text='indicates the IP address logged from the last request coming from the device', 39 | null=True, 40 | ), 41 | ), 42 | migrations.RunPython(forward, backward), 43 | migrations.RemoveField(model_name='config', name='last_ip'), 44 | ] 45 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0034_device_management_ip.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-07-11 12:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_netjsonconfig', '0033_migrate_last_ip'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='device', 15 | name='management_ip', 16 | field=models.GenericIPAddressField( 17 | blank=True, 18 | help_text='ip address of the management interface, if available', 19 | null=True, 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0035_renamed_status_choices.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-07-11 13:34 2 | 3 | import model_utils.fields 4 | from django.db import migrations 5 | 6 | 7 | def forward(apps, schema_editor): 8 | config_model = apps.get_model('django_netjsonconfig', 'Config') 9 | for config in config_model.objects.all(): 10 | if config.status != 'running': 11 | continue 12 | config.status = 'applied' 13 | config.save() 14 | 15 | 16 | def backward(apps, schema_editor): 17 | config_model = apps.get_model('django_netjsonconfig', 'Config') 18 | for config in config_model.objects.all(): 19 | if config.status != 'applied': 20 | continue 21 | config.status = 'running' 22 | config.save() 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('django_netjsonconfig', '0034_device_management_ip'), 29 | ] 30 | 31 | operations = [ 32 | migrations.AlterField( 33 | model_name='config', 34 | name='status', 35 | field=model_utils.fields.StatusField( 36 | choices=[(0, 'dummy')], 37 | default='modified', 38 | 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;', 39 | max_length=100, 40 | no_check_for_status=True, 41 | verbose_name='configuration status', 42 | ), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0036_device_hardware_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-10-25 13:02 2 | 3 | from django.db import migrations, models 4 | 5 | from .. import settings as app_settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0035_renamed_status_choices'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='device', 17 | name='hardware_id', 18 | field=models.CharField(**(app_settings.HARDWARE_ID_OPTIONS)), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0037_config_context.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-11-19 07:16 2 | 3 | import collections 4 | import jsonfield.fields 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0036_device_hardware_id'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='config', 17 | name='context', 18 | field=jsonfield.fields.JSONField( 19 | blank=True, 20 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 21 | help_text='Additional context (configuration variables) in JSON format', 22 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 23 | null=True, 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0038_vpn_key.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | import openwisp_utils.base 6 | import openwisp_utils.utils 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('django_netjsonconfig', '0037_config_context'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='vpn', 18 | name='key', 19 | field=openwisp_utils.base.KeyField( 20 | db_index=True, 21 | default=openwisp_utils.utils.get_random_key, 22 | help_text=None, 23 | max_length=64, 24 | validators=[ 25 | django.core.validators.RegexValidator( 26 | re.compile('^[^\\s/\\.]+$'), 27 | code='invalid', 28 | message='This value must not contain spaces, dots or slashes.', 29 | ) 30 | ], 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0039_vpn_format_dh.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def format_dh(apps, schema_editor): 5 | Vpn = apps.get_model('django_netjsonconfig', 'Vpn') 6 | 7 | for vpn in Vpn.objects.all(): 8 | if vpn.dh.startswith("b'") and vpn.dh.endswith("'"): 9 | vpn.dh = vpn.dh[2:-1] 10 | vpn.save() 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ('django_netjsonconfig', '0038_vpn_key'), 17 | ] 18 | 19 | operations = [ 20 | migrations.RunPython(format_dh, reverse_code=migrations.RunPython.noop), 21 | ] 22 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0040_update_context.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-02-26 19:53 2 | 3 | import collections 4 | from django.db import migrations 5 | import jsonfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0039_vpn_format_dh'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='config', 17 | name='context', 18 | field=jsonfield.fields.JSONField( 19 | blank=True, 20 | default=dict, 21 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 22 | help_text='Additional context (configuration variables) in JSON format', 23 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0041_update_context_data.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def forward(apps, schema_editor): 5 | """ 6 | Updates default value of context field 7 | """ 8 | if not schema_editor.connection.alias == 'default': 9 | return 10 | Config = apps.get_model('django_netjsonconfig', 'Config') 11 | 12 | for config in Config.objects.filter(context__isnull=True): 13 | config.context = {} 14 | config.save() 15 | 16 | 17 | class Migration(migrations.Migration): 18 | 19 | dependencies = [ 20 | ('django_netjsonconfig', '0040_update_context'), 21 | ] 22 | 23 | operations = [ 24 | migrations.RunPython(forward, reverse_code=migrations.RunPython.noop), 25 | ] 26 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0042_device_key_none.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-02-26 16:54 2 | 3 | import django.core.validators 4 | from django.db import migrations 5 | import openwisp_utils.base 6 | import re 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('django_netjsonconfig', '0041_update_context_data'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='device', 18 | name='key', 19 | field=openwisp_utils.base.KeyField( 20 | blank=True, 21 | db_index=True, 22 | default=None, 23 | help_text='unique device key', 24 | max_length=64, 25 | unique=True, 26 | validators=[ 27 | django.core.validators.RegexValidator( 28 | re.compile('^[^\\s/\\.]+$'), 29 | code='invalid', 30 | message='This value must not contain spaces, dots or slashes.', 31 | ) 32 | ], 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0043_add_indexes_on_ip_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-12 02:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_netjsonconfig', '0042_device_key_none'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='device', 15 | name='last_ip', 16 | field=models.GenericIPAddressField( 17 | blank=True, 18 | db_index=True, 19 | help_text='indicates the IP address logged from the last request coming from the device', 20 | null=True, 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name='device', 25 | name='management_ip', 26 | field=models.GenericIPAddressField( 27 | blank=True, 28 | db_index=True, 29 | help_text='ip address of the management interface, if available', 30 | null=True, 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/0044_template_default_values.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-08 23:46 2 | 3 | import collections 4 | from django.db import migrations 5 | import jsonfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_netjsonconfig', '0043_add_indexes_on_ip_fields'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='template', 17 | name='default_values', 18 | field=jsonfield.fields.JSONField( 19 | blank=True, 20 | default=dict, 21 | dump_kwargs={'ensure_ascii': False, 'indent': 4}, 22 | help_text='A dictionary containing the default values for the variables used by this template; these default variables will be used during schema validation.', 23 | load_kwargs={'object_pairs_hook': collections.OrderedDict}, 24 | verbose_name='Default Values', 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /django_netjsonconfig/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/django_netjsonconfig/migrations/__init__.py -------------------------------------------------------------------------------- /django_netjsonconfig/models.py: -------------------------------------------------------------------------------- 1 | from .base.config import AbstractConfig, TemplatesVpnMixin 2 | from .base.device import AbstractDevice 3 | from .base.tag import AbstractTaggedTemplate, AbstractTemplateTag 4 | from .base.template import AbstractTemplate 5 | from .base.vpn import AbstractVpn, AbstractVpnClient 6 | 7 | 8 | class Config(TemplatesVpnMixin, AbstractConfig): 9 | """ 10 | Concrete Config model 11 | """ 12 | 13 | class Meta(AbstractConfig.Meta): 14 | abstract = False 15 | 16 | 17 | class Device(AbstractDevice): 18 | """ 19 | Concrete device model 20 | """ 21 | 22 | class Meta(AbstractDevice.Meta): 23 | abstract = False 24 | 25 | 26 | class TemplateTag(AbstractTemplateTag): 27 | """ 28 | Concrete template tag model 29 | """ 30 | 31 | class Meta(AbstractTemplateTag.Meta): 32 | abstract = False 33 | 34 | 35 | class TaggedTemplate(AbstractTaggedTemplate): 36 | """ 37 | tagged item model with support for UUID primary keys 38 | """ 39 | 40 | class Meta(AbstractTaggedTemplate.Meta): 41 | abstract = False 42 | 43 | 44 | class Template(AbstractTemplate): 45 | """ 46 | Concrete Template model 47 | """ 48 | 49 | class Meta(AbstractTemplate.Meta): 50 | abstract = False 51 | 52 | 53 | class VpnClient(AbstractVpnClient): 54 | """ 55 | Concrete VpnClient model 56 | """ 57 | 58 | class Meta(AbstractVpnClient.Meta): 59 | abstract = False 60 | 61 | 62 | class Vpn(AbstractVpn): 63 | """ 64 | Concrete VPN model 65 | """ 66 | 67 | class Meta(AbstractVpn.Meta): 68 | abstract = False 69 | -------------------------------------------------------------------------------- /django_netjsonconfig/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | BACKENDS = getattr( 5 | settings, 6 | 'NETJSONCONFIG_BACKENDS', 7 | ( 8 | ('netjsonconfig.OpenWrt', 'OpenWRT'), 9 | ('netjsonconfig.OpenWisp', 'OpenWISP Firmware 1.x'), 10 | ), 11 | ) 12 | VPN_BACKENDS = getattr( 13 | settings, 14 | 'NETJSONCONFIG_VPN_BACKENDS', 15 | (('django_netjsonconfig.vpn_backends.OpenVpn', 'OpenVPN'),), 16 | ) 17 | DEFAULT_BACKEND = getattr(settings, 'NETJSONCONFIG_DEFAULT_BACKEND', BACKENDS[0][0]) 18 | DEFAULT_VPN_BACKEND = getattr( 19 | settings, 'NETJSONCONFIG_DEFAULT_VPN_BACKEND', VPN_BACKENDS[0][0] 20 | ) 21 | REGISTRATION_ENABLED = getattr(settings, 'NETJSONCONFIG_REGISTRATION_ENABLED', True) 22 | CONSISTENT_REGISTRATION = getattr( 23 | settings, 'NETJSONCONFIG_CONSISTENT_REGISTRATION', True 24 | ) 25 | REGISTRATION_SELF_CREATION = getattr( 26 | settings, 'NETJSONCONFIG_REGISTRATION_SELF_CREATION', True 27 | ) 28 | SHARED_SECRET = getattr(settings, 'NETJSONCONFIG_SHARED_SECRET', '') 29 | CONTEXT = getattr(settings, 'NETJSONCONFIG_CONTEXT', {}) 30 | assert isinstance(CONTEXT, dict), 'NETJSONCONFIG_CONTEXT must be a dictionary' 31 | DEFAULT_AUTO_CERT = getattr(settings, 'NETJSONCONFIG_DEFAULT_AUTO_CERT', True) 32 | CERT_PATH = getattr(settings, 'NETJSONCONFIG_CERT_PATH', '/etc/x509') 33 | COMMON_NAME_FORMAT = getattr( 34 | settings, 'NETJSONCONFIG_COMMON_NAME_FORMAT', '{mac_address}-{name}' 35 | ) 36 | MANAGEMENT_IP_DEVICE_LIST = getattr( 37 | settings, 'NETJSONCONFIG_MANAGEMENT_IP_DEVICE_LIST', True 38 | ) 39 | BACKEND_DEVICE_LIST = getattr(settings, 'NETJSONCONFIG_BACKEND_DEVICE_LIST', True) 40 | 41 | HARDWARE_ID_ENABLED = getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_ENABLED', False) 42 | HARDWARE_ID_OPTIONS = { 43 | 'blank': not HARDWARE_ID_ENABLED, 44 | 'null': True, 45 | 'max_length': 32, 46 | 'unique': True, 47 | 'verbose_name': _('Serial number'), 48 | 'help_text': _('Serial number of this device'), 49 | } 50 | HARDWARE_ID_OPTIONS.update(getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_OPTIONS', {})) 51 | HARDWARE_ID_AS_NAME = getattr(settings, 'NETJSONCONFIG_HARDWARE_ID_AS_NAME', True) 52 | -------------------------------------------------------------------------------- /django_netjsonconfig/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | checksum_requested = Signal(providing_args=['instance', 'request']) 4 | config_download_requested = Signal(providing_args=['instance', 'request']) 5 | config_status_changed = Signal(providing_args=['instance']) 6 | # device and config args are maintained for backward compatibility 7 | config_modified = Signal(providing_args=['instance', 'device', 'config']) 8 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/css/admin.css: -------------------------------------------------------------------------------- 1 | a.button:focus, 2 | a.jsoneditor-exit:focus{ text-decoration: none } 3 | .djnjc-preformatted, .field-config .vLargeTextField, 4 | .jsoneditor .vLargeTextField{ 5 | font-size:1em; 6 | font-family: "Bitstream Vera Sans Mono", 7 | "Monaco", 8 | "Droid Sans Mono", 9 | "DejaVu Sans Mono", 10 | "Ubuntu Mono", 11 | "Courier New", Courier, monospace; 12 | color: #f9f9f9; 13 | overflow: auto; 14 | background-color: #222; 15 | white-space: pre-wrap; 16 | word-wrap: break-word; 17 | padding: 20px; 18 | line-height: 22px; 19 | } 20 | .jsoneditor .vLargeTextField{ 21 | color: #333; 22 | background-color: #fff; 23 | padding: 15px; 24 | font-weight: bold 25 | } 26 | 27 | .vLargeTextField.jsoneditor-raw{ 28 | min-width: 100%; 29 | min-height: 500px; 30 | box-sizing: border-box; 31 | } 32 | 33 | input.readonly{ 34 | border: 1px solid rgba(0, 0, 0, 0.05) !important; 35 | background-color: rgba(0, 0, 0, 0.070); 36 | } 37 | 38 | .djnjc-overlay { 39 | display: none; 40 | position: fixed; 41 | left: 0; 42 | top: 0; 43 | z-index: 11; 44 | width: 100%; 45 | height: 100%; 46 | background: rgba(0, 0, 0, 0.91); 47 | } 48 | 49 | .djnjc-overlay .inner{ 50 | width: 100%; 51 | height: 100%; 52 | overflow: auto; 53 | padding: 0; 54 | } 55 | 56 | .djnjc-overlay.loading{ 57 | display: flex; 58 | background: rgba(255, 255, 255, 0.98); 59 | align-items: center; 60 | } 61 | .spinner { 62 | width: 40px; 63 | height: 40px; 64 | background-color: rgba(0, 0, 0, 0.8); 65 | margin: 0 auto; 66 | border-radius: 100%; 67 | -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; 68 | animation: sk-scaleout 1.0s infinite ease-in-out; 69 | } 70 | #loading-overlay, 71 | #tabs-container { display: none } 72 | #device_form #loading-overlay { display: flex } 73 | #device_form #tabs-container { display: block } 74 | 75 | @-webkit-keyframes sk-scaleout { 76 | 0% { -webkit-transform: scale(0) } 77 | 100% { 78 | -webkit-transform: scale(1.0); 79 | opacity: 0; 80 | } 81 | } 82 | 83 | @keyframes sk-scaleout { 84 | 0% { 85 | -webkit-transform: scale(0); 86 | transform: scale(0); 87 | } 100% { 88 | -webkit-transform: scale(1.0); 89 | transform: scale(1.0); 90 | opacity: 0; 91 | } 92 | } 93 | 94 | .djnjc-overlay .djnjc-preformatted{ 95 | margin: 0; 96 | padding: 40px 60px 20px; 97 | background-color: transparent; 98 | color: #adffa6; 99 | line-height: 1.5em; 100 | } 101 | 102 | .djnjc-preformatted.error{ 103 | color: #ff7277 104 | } 105 | 106 | .djnjc-overlay .close, 107 | .djnjc-overlay .close:focus, 108 | .djnjc-overlay .close:active{ 109 | position: absolute; 110 | right: 3%; 111 | top: 1.5%; 112 | font-weight: bold; 113 | font-size: 15px; 114 | background-color: #772299; 115 | color: #fff; 116 | padding: 5px 8px; 117 | border-radius: 5px; 118 | text-decoration: none; 119 | } 120 | .djnjc-overlay .close:hover{ 121 | background-color: #9e47c1; 122 | } 123 | 124 | .errors.field-templates li, 125 | .errorlist.nonfield li{ 126 | white-space: pre-line; 127 | word-break: break-all; 128 | } 129 | .form-row select{ background-color: #fff } 130 | .form-row input[disabled], .form-row select[disabled]{ background-color: #f4f4f4 } 131 | /* hide vpn specific fields by default */ 132 | .field-vpn, .field-auto_cert{ 133 | display: none; 134 | } 135 | #container .help{ margin-top: 3px; font-size: 13px } 136 | #container .help a, 137 | #netjsonconfig-hint a, #netjsonconfig-hint-advancedmode a{ 138 | text-decoration: underline; 139 | vertical-align: initial; 140 | } 141 | #netjsonconfig-hint, #netjsonconfig-hint-advancedmode { width: auto } 142 | 143 | #container ul.sortedm2m-items{ height: auto; min-height: auto } 144 | #container .sortedm2m-container .help{ 145 | margin-bottom: 20px; 146 | color: #333; 147 | font-weight: bold; 148 | } 149 | 150 | #template_form #id_tags{ width: 261px } 151 | 152 | #device_form #id_model, 153 | #device_form #id_os, 154 | #device_form #id_system{ width: 610px } 155 | #device_form #id_notes{ height: 42px } 156 | 157 | #config-group .field-status .help{ 158 | white-space: pre; 159 | color: #777; 160 | } 161 | 162 | div.change-form #device_form div.inline-group.tab-content > fieldset.module > h2, 163 | div.change-form #device_form div.inline-group.tab-content > .tabular > fieldset.module > h2, 164 | div.change-form #device_form > div > fieldset.module.aligned, 165 | div.change-form #device_form > div > div.inline-group{ 166 | display: none; 167 | } 168 | 169 | .change-form #device_form div.inline-group.tab-content > fieldset.module > div.inline-related > h3{ 170 | margin-top: -1em; 171 | border-top: 0 none; 172 | padding: 1em; 173 | } 174 | 175 | .change-form #device_form div.inline-group.tab-content > .tabular{ 176 | margin-top: -1em; 177 | } 178 | 179 | ul.tabs{ 180 | margin: 0; 181 | padding: 0; 182 | list-style: none; 183 | } 184 | ul.tabs li{ 185 | display: inline-block; 186 | margin-right: 4px; 187 | } 188 | ul.tabs a, 189 | ul.tabs a:hover, 190 | ul.tabs a:focus{ 191 | display: inline-block; 192 | padding: 10px 15px; 193 | color: #333; 194 | background: #E6E6E6; 195 | border-radius: 3px; 196 | border-bottom-right-radius: 0; 197 | border-bottom-left-radius: 0; 198 | } 199 | 200 | ul.tabs a:hover, 201 | ul.tabs a:focus{ 202 | color: #000; 203 | background: #cdcdcd; 204 | } 205 | 206 | ul.tabs a.current{ 207 | background: #f8f8f8; 208 | color: #666; 209 | border: 1px solid rgba(0, 0, 0, 0.1); 210 | border-bottom: 0 none; 211 | border-color: rgba(0, 0, 0, 0.25); 212 | cursor: default; 213 | } 214 | 215 | div.tabs-divider{ 216 | margin-top: -4px; 217 | border-top: 1px solid rgba(0, 0, 0, 0.1); 218 | } 219 | 220 | .tab-content{ 221 | display: none; 222 | padding: 1em 0 0em 0em !important; 223 | } 224 | 225 | .tab-content.current{ 226 | display: block !important; 227 | } 228 | 229 | .tabs-loading{ 230 | text-align: center; 231 | padding: 17em 0; 232 | font-size: 16px; 233 | } 234 | 235 | @media (max-width: 767px) { 236 | ul.tabs li { 237 | display: block; 238 | } 239 | ul.tabs a, 240 | ul.tabs a:hover, 241 | ul.tabs a:focus { 242 | display: block; 243 | border-radius 0; 244 | width: 100%; 245 | padding-left: 0; 246 | padding-right: 0; 247 | border: 1px solid rgba(0, 0, 0, 0.1) !important; 248 | border-radius: 0; 249 | text-align: center; 250 | } 251 | div.tabs-divider{ display: none } 252 | } 253 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/css/lib/jsonschema-ui.css: -------------------------------------------------------------------------------- 1 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group{ margin-top: 15px } 2 | .jsoneditor-wrapper div.jsoneditor .grid-row:first-child > .inline-group{ margin-top: 0 } 3 | .form-row.field-config{ display: none } 4 | .form-row.field-config .advanced-mode{ display: none } 5 | .jsoneditor-wrapper div.jsoneditor-wrapper > fieldset{ 6 | margin-bottom: 0; 7 | border-bottom: 1px solid #eee; 8 | } 9 | .jsoneditor-wrapper div.jsoneditor-wrapper h2{ padding: 18px } 10 | input.deletelink{ background: #ba2121 } 11 | input.deletelink:hover, 12 | input.deletelink:focus{ background: #a41515 } 13 | .jsoneditor-wrapper div.jsoneditor input.button, 14 | .jsoneditor-wrapper div.jsoneditor input.deletelink, 15 | .form-row .json-editor-btn-edit{ 16 | margin-left: 10px; 17 | padding: 6px 12px; 18 | } 19 | a.json-editor-btn-edit{ 20 | margin-left: 10px; 21 | padding: 6px 12px; 22 | color: #ffffff; 23 | } 24 | 25 | .form-row .json-editor-btn-edit{ margin-bottom: 10px !important } 26 | .jsoneditor-wrapper div.jsoneditor .form-row{ padding: 15px 15px 15px 0 } 27 | .jsoneditor-wrapper div.jsoneditor label{ margin-left: 20px } 28 | .jsoneditor-wrapper div.jsoneditor .form-row:last-child{ border-bottom-width: 0 } 29 | .jsoneditor-wrapper div.jsoneditor .inline-group{ 30 | clear: both; 31 | margin: 0; 32 | border: 1px solid #eee; 33 | } 34 | 35 | /* avoid redundant borders */ 36 | .jsoneditor-wrapper div.jsoneditor .inline-group > .inline-related > .grid-container > div > .grid-row > .grid-column > div > .inline-group, 37 | .jsoneditor-wrapper div.jsoneditor .inline-group > .inline-related > .grid-container > div > .inline-group{ 38 | border: 0 none; 39 | } 40 | 41 | /* advanced mode and object properties */ 42 | .jsoneditor-wrapper div.jsoneditor > div > h3.controls{ 43 | padding: 12px 0; 44 | margin: 0; 45 | } 46 | .jsoneditor-wrapper div.jsoneditor > div > h3.controls .button{ margin: 0 13px } 47 | 48 | .jsoneditor-wrapper div.jsoneditor div[data-schematype="array"] > .inline-related > div > .inline-related, 49 | .jsoneditor-wrapper div.jsoneditor div[data-schematype="array"] > .inline-related > div > .inline-group{ 50 | margin: 20px; 51 | } 52 | .jsoneditor-wrapper div.jsoneditor div[data-schematype="array"] > .inline-related > div > .inline-related:last-child, 53 | .jsoneditor-wrapper div.jsoneditor div[data-schematype="array"] > .inline-related > div > .inline-group:last-child{ 54 | margin-bottom: 0 55 | } 56 | .jsoneditor-wrapper div.jsoneditor div[data-schematype="array"] > .inline-related > div > .inline-related[data-schematype="string"]{ 57 | margin: 0; 58 | } 59 | 60 | .jsoneditor-wrapper div.jsoneditor div[data-schemapath="root.interfaces"] > div > div > div { 61 | border-bottom: 2px dotted #ccc; 62 | } 63 | 64 | .jsoneditor-wrapper div.jsoneditor div.control{ margin: 15px 0 } 65 | .jsoneditor-wrapper div.jsoneditor h3{ 66 | height: 56px; 67 | line-height: 55px; 68 | padding: 0 12px 0 15px; 69 | overflow: hidden; 70 | } 71 | .jsoneditor-wrapper div.jsoneditor h3 span{ 72 | display: inline-block; 73 | margin-right: 15px; 74 | line-height: 0; 75 | } 76 | .jsoneditor-wrapper div.jsoneditor h3 span.control{ 77 | margin-top: 12px; 78 | float: right; 79 | } 80 | .jsoneditor-wrapper div.jsoneditor .grid-column > h3{ 81 | border: 0 none; 82 | outline: 1px solid #eee; 83 | } 84 | 85 | span.control input{ margin: 0 0 0 15px } 86 | .jsoneditor-wrapper div.jsoneditor span.control{ 87 | padding: 0; 88 | margin-right: 0; 89 | } 90 | 91 | .jsoneditor-wrapper div.jsoneditor div[data-schemapath="root.interfaces"] > div > div > div { 92 | border-bottom: 2px dotted #ccc; 93 | padding-bottom: 20px; 94 | margin-bottom: 22px !important; 95 | } 96 | .jsoneditor-wrapper div.jsoneditor div[data-schemapath="root.interfaces"] > div > div > div:last-child { 97 | margin-bottom: 0 !important; 98 | } 99 | 100 | .jsoneditor-wrapper div.jsoneditor div[data-schemapath="root.interfaces"] > div > div > div > .control{ 101 | margin-bottom: 0; 102 | } 103 | 104 | div[data-schematype="array"] > div > div > 105 | div[data-schematype="string"].inline-related .control{ 106 | margin: -49px 10px 0; 107 | float: right; 108 | clear: both; 109 | } 110 | div[data-schematype="array"] > div > div > 111 | div[data-schematype="string"].inline-related{ padding: 0 !important } 112 | 113 | /* hide empty divs */ 114 | .jsoneditor-wrapper div.jsoneditor div:empty{ display: none !important } 115 | 116 | /* begin custom properties adjustments */ 117 | .jsoneditor-wrapper div.jsoneditor .inline-group{ margin-bottom: 0 } 118 | .jsoneditor-wrapper div.jsoneditor .inline-group > label, 119 | .jsoneditor-wrapper div.jsoneditor .inline-group > select{ 120 | vertical-align: top; 121 | margin: 15px 0 10px 15px; 122 | } 123 | .jsoneditor-wrapper div.jsoneditor .inline-group > label{ 124 | font-style: italic; 125 | display: inline-block; 126 | clear: both; 127 | float: none; 128 | margin-left: 15px; 129 | margin-bottom: 0; 130 | } 131 | .inline-group[data-schematype="object"] > .inline-related > .grid-container > 132 | div > .grid-row > .inline-group > div > .inline-group{ 133 | border: 0 none !important; 134 | } 135 | div[data-schematype="object"] > .inline-related > .grid-container > 136 | div > .grid-row > .inline-group { 137 | border-bottom: 1px solid #eee; 138 | } 139 | 140 | .grid-row > .inline-group > div > .inline-group > div.form-row{ 141 | border: 0 none; 142 | padding: 0; 143 | margin: 0 0 15px 15px; 144 | } 145 | 146 | div[data-schematype="object"] > .inline-related > .grid-container > div > 147 | .grid-row > .inline-group > div > div{ 148 | border: 0 none; 149 | } 150 | 151 | .jsoneditor-wrapper div.jsoneditor > div > .inline-related > .grid-container > div > .grid-row > 152 | .inline-group > div { 153 | display: block; 154 | } 155 | 156 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > h3 span{ display: inline-block !important } 157 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > .inline-related > div > .inline-related > label, 158 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > .inline-related > div > .inline-related > select{ 159 | position: static; 160 | margin-bottom: 15px; 161 | margin-left: 0; 162 | } 163 | 164 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > .inline-related > div > .inline-related 165 | .grid-row > .inline-group{ 166 | border-top: 0 none; 167 | border-left: 0 none; 168 | border-right: 0 none; 169 | } 170 | 171 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > .inline-related > div > .inline-related 172 | .inline-group[data-schematype="array"] .inline-related > div > .inline-related > div > .inline-group .form-row{ 173 | margin-left: 15px; 174 | border: 0 none; 175 | } 176 | 177 | .jsoneditor-wrapper div.jsoneditor .grid-row > .inline-group > div > .inline-group > .inline-related > div > .inline-related 178 | .inline-group[data-schematype="array"] .inline-related > div > .inline-related .deletelink{ 179 | margin-left: 0; 180 | } 181 | /* end custom properties adjustments */ 182 | 183 | /* oneOf switcher */ 184 | .jsoneditor-wrapper div.jsoneditor .inline-related > label, 185 | .jsoneditor-wrapper div.jsoneditor .inline-related > select, 186 | .jsoneditor-wrapper div.jsoneditor .grid-column > label, 187 | .jsoneditor-wrapper div.jsoneditor .grid-column > select{ 188 | position: absolute; 189 | top: 14px; 190 | left: 0; 191 | z-index: 1; 192 | font-weight: bold; 193 | } 194 | .jsoneditor-wrapper div.jsoneditor .inline-related > select, 195 | .jsoneditor-wrapper div.jsoneditor .grid-column > select{ 196 | margin-left: 188px; 197 | background-color: #fff 198 | } 199 | .jsoneditor-wrapper div.jsoneditor .grid-row .grid-column{ position: relative } 200 | 201 | .jsoneditor-wrapper div.jsoneditor .modal{ 202 | position: absolute; 203 | z-index: 10; 204 | background-color: white; 205 | border: 1px solid #ddd; 206 | padding-bottom: 10px; 207 | width: 340px; 208 | margin-left: -215px; 209 | } 210 | /* header */ 211 | .jsoneditor-wrapper div.jsoneditor .advanced-mode{ margin-top: -4px !important } 212 | .normal-mode{ float: right } 213 | .jsoneditor-wrapper div.jsoneditor > div > h3.controls{ text-align: right } 214 | .jsoneditor-wrapper div.jsoneditor > div > h3.controls > .control{ float: left } 215 | /* configuration menu modal */ 216 | .jsoneditor-wrapper div.jsoneditor > div > h3.controls > .control > .modal{ 217 | margin-left: 0; 218 | width: auto; 219 | } 220 | .jsoneditor-wrapper div.jsoneditor .modal .vTextField[type=text], 221 | .jsoneditor-wrapper div.jsoneditor .modal .button{ 222 | margin-top: 12px !important; 223 | margin-bottom: 3px !important 224 | } 225 | 226 | .jsoneditor-wrapper div.jsoneditor input[type=checkbox]{ margin-right: 7px } 227 | .jsoneditor-wrapper div.jsoneditor textarea{ 228 | min-width: 75%; 229 | min-height: 330px 230 | } 231 | .jsoneditor-wrapper div.jsoneditor .modal textarea{ 232 | margin-bottom: 10px; 233 | min-width: 450px; 234 | } 235 | .jsoneditor-wrapper div.jsoneditor .modal label{ margin-left: 5px } 236 | .jsoneditor-wrapper div.jsoneditor .property-selector{ 237 | max-height: 240px !important; 238 | width: auto !important; 239 | } 240 | .jsoneditor-wrapper div.jsoneditor .property-selector{ 241 | padding: 10px !important; 242 | } 243 | .jsoneditor-wrapper div.jsoneditor .property-selector input{ 244 | margin: 0 8px 0 0 245 | } 246 | .jsoneditor-wrapper div.jsoneditor .property-selector .form-row{ 247 | padding: 2px 10px !important 248 | } 249 | 250 | .jsoneditor-wrapper div.jsoneditor .errorlist{ 251 | margin-top: 3px; 252 | margin-left: 181px; 253 | } 254 | 255 | .jsoneditor-wrapper div.jsoneditor div.form-row > .help{ 256 | margin-left: 181px; 257 | margin-top: 4px; 258 | font-size: 13px; 259 | color: #888; 260 | } 261 | 262 | .jsoneditor-wrapper div.jsoneditor .inline-group > .help, 263 | .jsoneditor-wrapper div.jsoneditor .grid-column > .help{ 264 | padding: 20px 15px 20px 18px; 265 | margin: 0; 266 | border-bottom: 1px solid #eee; 267 | font-size: 13px; 268 | } 269 | 270 | .jsoneditor-wrapper div.jsoneditor .modal label{ width: auto } 271 | 272 | .jsoneditor-wrapper div.jsoneditor div.grid-column[data-schematype="boolean"] label{ 273 | float: left; 274 | } 275 | .jsoneditor-wrapper div.jsoneditor div.grid-column[data-schematype="boolean"] .help{ 276 | float: left; 277 | clear: none; 278 | margin: 0; 279 | } 280 | 281 | #id_config_jsoneditor, 282 | #id_config-0-config_jsoneditor{ 283 | border: 0px !important; 284 | height: auto !important; 285 | } 286 | 287 | /* support django admin inline */ 288 | 289 | div.jsoneditor > div > h3.controls{ 290 | background: transparent !important; 291 | } 292 | 293 | div.jsoneditor .inline-related h3 { 294 | color: #666 !important; 295 | background-color: #f8f8f8 !important; 296 | border-top: 1px solid #eee !important; 297 | border-bottom: 1px solid #eee !important; 298 | font-size: 13px !important; 299 | } 300 | 301 | #config-0.inline-related{ position: static } 302 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/lib/tomorrow_night_bright.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/tomorrow_night_bright",["require","exports","module","ace/lib/dom"], function(acequire, exports, module) { 2 | 3 | exports.isDark = true; 4 | exports.cssClass = "ace-tomorrow-night-bright"; 5 | exports.cssText = ".ace-tomorrow-night-bright .ace_gutter {\ 6 | background: #1a1a1a;\ 7 | color: #DEDEDE\ 8 | }\ 9 | .ace-tomorrow-night-bright .ace_print-margin {\ 10 | width: 1px;\ 11 | background: #1a1a1a\ 12 | }\ 13 | .ace-tomorrow-night-bright {\ 14 | background-color: #000000;\ 15 | color: #DEDEDE\ 16 | }\ 17 | .ace-tomorrow-night-bright .ace_cursor {\ 18 | color: #9F9F9F\ 19 | }\ 20 | .ace-tomorrow-night-bright .ace_marker-layer .ace_selection {\ 21 | background: #424242\ 22 | }\ 23 | .ace-tomorrow-night-bright.ace_multiselect .ace_selection.ace_start {\ 24 | box-shadow: 0 0 3px 0px #000000;\ 25 | }\ 26 | .ace-tomorrow-night-bright .ace_marker-layer .ace_step {\ 27 | background: rgb(102, 82, 0)\ 28 | }\ 29 | .ace-tomorrow-night-bright .ace_marker-layer .ace_bracket {\ 30 | margin: -1px 0 0 -1px;\ 31 | border: 1px solid #888888\ 32 | }\ 33 | .ace-tomorrow-night-bright .ace_marker-layer .ace_highlight {\ 34 | border: 1px solid rgb(110, 119, 0);\ 35 | border-bottom: 0;\ 36 | box-shadow: inset 0 -1px rgb(110, 119, 0);\ 37 | margin: -1px 0 0 -1px;\ 38 | background: rgba(255, 235, 0, 0.1)\ 39 | }\ 40 | .ace-tomorrow-night-bright .ace_marker-layer .ace_active-line {\ 41 | background: #2A2A2A\ 42 | }\ 43 | .ace-tomorrow-night-bright .ace_gutter-active-line {\ 44 | background-color: #2A2A2A\ 45 | }\ 46 | .ace-tomorrow-night-bright .ace_stack {\ 47 | background-color: rgb(66, 90, 44)\ 48 | }\ 49 | .ace-tomorrow-night-bright .ace_marker-layer .ace_selected-word {\ 50 | border: 1px solid #888888\ 51 | }\ 52 | .ace-tomorrow-night-bright .ace_invisible {\ 53 | color: #343434\ 54 | }\ 55 | .ace-tomorrow-night-bright .ace_keyword,\ 56 | .ace-tomorrow-night-bright .ace_meta,\ 57 | .ace-tomorrow-night-bright .ace_storage,\ 58 | .ace-tomorrow-night-bright .ace_storage.ace_type,\ 59 | .ace-tomorrow-night-bright .ace_support.ace_type {\ 60 | color: #C397D8\ 61 | }\ 62 | .ace-tomorrow-night-bright .ace_keyword.ace_operator {\ 63 | color: #70C0B1\ 64 | }\ 65 | .ace-tomorrow-night-bright .ace_constant.ace_character,\ 66 | .ace-tomorrow-night-bright .ace_constant.ace_language,\ 67 | .ace-tomorrow-night-bright .ace_constant.ace_numeric,\ 68 | .ace-tomorrow-night-bright .ace_keyword.ace_other.ace_unit,\ 69 | .ace-tomorrow-night-bright .ace_support.ace_constant,\ 70 | .ace-tomorrow-night-bright .ace_variable.ace_parameter {\ 71 | color: #E78C45\ 72 | }\ 73 | .ace-tomorrow-night-bright .ace_constant.ace_other {\ 74 | color: #EEEEEE\ 75 | }\ 76 | .ace-tomorrow-night-bright .ace_invalid {\ 77 | color: #CED2CF;\ 78 | background-color: #DF5F5F\ 79 | }\ 80 | .ace-tomorrow-night-bright .ace_invalid.ace_deprecated {\ 81 | color: #CED2CF;\ 82 | background-color: #B798BF\ 83 | }\ 84 | .ace-tomorrow-night-bright .ace_fold {\ 85 | background-color: #7AA6DA;\ 86 | border-color: #DEDEDE\ 87 | }\ 88 | .ace-tomorrow-night-bright .ace_entity.ace_name.ace_function,\ 89 | .ace-tomorrow-night-bright .ace_support.ace_function,\ 90 | .ace-tomorrow-night-bright .ace_variable {\ 91 | color: #7AA6DA\ 92 | }\ 93 | .ace-tomorrow-night-bright .ace_support.ace_class,\ 94 | .ace-tomorrow-night-bright .ace_support.ace_type {\ 95 | color: #E7C547\ 96 | }\ 97 | .ace-tomorrow-night-bright .ace_heading,\ 98 | .ace-tomorrow-night-bright .ace_markup.ace_heading,\ 99 | .ace-tomorrow-night-bright .ace_string {\ 100 | color: #B9CA4A\ 101 | }\ 102 | .ace-tomorrow-night-bright .ace_entity.ace_name.ace_tag,\ 103 | .ace-tomorrow-night-bright .ace_entity.ace_other.ace_attribute-name,\ 104 | .ace-tomorrow-night-bright .ace_meta.ace_tag,\ 105 | .ace-tomorrow-night-bright .ace_string.ace_regexp,\ 106 | .ace-tomorrow-night-bright .ace_variable {\ 107 | color: #D54E53\ 108 | }\ 109 | .ace-tomorrow-night-bright .ace_comment {\ 110 | color: #969896\ 111 | }\ 112 | .ace-tomorrow-night-bright .ace_c9searchresults.ace_keyword {\ 113 | color: #C2C280\ 114 | }\ 115 | .ace-tomorrow-night-bright .ace_indent-guide {\ 116 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYFBXV/8PAAJoAXX4kT2EAAAAAElFTkSuQmCC) right repeat-y\ 117 | }"; 118 | 119 | var dom = acequire("../lib/dom"); 120 | dom.importCssString(exports.cssText, exports.cssClass); 121 | }); 122 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/lib/utils.js: -------------------------------------------------------------------------------- 1 | var cleanedData, 2 | pattern = /^\{\{\s*(\w*)\s*\}\}$/g, 3 | getContext, 4 | evaluateVars, 5 | cleanData, 6 | context_json_valid, 7 | span = document.createElement('span'); 8 | 9 | span.setAttribute('style', 'color:red'); 10 | span.setAttribute('id', 'context-error'); 11 | 12 | getContext = function () { 13 | var context_div = document.querySelectorAll(".field-context, .field-default_values")[0]; 14 | if (context_div && !context_div.querySelector('span')) { 15 | context_div.appendChild(span); 16 | } 17 | return document.querySelectorAll("#id_config-0-context, #id_default_values")[0]; 18 | }; 19 | 20 | // check default_values is valid 21 | context_json_valid = function () { 22 | var json = getContext(); 23 | try { 24 | JSON.parse(json.value); 25 | } catch (e) { 26 | span.innerHTML = "Invalid JSON: " + e.message; 27 | return false; 28 | } 29 | span.innerHTML = ""; 30 | return true; 31 | } 32 | 33 | evaluateVars = function (data, context) { 34 | if (typeof data === 'object') { 35 | Object.keys(data).forEach(function (key) { 36 | data[key] = evaluateVars(data[key], context); 37 | }); 38 | } 39 | if (typeof data === 'string') { 40 | var found_vars = data.match(pattern); 41 | if (found_vars !== null) { 42 | found_vars.forEach(function (element) { 43 | element = element.replace(/^\{\{\s+|\s+\}\}$|^\{\{|\}\}$/g, ''); 44 | if (context.hasOwnProperty(element)) { 45 | data = data.replace(pattern, context[element]); 46 | } 47 | }); 48 | } 49 | } 50 | return data; 51 | }; 52 | 53 | cleanData = function (data) { 54 | var json = getContext(); 55 | if (json && data && context_json_valid()) { 56 | cleanedData = evaluateVars(data, JSON.parse(json.value)); 57 | return cleanedData; 58 | } else { 59 | return data; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/preview.js: -------------------------------------------------------------------------------- 1 | django.jQuery(function($) { 2 | var overlay = $('.djnjc-overlay'), 3 | body = $('body'), 4 | inner = overlay.find('.inner'), 5 | preview_url = $('.previewlink').attr('data-url'); 6 | var openPreview = function() { 7 | var selectors = 'input[type=text], input[type=hidden], select, textarea', 8 | fields = $(selectors, '#content-main form').not('#id_config_jsoneditor *'), 9 | $id = $('#id_id'), 10 | data = {}, 11 | loadingOverlay = $('#loading-overlay'); 12 | loadingOverlay.show(); 13 | // add id to POST data 14 | // note: may be overridden by fields of OneToOne relation 15 | if ($id.length) { data['id'] = $id.val(); } 16 | // gather data to send in POST 17 | fields.each(function(i, field){ 18 | var $field = $(field), 19 | name = $field.attr('name'); 20 | // skip management fields 21 | if(!name || 22 | name.indexOf('initial-') === 0 || 23 | name.indexOf('config-__') === 0 || 24 | name.indexOf('_FORMS') != -1){ return; } 25 | // rename fields of OneToOne relation 26 | if(name.indexOf('config-0-') === 0){ 27 | name = name.replace('config-0-', ''); 28 | } 29 | data[name] = $field.val(); 30 | }); 31 | // show preview 32 | $.post(preview_url, data, function(html){ 33 | inner.html($('#content-main div', html).html()); 34 | overlay.show(); 35 | body.css('overflow', 'hidden'); 36 | overlay.find('pre').trigger('click'); 37 | // close preview 38 | overlay.find('.close').click(function(e){ 39 | e.preventDefault(); 40 | closePreview(); 41 | }); 42 | loadingOverlay.fadeOut(250); 43 | }) 44 | .fail (function(xhr){ 45 | // if validation error, show it on page 46 | if (xhr.status == 400) { 47 | alert('There was an issue while generating the preview \n' + 48 | 'Details: ' + xhr.responseText); 49 | } 50 | // 500 internal server error 51 | // rare case, leaving it untranslated for simplicity 52 | else{ 53 | var message = 'Error while generating preview'; 54 | if (gettext) { message = gettext(message); } 55 | alert(message + ':\n\n' + xhr.responseText); 56 | } 57 | }); 58 | }; 59 | var closePreview = function () { 60 | overlay.hide(); 61 | inner.html(''); 62 | body.attr('style', ''); 63 | }; 64 | $('.previewlink').click(function(e){ 65 | var configUi = $('#id_config_jsoneditor, #id_config-0-config_jsoneditor'), 66 | message; 67 | e.preventDefault(); 68 | // show preview only if there's a configuration 69 | // (device items may not have one) 70 | if(configUi.length){ 71 | openPreview(); 72 | } 73 | else{ 74 | message = 'No configuration available'; 75 | if (gettext) { message = gettext(message); } 76 | alert(message); 77 | } 78 | }); 79 | $(document).keyup(function(e) { 80 | // ALT+P 81 | if (e.altKey && e.which == 80) { 82 | // unfocus any active input before proceeding 83 | $(document.activeElement).trigger('blur'); 84 | // wait for JSON editor to update the 85 | // corresonding raw value before proceding 86 | setTimeout(openPreview, 15); 87 | } 88 | // ESC 89 | else if (!e.ctrlKey && e.which == 27) { 90 | closePreview(); 91 | } 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/switcher.js: -------------------------------------------------------------------------------- 1 | django.jQuery(function($) { 2 | var type_select = $('#id_type'), 3 | vpn_specific = $('.field-vpn, .field-auto_cert'), 4 | gettext = window.gettext || function(v){ return v; }, 5 | toggle_vpn_specific = function(changed){ 6 | if (type_select.val() == 'vpn') { 7 | vpn_specific.show(); 8 | if (changed === true && $('.autovpn').length < 1 && $('#id_config').val() === '{}') { 9 | var p1 = gettext('Click on Save to automatically generate the ' + 10 | 'VPN client configuration (will be based on ' + 11 | 'the configuration of the server).'), 12 | p2 = gettext('You can then tweak the VPN client ' + 13 | 'configuration in the next step.'); 14 | $('.jsoneditor-wrapper').hide() 15 | .after('
'); 16 | $('.autovpn').html('

' + p1 + '

' + 17 | '

' + p2 + '

'); 18 | } 19 | } 20 | else{ 21 | vpn_specific.hide(); 22 | if ($('.autovpn').length > 0) { 23 | $('.jsoneditor-wrapper').show(); 24 | $('.autovpn').hide(); 25 | } 26 | } 27 | }; 28 | type_select.on('change', function(){ 29 | toggle_vpn_specific(true); 30 | }); 31 | toggle_vpn_specific(); 32 | }); 33 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/tabs.js: -------------------------------------------------------------------------------- 1 | django.jQuery(function ($) { 2 | 'use strict'; 3 | if ($('.add-form').length || !$('#device_form').length) { 4 | return; 5 | } 6 | 7 | // trigger window resize event 8 | // workaround that fixes problems with leafelet maps 9 | var triggerResize = function () { 10 | var resizeEvent = window.document.createEvent('UIEvents'); 11 | resizeEvent.initUIEvent('resize', true, false, window, 0); 12 | window.dispatchEvent(resizeEvent); 13 | }, 14 | showTab = function(menuLink){ 15 | var tabId = menuLink.attr('href'); 16 | $('ul.tabs a').removeClass('current'); 17 | $('.tab-content').removeClass('current'); 18 | menuLink.addClass('current'); 19 | $(tabId).addClass('current'); 20 | triggerResize(); 21 | $.event.trigger({ 22 | type: 'tabshown', 23 | tabId: tabId, 24 | }); 25 | return tabId; 26 | }, 27 | showFragment = function(fragment) { 28 | if (!fragment) { return; } 29 | showTab($('ul.tabs a[href="' + fragment + '"]')); 30 | }; 31 | 32 | $('ul.tabs a').click(function(e){ 33 | var tabId = showTab($(this)); 34 | e.preventDefault(); 35 | history.pushState(tabId, '', tabId); 36 | }); 37 | 38 | var overview = $('#device_form > div > fieldset.module.aligned') 39 | .addClass('tab-content') 40 | .attr('id', 'overview-group'), 41 | tabs = $('#device_form > div > div.inline-group') 42 | .addClass('tab-content'), 43 | tabsContainer = $('#tabs-container ul'); 44 | tabs.each(function(i, el) { 45 | var $el = $(el), 46 | tabId = $el.attr('id'), 47 | label = $el.find('> fieldset.module > h2, ' + 48 | '> .tabular > fieldset.module > h2').text(); 49 | tabsContainer.append( 50 | '
  • ' + label + '
  • ' 51 | ); 52 | }); 53 | 54 | $('.tabs-loading').hide(); 55 | 56 | // open fragment 57 | $(window).on('hashchange', function (e) { 58 | showFragment(window.location.hash); 59 | }); 60 | 61 | // open fragment on page opening if present 62 | if (window.location.hash) { 63 | showFragment(window.location.hash); 64 | } else { 65 | $('ul.tabs li:first-child a').addClass('current'); 66 | overview.addClass('current'); 67 | } 68 | 69 | // if there's any validation error, show the first one 70 | var errors = $('.errorlist'); 71 | if (errors.length) { 72 | var erroredTab = errors.eq(0).parents('.tab-content'); 73 | if (erroredTab.length) { 74 | window.location.hash = '#' + erroredTab.attr('id'); 75 | } 76 | } 77 | 78 | $('#loading-overlay').fadeOut(400); 79 | }); 80 | -------------------------------------------------------------------------------- /django_netjsonconfig/static/django-netjsonconfig/js/unsaved_changes.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var form = '#content-main form', 3 | mapValues = function(object) { 4 | $('input, select, textarea', form).each(function(i, el){ 5 | var field = $(el), 6 | name = field.attr('name'), 7 | value = field.val(); 8 | // ignore fields that have no name attribute, begin with "_" or "initial-" 9 | if (!name || name.substr(0, 1) == '_' || name.substr(0, 8) == 'initial-' || 10 | // ignore hidden fields 11 | name == 'csrfmiddlewaretoken' || 12 | // ignore hidden inline helper fields 13 | name.indexOf('__prefix__') >= 0 || 14 | name.indexOf('root') === 0) { 15 | return; 16 | } 17 | // fix checkbox values inconsistency 18 | if (field.attr('type') == 'checkbox') { 19 | object[name] = field.is(':checked'); 20 | } 21 | else { 22 | object[name] = value; 23 | } 24 | // convert JSON string to Javascript object in order 25 | // to perform object comparison with `objectIsEqual` 26 | if (name == 'config' || name == 'config-0-config' || name == 'config-0-context') { 27 | try{ 28 | object[name] = JSON.parse(value); 29 | } 30 | catch(ignore){} 31 | } 32 | }); 33 | }; 34 | 35 | var unsaved_changes = function(e) { 36 | // get current values 37 | var current_values = {}; 38 | mapValues(current_values); 39 | var changed = false, 40 | message = 'You haven\'t saved your changes yet!', 41 | initialField, initialValue, 42 | name; 43 | if (gettext) { message = gettext(message); } // i18n if enabled 44 | // compare initial with current values 45 | for (name in django._njc_initial_values) { 46 | // use initial values from initial fields if present 47 | initialField = $('#initial-id_' + name); 48 | initialValue = initialField.length ? initialField.val() : django._njc_initial_values[name]; 49 | // fix checkbox value inconsistency 50 | if (initialValue == 'True') { initialValue = true; } 51 | else if (initialValue == 'False') { initialValue = false; } 52 | if (name == 'config') { initialValue = JSON.parse(initialValue); } 53 | 54 | if (!objectIsEqual(initialValue, current_values[name])) { 55 | changed = true; 56 | break; 57 | } 58 | } 59 | if (changed) { 60 | e.returnValue = message; 61 | return message; 62 | } 63 | }; 64 | 65 | // compares equality of two objects 66 | var objectIsEqual = function(obj1, obj2) { 67 | if (typeof obj1 != 'object' && typeof obj2 != 'object') { 68 | return obj1 == obj2; 69 | } 70 | 71 | // jslint doesn't like comparing typeof with a non-constant 72 | // see https://stackoverflow.com/a/18526510 73 | var obj1Type = typeof obj1, 74 | obj2Type = typeof obj2; 75 | if (obj1Type != obj2Type) { 76 | return false; 77 | } 78 | var p; 79 | for(p in obj1) { 80 | switch(typeof obj1[p]) { 81 | case 'object': 82 | if (!objectIsEqual(obj1[p], obj2[p])) { return false; } break; 83 | default: 84 | if (obj1[p] != obj2[p]) { return false; } 85 | } 86 | } 87 | for(p in obj2) { 88 | if(obj1[p] === undefined) { return false; } 89 | } 90 | return true; 91 | }; 92 | 93 | $(function ($) { 94 | if (!$('.submit-row').length) { return; } 95 | // populate initial map of form values 96 | django._njc_initial_values = {}; 97 | mapValues(django._njc_initial_values); 98 | // do not perform unsaved_changes if submitting form 99 | $(form).submit(function() { 100 | $(window).unbind('beforeunload', unsaved_changes); 101 | }); 102 | // bind unload event 103 | $(window).bind('beforeunload', unsaved_changes); 104 | }); 105 | }(django.jQuery)); 106 | -------------------------------------------------------------------------------- /django_netjsonconfig/templates/admin/django_netjsonconfig/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_modify %} 3 | 4 | {% block object-tools-items %} 5 |
  • 6 | {% if download_url %} 7 | {% trans "Download configuration" %} 8 | {% endif %} 9 |
  • 10 | {{ block.super }} 11 | {% endblock %} 12 | 13 | {% block submit_buttons_top %}{% submit_row %}{% endblock %} 14 | {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} 15 | 16 | {% block field_sets %} 17 | {% if not add %} 18 |
    20 |
    21 |
    22 | 23 |
    24 | 30 |
    31 |
    32 | {% endif %} 33 | {{ block.super }} 34 | {% endblock %} 35 | 36 | {% block content %} 37 |
    38 | {{ block.super }} 39 |
    40 |
    41 |
    42 | 43 | {% block default_templates_js %} 44 | {% if default_templates %} 45 | 51 | {% endif %} 52 | {% endblock %} 53 |
    54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /django_netjsonconfig/templates/admin/django_netjsonconfig/preview.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_urls %} 3 | 4 | {% block content %} 5 |
    6 |
    7 | × {% trans 'Close' %} 8 | {% if not error %} 9 |
    {{ output }}
    10 | {% else %} 11 |
    {{ error }}
    12 | {% endif %} 13 |
    14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | test utilities shared among test classes 3 | these mixins are reused also in openwisp2 4 | change with care. 5 | """ 6 | from uuid import uuid4 7 | 8 | from django_x509.tests import TestX509Mixin 9 | 10 | 11 | class CreateDeviceMixin(object): 12 | TEST_MAC_ADDRESS = '00:11:22:33:44:55' 13 | 14 | def _create_device(self, **kwargs): 15 | options = dict( 16 | name='test-device', 17 | mac_address=self.TEST_MAC_ADDRESS, 18 | hardware_id=str(uuid4().hex), 19 | model='TP-Link TL-WDR4300 v1', 20 | os='LEDE Reboot 17.01-SNAPSHOT r3313-c2999ef', 21 | ) 22 | options.update(kwargs) 23 | d = self.device_model(**options) 24 | d.full_clean() 25 | d.save() 26 | return d 27 | 28 | def _create_device_config(self, device_opts=None, config_opts=None): 29 | device_opts = device_opts or {} 30 | config_opts = config_opts or {} 31 | device_opts['name'] = 'test' 32 | d = self._create_device(**device_opts) 33 | config_opts['device'] = d 34 | self._create_config(**config_opts) 35 | return d 36 | 37 | 38 | class CreateConfigMixin(CreateDeviceMixin): 39 | TEST_KEY = 'w1gwJxKaHcamUw62TQIPgYchwLKn3AA0' 40 | 41 | def _create_config(self, **kwargs): 42 | options = dict(backend='netjsonconfig.OpenWrt', config={'general': {}}) 43 | options.update(kwargs) 44 | if 'device' not in kwargs: 45 | options['device'] = self._create_device(name='test-device') 46 | c = self.config_model(**options) 47 | c.full_clean() 48 | c.save() 49 | return c 50 | 51 | 52 | class CreateTemplateMixin(object): 53 | def _create_template(self, **kwargs): 54 | model_kwargs = { 55 | "name": "test-template", 56 | "backend": "netjsonconfig.OpenWrt", 57 | "config": {"interfaces": [{"name": "eth0", "type": "ethernet"}]}, 58 | } 59 | model_kwargs.update(kwargs) 60 | t = self.template_model(**model_kwargs) 61 | t.full_clean() 62 | t.save() 63 | return t 64 | 65 | 66 | class CreateVpnMixin(object): 67 | _dh = """-----BEGIN DH PARAMETERS----- 68 | MIGHAoGBAMkiqC2kAkjhysnuBORxJgDMdq3JrvaNh1kZW0IkFiyLRyhtYf92atP4 69 | ycYELVoRZoRZ8zp2Y2L71vHRNx5okiXZ1xRWDfEVp7TFVc+oCTTRwJqyq21/DJpe 70 | Qt01H2yL7CvdEUi/gCUJNS9Jm40248nwKgyrwyoS3SjY49CAcEYLAgEC 71 | -----END DH PARAMETERS-----""" 72 | _vpn_config = { 73 | "openvpn": [ 74 | { 75 | "ca": "ca.pem", 76 | "cert": "cert.pem", 77 | "dev": "tap0", 78 | "dev_type": "tap", 79 | "dh": "dh.pem", 80 | "key": "key.pem", 81 | "mode": "server", 82 | "name": "example-vpn", 83 | "proto": "udp", 84 | "tls_server": True, 85 | } 86 | ] 87 | } 88 | 89 | def _create_vpn(self, ca_options={}, **kwargs): 90 | options = dict( 91 | name='test', 92 | host='vpn1.test.com', 93 | ca=None, 94 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 95 | config=self._vpn_config, 96 | dh=self._dh, 97 | ) 98 | options.update(**kwargs) 99 | if not options['ca']: 100 | options['ca'] = self._create_ca(**ca_options) 101 | vpn = self.vpn_model(**options) 102 | vpn.full_clean() 103 | vpn.save() 104 | return vpn 105 | 106 | 107 | class TestVpnX509Mixin(CreateVpnMixin, TestX509Mixin): 108 | pass 109 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/test_device.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from unittest import mock 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.test import TestCase 6 | 7 | from .. import settings as app_settings 8 | from ..models import Config, Device 9 | from ..validators import device_name_validator, mac_address_validator 10 | from . import CreateConfigMixin 11 | 12 | 13 | class TestDevice(CreateConfigMixin, TestCase): 14 | """ 15 | tests for Device model 16 | """ 17 | 18 | config_model = Config 19 | device_model = Device 20 | 21 | @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_AS_NAME', False) 22 | def test_str_name(self): 23 | d = Device(name='test') 24 | self.assertEqual(str(d), 'test') 25 | 26 | @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_AS_NAME', True) 27 | def test_str_hardware_id(self): 28 | d = Device(name='test', hardware_id='123') 29 | self.assertEqual(str(d), '123') 30 | 31 | def test_mac_address_validator(self): 32 | d = Device(name='test', key=self.TEST_KEY) 33 | bad_mac_addresses_list = [ 34 | '{0}:BB:CC'.format(self.TEST_MAC_ADDRESS), 35 | 'AA:BB:CC:11:22033', 36 | 'AA BB CC 11 22 33', 37 | ] 38 | for mac_address in bad_mac_addresses_list: 39 | d.mac_address = mac_address 40 | try: 41 | d.full_clean() 42 | except ValidationError as e: 43 | self.assertIn('mac_address', e.message_dict) 44 | self.assertEqual( 45 | mac_address_validator.message, e.message_dict['mac_address'][0] 46 | ) 47 | else: 48 | self.fail('ValidationError not raised for "{0}"'.format(mac_address)) 49 | 50 | def test_config_status_modified(self): 51 | c = self._create_config(device=self._create_device(), status='applied') 52 | self.assertEqual(c.status, 'applied') 53 | c.device.name = 'test-status-modified' 54 | c.device.full_clean() 55 | c.device.save() 56 | c.refresh_from_db() 57 | self.assertEqual(c.status, 'modified') 58 | 59 | def test_key_validator(self): 60 | d = Device(name='test', mac_address=self.TEST_MAC_ADDRESS, hardware_id='1234') 61 | d.key = 'key/key' 62 | with self.assertRaises(ValidationError): 63 | d.full_clean() 64 | d.key = 'key.key' 65 | with self.assertRaises(ValidationError): 66 | d.full_clean() 67 | d.key = 'key key' 68 | with self.assertRaises(ValidationError): 69 | d.full_clean() 70 | d.key = self.TEST_KEY 71 | d.full_clean() 72 | 73 | def test_backend(self): 74 | d = self._create_device() 75 | self.assertIsNone(d.backend) 76 | c = self._create_config(device=d) 77 | self.assertIsNotNone(d.backend) 78 | self.assertEqual(d.backend, c.get_backend_display()) 79 | 80 | def test_status(self): 81 | d = self._create_device() 82 | self.assertEqual(d.status, None) 83 | c = self._create_config(device=d) 84 | self.assertIsNotNone(d.status) 85 | self.assertEqual(d.status, c.get_status_display()) 86 | 87 | def test_config_model(self): 88 | d = Device() 89 | self.assertIs(d.get_config_model(), Config) 90 | 91 | def test_config_model_static(self): 92 | self.assertIs(Device.get_config_model(), Config) 93 | 94 | def test_get_default_templates(self): 95 | d = self._create_device() 96 | self.assertEqual( 97 | d.get_default_templates().count(), Config().get_default_templates().count() 98 | ) 99 | self._create_config(device=d) 100 | self.assertEqual( 101 | d.get_default_templates().count(), Config().get_default_templates().count() 102 | ) 103 | 104 | def test_bad_hostnames(self): 105 | bad_host_name_list = [ 106 | 'test device', 107 | 'openwisp..mydomain.com', 108 | 'openwisp,mydomain.test', 109 | '{0}:BB:CC'.format(self.TEST_MAC_ADDRESS), 110 | 'AA:BB:CC:11:22033', 111 | ] 112 | for host in bad_host_name_list: 113 | try: 114 | self._create_device(name=host) 115 | except ValidationError as e: 116 | self.assertIn('name', e.message_dict) 117 | self.assertEqual( 118 | device_name_validator.message, e.message_dict['name'][0] 119 | ) 120 | else: 121 | self.fail('ValidationError not raised for "{0}"'.format(host)) 122 | 123 | def test_add_device_with_context(self): 124 | d = self._create_device() 125 | d.save() 126 | c = self._create_config( 127 | device=d, 128 | config={ 129 | "openwisp": [ 130 | { 131 | "config_name": "controller", 132 | "config_value": "http", 133 | "url": "http://controller.examplewifiservice.com", 134 | "interval": "{{ interval }}", 135 | "verify_ssl": "1", 136 | "uuid": "UUID", 137 | "key": self.TEST_KEY, 138 | } 139 | ] 140 | }, 141 | context={'interval': '60'}, 142 | ) 143 | self.assertEqual(c.json(dict=True)['openwisp'][0]['interval'], '60') 144 | 145 | def test_get_context_with_config(self): 146 | d = self._create_device() 147 | c = self._create_config(device=d) 148 | self.assertEqual(d.get_context(), c.get_context()) 149 | 150 | def test_get_context_without_config(self): 151 | d = self._create_device() 152 | self.assertEqual(d.get_context(), Config(device=d).get_context()) 153 | 154 | @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', False) 155 | def test_generate_random_key(self): 156 | d = self.device_model(name='test_generate_key', mac_address='00:11:22:33:44:55') 157 | self.assertIsNone(d.key) 158 | # generating key twice shall not yield same result 159 | self.assertNotEqual( 160 | d.generate_key(app_settings.SHARED_SECRET), 161 | d.generate_key(app_settings.SHARED_SECRET), 162 | ) 163 | 164 | @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', True) 165 | @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_ENABLED', False) 166 | def test_generate_consistent_key_mac_address(self): 167 | d = self.device_model(name='test_generate_key', mac_address='00:11:22:33:44:55') 168 | self.assertIsNone(d.key) 169 | string = '{}+{}'.format(d.mac_address, app_settings.SHARED_SECRET).encode( 170 | 'utf-8' 171 | ) 172 | expected = md5(string).hexdigest() 173 | key = d.generate_key(app_settings.SHARED_SECRET) 174 | self.assertEqual(key, expected) 175 | self.assertEqual(key, d.generate_key(app_settings.SHARED_SECRET)) 176 | 177 | @mock.patch('django_netjsonconfig.settings.CONSISTENT_REGISTRATION', True) 178 | @mock.patch('django_netjsonconfig.settings.HARDWARE_ID_ENABLED', True) 179 | def test_generate_consistent_key_mac_hardware_id(self): 180 | d = self.device_model( 181 | name='test_generate_key', 182 | mac_address='00:11:22:33:44:55', 183 | hardware_id='1234', 184 | ) 185 | self.assertIsNone(d.key) 186 | string = '{}+{}'.format(d.hardware_id, app_settings.SHARED_SECRET).encode( 187 | 'utf-8' 188 | ) 189 | expected = md5(string).hexdigest() 190 | key = d.generate_key(app_settings.SHARED_SECRET) 191 | self.assertEqual(key, expected) 192 | self.assertEqual(key, d.generate_key(app_settings.SHARED_SECRET)) 193 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/test_tag.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..models import Template 4 | from . import CreateTemplateMixin 5 | 6 | 7 | class TestTag(CreateTemplateMixin, TestCase): 8 | """ 9 | tests for Tag model 10 | """ 11 | 12 | template_model = Template 13 | 14 | def test_tag(self): 15 | t = self._create_template() 16 | t.tags.add('mesh') 17 | self.assertEqual(t.tags.filter(name='mesh').count(), 1) 18 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/test_template.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.core.exceptions import ValidationError 4 | from django.test import TestCase 5 | from django_x509.models import Ca 6 | 7 | from netjsonconfig import OpenWrt 8 | from openwisp_utils.tests import catch_signal 9 | 10 | from ..models import Config, Device, Template, Vpn 11 | from ..signals import config_modified, config_status_changed 12 | from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin 13 | 14 | 15 | class TestTemplate(CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin, TestCase): 16 | """ 17 | tests for Template model 18 | """ 19 | 20 | ca_model = Ca 21 | config_model = Config 22 | device_model = Device 23 | template_model = Template 24 | vpn_model = Vpn 25 | 26 | def test_str(self): 27 | t = Template(name='test', backend='netjsonconfig.OpenWrt') 28 | self.assertEqual(str(t), 'test') 29 | 30 | def test_backend_class(self): 31 | t = Template(name='test', backend='netjsonconfig.OpenWrt') 32 | self.assertIs(t.backend_class, OpenWrt) 33 | 34 | def test_backend_instance(self): 35 | config = {'general': {'hostname': 'template'}} 36 | t = Template(name='test', backend='netjsonconfig.OpenWrt', config=config) 37 | self.assertIsInstance(t.backend_instance, OpenWrt) 38 | 39 | def test_validation(self): 40 | config = {'interfaces': {'invalid': True}} 41 | t = Template(name='test', backend='netjsonconfig.OpenWrt', config=config) 42 | # ensure django ValidationError is raised 43 | with self.assertRaises(ValidationError): 44 | t.full_clean() 45 | 46 | def test_config_status_modified_after_change(self): 47 | t = self._create_template() 48 | c = self._create_config(device=self._create_device(name='test-status')) 49 | self.assertEqual(c.status, 'modified') 50 | 51 | with catch_signal(config_status_changed) as handler: 52 | c.templates.add(t) 53 | handler.assert_not_called() 54 | 55 | c.status = 'applied' 56 | c.save() 57 | c.refresh_from_db() 58 | self.assertEqual(c.status, 'applied') 59 | t.config['interfaces'][0]['name'] = 'eth1' 60 | t.full_clean() 61 | 62 | with catch_signal(config_status_changed) as handler: 63 | t.save() 64 | c.refresh_from_db() 65 | handler.assert_called_once_with( 66 | sender=Config, signal=config_status_changed, instance=c, 67 | ) 68 | self.assertEqual(c.status, 'modified') 69 | 70 | # status has already changed to modified 71 | # sgnal should not be triggered again 72 | with catch_signal(config_status_changed) as handler: 73 | t.config['interfaces'][0]['name'] = 'eth2' 74 | t.full_clean() 75 | t.save() 76 | c.refresh_from_db() 77 | handler.assert_not_called() 78 | self.assertEqual(c.status, 'modified') 79 | 80 | def test_config_status_modified_after_template_added(self): 81 | t = self._create_template() 82 | c = self._create_config(device=self._create_device(name='test-status')) 83 | c.status = 'applied' 84 | c.save() 85 | c.refresh_from_db() 86 | with catch_signal(config_status_changed) as handler: 87 | c.templates.add(t) 88 | c.refresh_from_db() 89 | handler.assert_called_once_with( 90 | sender=Config, signal=config_status_changed, instance=c, 91 | ) 92 | 93 | def test_config_modified_signal_always_sent(self): 94 | t = self._create_template() 95 | c = self._create_config(device=self._create_device(name='test-status')) 96 | self.assertEqual(c.status, 'modified') 97 | 98 | with catch_signal(config_modified) as handler: 99 | c.templates.add(t) 100 | handler.assert_called_once_with( 101 | sender=Config, 102 | signal=config_modified, 103 | instance=c, 104 | device=c.device, 105 | config=c, 106 | ) 107 | 108 | c.status = 'applied' 109 | c.save() 110 | c.refresh_from_db() 111 | self.assertEqual(c.status, 'applied') 112 | t.config['interfaces'][0]['name'] = 'eth1' 113 | t.full_clean() 114 | 115 | with catch_signal(config_modified) as handler: 116 | t.save() 117 | c.refresh_from_db() 118 | handler.assert_called_once() 119 | self.assertEqual(c.status, 'modified') 120 | 121 | # status has already changed to modified 122 | # sgnal should be triggered anyway 123 | with catch_signal(config_modified) as handler: 124 | t.config['interfaces'][0]['name'] = 'eth2' 125 | t.full_clean() 126 | t.save() 127 | c.refresh_from_db() 128 | handler.assert_called_once() 129 | self.assertEqual(c.status, 'modified') 130 | 131 | def test_no_auto_hostname(self): 132 | t = self._create_template() 133 | self.assertNotIn('general', t.backend_instance.config) 134 | t.refresh_from_db() 135 | self.assertNotIn('general', t.config) 136 | 137 | def test_default_template(self): 138 | # no default templates defined yet 139 | c = self._create_config() 140 | self.assertEqual(c.templates.count(), 0) 141 | c.device.delete() 142 | # create default templates for different backends 143 | t1 = self._create_template( 144 | name='default-openwrt', backend='netjsonconfig.OpenWrt', default=True 145 | ) 146 | t2 = self._create_template( 147 | name='default-openwisp', backend='netjsonconfig.OpenWisp', default=True 148 | ) 149 | c1 = self._create_config( 150 | device=self._create_device(name='test-openwrt'), 151 | backend='netjsonconfig.OpenWrt', 152 | ) 153 | d2 = self._create_device( 154 | name='test-openwisp', mac_address=self.TEST_MAC_ADDRESS.replace('55', '56') 155 | ) 156 | c2 = self._create_config(device=d2, backend='netjsonconfig.OpenWisp') 157 | # ensure OpenWRT device has only the default OpenWRT backend 158 | self.assertEqual(c1.templates.count(), 1) 159 | self.assertEqual(c1.templates.first().id, t1.id) 160 | # ensure OpenWISP device has only the default OpenWISP backend 161 | self.assertEqual(c2.templates.count(), 1) 162 | self.assertEqual(c2.templates.first().id, t2.id) 163 | 164 | def test_vpn_missing(self): 165 | try: 166 | self._create_template(type='vpn') 167 | except ValidationError as err: 168 | self.assertTrue('vpn' in err.message_dict) 169 | else: 170 | self.fail('ValidationError not raised') 171 | 172 | def test_generic_has_no_vpn(self): 173 | t = self._create_template(vpn=self._create_vpn()) 174 | self.assertIsNone(t.vpn) 175 | self.assertFalse(t.auto_cert) 176 | 177 | def test_generic_has_create_cert_false(self): 178 | t = self._create_template() 179 | self.assertFalse(t.auto_cert) 180 | 181 | def test_auto_client_template(self): 182 | vpn = self._create_vpn() 183 | t = self._create_template( 184 | name='autoclient', type='vpn', auto_cert=True, vpn=vpn, config={} 185 | ) 186 | control = t.vpn.auto_client() 187 | self.assertDictEqual(t.config, control) 188 | 189 | def test_auto_client_template_auto_cert_False(self): 190 | vpn = self._create_vpn() 191 | t = self._create_template( 192 | name='autoclient', type='vpn', auto_cert=False, vpn=vpn, config={} 193 | ) 194 | vpn = t.config['openvpn'][0] 195 | self.assertEqual(vpn['cert'], 'cert.pem') 196 | self.assertEqual(vpn['key'], 'key.pem') 197 | self.assertEqual(len(t.config['files']), 1) 198 | self.assertIn('ca_path', t.config['files'][0]['path']) 199 | 200 | def test_template_context_var(self): 201 | t = self._create_template( 202 | config={ 203 | 'files': [ 204 | { 205 | 'path': '/etc/vpnserver1', 206 | 'mode': '0644', 207 | 'contents': '{{ name }}\n{{ vpnserver1 }}\n', 208 | } 209 | ] 210 | } 211 | ) 212 | c = self._create_config() 213 | c.templates.add(t) 214 | # clear cache 215 | del c.backend_instance 216 | output = c.backend_instance.render() 217 | vpnserver1 = settings.NETJSONCONFIG_CONTEXT['vpnserver1'] 218 | self.assertIn(vpnserver1, output) 219 | 220 | def test_get_context(self): 221 | t = self._create_template() 222 | expected = {} 223 | expected.update(settings.NETJSONCONFIG_CONTEXT) 224 | self.assertEqual(t.get_context(), expected) 225 | 226 | def test_tamplates_clone(self): 227 | t = self._create_template(default=True) 228 | t.save() 229 | user = User.objects.create_superuser( 230 | username='admin', password='tester', email='admin@admin.com' 231 | ) 232 | c = t.clone(user) 233 | c.full_clean() 234 | c.save() 235 | self.assertEqual(c.name, '{} (Clone)'.format(t.name)) 236 | self.assertIsNotNone(c.pk) 237 | self.assertNotEqual(c.pk, t.pk) 238 | self.assertFalse(c.default) 239 | 240 | def test_duplicate_files_in_template(self): 241 | try: 242 | self._create_template( 243 | name='test-vpn-1', 244 | config={ 245 | 'files': [ 246 | { 247 | 'path': '/etc/vpnserver1', 248 | 'mode': '0644', 249 | 'contents': '{{ name }}\n{{ vpnserver1 }}\n', 250 | }, 251 | { 252 | 'path': '/etc/vpnserver1', 253 | 'mode': '0644', 254 | 'contents': '{{ name }}\n{{ vpnserver1 }}\n', 255 | }, 256 | ] 257 | }, 258 | ) 259 | except ValidationError as e: 260 | self.assertIn('Invalid configuration triggered by "#/files"', str(e)) 261 | else: 262 | self.fail('ValidationError not raised!') 263 | 264 | def test_variable_substition(self): 265 | config = {"dns_servers": ["{{dns}}"]} 266 | default_values = {"dns": "4.4.4.4"} 267 | options = { 268 | "name": "test1", 269 | "backend": "netjsonconfig.OpenWrt", 270 | "config": config, 271 | "default_values": default_values, 272 | } 273 | temp = self.template_model(**options) 274 | temp.full_clean() 275 | temp.save() 276 | obj = self.template_model.objects.get(name='test1') 277 | self.assertEqual(obj.name, 'test1') 278 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | User = get_user_model() 6 | 7 | 8 | class TestViews(TestCase): 9 | """ 10 | tests for django_netjsonconfig.views 11 | """ 12 | 13 | def setUp(self): 14 | User.objects.create_superuser( 15 | username='admin', password='tester', email='admin@admin.com' 16 | ) 17 | 18 | def test_schema_403(self): 19 | response = self.client.get(reverse('admin:schema')) 20 | self.assertEqual(response.status_code, 403) 21 | self.assertIn('error', response.json()) 22 | 23 | def test_schema_200(self): 24 | self.client.force_login(User.objects.get(pk=1)) 25 | response = self.client.get(reverse('admin:schema')) 26 | self.assertEqual(response.status_code, 200) 27 | self.assertIn('netjsonconfig.OpenWrt', response.json()) 28 | 29 | def test_schema_hostname_hidden(self): 30 | from ..views import available_schemas 31 | 32 | for key, schema in available_schemas.items(): 33 | if 'general' not in schema['properties']: 34 | continue 35 | if 'hostname' in schema['properties']['general']['properties']: 36 | self.fail('hostname property must be hidden') 37 | -------------------------------------------------------------------------------- /django_netjsonconfig/tests/test_vpn.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ValidationError 3 | from django.test import TestCase 4 | from django_x509.models import Ca, Cert 5 | 6 | from ..models import Config, Device, Template, Vpn, VpnClient 7 | from ..vpn_backends import OpenVpn 8 | from . import CreateConfigMixin, CreateTemplateMixin, TestVpnX509Mixin 9 | 10 | 11 | class TestVpn(TestVpnX509Mixin, CreateConfigMixin, CreateTemplateMixin, TestCase): 12 | """ 13 | tests for Vpn model 14 | """ 15 | 16 | maxDiff = None 17 | ca_model = Ca 18 | config_model = Config 19 | device_model = Device 20 | template_model = Template 21 | vpn_model = Vpn 22 | 23 | def test_config_not_none(self): 24 | v = Vpn( 25 | name='test', 26 | host='vpn1.test.com', 27 | ca=self._create_ca(), 28 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 29 | config=None, 30 | dh=self._dh, 31 | ) 32 | try: 33 | v.full_clean() 34 | except ValidationError: 35 | pass 36 | self.assertEqual(v.config, {}) 37 | 38 | def test_backend_class(self): 39 | v = Vpn( 40 | name='test', 41 | host='vpn1.test.com', 42 | ca=self._create_ca(), 43 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 44 | ) 45 | self.assertIs(v.backend_class, OpenVpn) 46 | 47 | def test_backend_instance(self): 48 | v = Vpn( 49 | name='test', 50 | host='vpn1.test.com', 51 | ca=self._create_ca(), 52 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 53 | config={}, 54 | ) 55 | self.assertIsInstance(v.backend_instance, OpenVpn) 56 | 57 | def test_validation(self): 58 | config = {'openvpn': {'invalid': True}} 59 | v = Vpn( 60 | name='test', 61 | host='vpn1.test.com', 62 | ca=self._create_ca(), 63 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 64 | config=config, 65 | ) 66 | # ensure django ValidationError is raised 67 | with self.assertRaises(ValidationError): 68 | v.full_clean() 69 | 70 | def test_json(self): 71 | v = self._create_vpn() 72 | self.assertDictEqual(v.json(dict=True), self._vpn_config) 73 | 74 | def test_automatic_cert_creation(self): 75 | vpn = self._create_vpn() 76 | self.assertIsNotNone(vpn.cert) 77 | server_extensions = [ 78 | {"name": "nsCertType", "value": "server", "critical": False} 79 | ] 80 | self.assertEqual(vpn.cert.extensions, server_extensions) 81 | 82 | def test_vpn_client_unique_together(self): 83 | vpn = self._create_vpn() 84 | t = self._create_template(name='vpn-test', type='vpn', vpn=vpn) 85 | c = self._create_config() 86 | c.templates.add(t) 87 | # one VpnClient instance has been automatically created 88 | # now try to create a duplicate 89 | client = VpnClient(vpn=vpn, config=c, auto_cert=True) 90 | try: 91 | client.full_clean() 92 | except ValidationError as e: 93 | self.assertIn( 94 | 'with this Config and Vpn already exists', e.message_dict['__all__'][0] 95 | ) 96 | else: 97 | self.fail('unique_together clause not triggered') 98 | 99 | def test_vpn_client_auto_cert_deletes_cert(self): 100 | vpn = self._create_vpn() 101 | t = self._create_template(name='vpn-test', type='vpn', vpn=vpn, auto_cert=True) 102 | c = self._create_config() 103 | c.templates.add(t) 104 | vpnclient = c.vpnclient_set.first() 105 | cert_pk = vpnclient.cert.pk 106 | self.assertEqual(Cert.objects.filter(pk=cert_pk).count(), 1) 107 | c.delete() 108 | self.assertEqual(VpnClient.objects.filter(pk=vpnclient.pk).count(), 0) 109 | self.assertEqual(Cert.objects.filter(pk=cert_pk).count(), 0) 110 | 111 | def test_vpn_cert_and_ca_mismatch(self): 112 | ca = self._create_ca() 113 | different_ca = self._create_ca() 114 | cert = Cert( 115 | name='test-cert-vpn', 116 | ca=ca, 117 | key_length='2048', 118 | digest='sha256', 119 | country_code='IT', 120 | state='RM', 121 | city='Rome', 122 | organization_name='OpenWISP', 123 | email='test@test.com', 124 | common_name='openwisp.org', 125 | ) 126 | cert.full_clean() 127 | cert.save() 128 | vpn = Vpn( 129 | name='test', 130 | host='vpn1.test.com', 131 | ca=different_ca, 132 | cert=cert, 133 | backend='django_netjsonconfig.vpn_backends.OpenVpn', 134 | ) 135 | try: 136 | vpn.full_clean() 137 | except ValidationError as e: 138 | self.assertIn('cert', e.message_dict) 139 | else: 140 | self.fail('Mismatch between ca and cert but ' 'ValidationError not raised') 141 | 142 | def test_auto_client(self): 143 | vpn = self._create_vpn() 144 | auto = vpn.auto_client() 145 | context_keys = vpn._get_auto_context_keys() 146 | for key in context_keys.keys(): 147 | context_keys[key] = '{{%s}}' % context_keys[key] 148 | control = vpn.backend_class.auto_client( 149 | host=vpn.host, server=self._vpn_config['openvpn'][0], **context_keys 150 | ) 151 | control['files'] = [ 152 | { 153 | 'path': context_keys['ca_path'], 154 | 'mode': '0600', 155 | 'contents': context_keys['ca_contents'], 156 | }, 157 | { 158 | 'path': context_keys['cert_path'], 159 | 'mode': '0600', 160 | 'contents': context_keys['cert_contents'], 161 | }, 162 | { 163 | 'path': context_keys['key_path'], 164 | 'mode': '0600', 165 | 'contents': context_keys['key_contents'], 166 | }, 167 | ] 168 | self.assertDictEqual(auto, control) 169 | 170 | def test_auto_client_auto_cert_False(self): 171 | vpn = self._create_vpn() 172 | auto = vpn.auto_client(auto_cert=False) 173 | context_keys = vpn._get_auto_context_keys() 174 | for key in context_keys.keys(): 175 | context_keys[key] = '{{%s}}' % context_keys[key] 176 | for key in ['cert_path', 'cert_contents', 'key_path', 'key_contents']: 177 | del context_keys[key] 178 | control = vpn.backend_class.auto_client( 179 | host=vpn.host, server=self._vpn_config['openvpn'][0], **context_keys 180 | ) 181 | control['files'] = [ 182 | { 183 | 'path': context_keys['ca_path'], 184 | 'mode': '0600', 185 | 'contents': context_keys['ca_contents'], 186 | } 187 | ] 188 | self.assertDictEqual(auto, control) 189 | 190 | def test_vpn_client_get_common_name(self): 191 | vpn = self._create_vpn() 192 | d = self._create_device() 193 | c = self._create_config(device=d) 194 | client = VpnClient(vpn=vpn, config=c, auto_cert=True) 195 | self.assertEqual( 196 | client._get_common_name(), '{mac_address}-{name}'.format(**d.__dict__) 197 | ) 198 | d.name = d.mac_address 199 | self.assertEqual(client._get_common_name(), d.mac_address) 200 | 201 | def test_get_auto_context_keys(self): 202 | vpn = self._create_vpn() 203 | keys = vpn._get_auto_context_keys() 204 | pk = vpn.pk.hex 205 | control = { 206 | 'ca_path': 'ca_path_{0}'.format(pk), 207 | 'ca_contents': 'ca_contents_{0}'.format(pk), 208 | 'cert_path': 'cert_path_{0}'.format(pk), 209 | 'cert_contents': 'cert_contents_{0}'.format(pk), 210 | 'key_path': 'key_path_{0}'.format(pk), 211 | 'key_contents': 'key_contents_{0}'.format(pk), 212 | } 213 | self.assertEqual(keys, control) 214 | 215 | def test_get_context(self): 216 | v = self._create_vpn() 217 | expected = { 218 | 'ca': v.ca.certificate, 219 | 'cert': v.cert.certificate, 220 | 'key': v.cert.private_key, 221 | 'dh': v.dh, 222 | } 223 | expected.update(settings.NETJSONCONFIG_CONTEXT) 224 | self.assertEqual(v.get_context(), expected) 225 | 226 | def test_dh(self): 227 | v = self._create_vpn() 228 | v.dh = None 229 | v.save() 230 | self.assertIsNotNone(v.dh) 231 | self.assertNotEqual(v.dh, '') 232 | self.assertTrue(v.dh.startswith('-----BEGIN DH PARAMETERS-----')) 233 | self.assertTrue(v.dh.endswith('-----END DH PARAMETERS-----\n')) 234 | 235 | def test_get_context_empty_vpn(self): 236 | v = Vpn() 237 | self.assertEqual(v.get_context(), settings.NETJSONCONFIG_CONTEXT) 238 | 239 | def test_key_validator(self): 240 | v = self._create_vpn() 241 | v.key = 'key/key' 242 | with self.assertRaises(ValidationError): 243 | v.full_clean() 244 | v.key = 'key.key' 245 | with self.assertRaises(ValidationError): 246 | v.full_clean() 247 | v.key = 'key key' 248 | with self.assertRaises(ValidationError): 249 | v.full_clean() 250 | v.key = self.TEST_KEY 251 | v.full_clean() 252 | -------------------------------------------------------------------------------- /django_netjsonconfig/urls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | There are no urls in this file. It has no functional use. 3 | This file exists only to maintain backward compatibility. 4 | ''' 5 | 6 | app_name = 'django_netjsonconfig' # pragma: no cover 7 | urlpatterns = [] # pragma: no cover 8 | -------------------------------------------------------------------------------- /django_netjsonconfig/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf.urls import url 4 | from django.core.exceptions import ValidationError 5 | from django.http import Http404, HttpResponse 6 | from django.shortcuts import get_object_or_404 as base_get_object_or_404 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def get_object_or_404(model, **kwargs): 12 | """ 13 | like ``django.shortcuts.get_object_or_404`` 14 | but handles eventual exceptions caused by 15 | malformed UUIDs (raising an ``Http404`` exception) 16 | """ 17 | try: 18 | return base_get_object_or_404(model, **kwargs) 19 | except ValidationError: 20 | raise Http404() 21 | 22 | 23 | class ControllerResponse(HttpResponse): 24 | """ 25 | extends ``django.http.HttpResponse`` by adding a custom HTTP header 26 | """ 27 | 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self['X-Openwisp-Controller'] = 'true' 31 | 32 | 33 | def send_file(filename, contents): 34 | """ 35 | returns a ``ControllerResponse`` object with an attachment 36 | """ 37 | response = ControllerResponse(contents, content_type='application/octet-stream') 38 | response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) 39 | return response 40 | 41 | 42 | def send_device_config(config, request): 43 | """ 44 | calls ``update_last_ip`` and returns a ``ControllerResponse`` 45 | which includes the configuration tar.gz as attachment 46 | """ 47 | update_last_ip(config.device, request) 48 | return send_file( 49 | filename='{0}.tar.gz'.format(config.name), contents=config.generate().getvalue() 50 | ) 51 | 52 | 53 | def send_vpn_config(vpn, request): 54 | """ 55 | returns a ``ControllerResponse``which includes the configuration 56 | tar.gz as attachment 57 | """ 58 | return send_file( 59 | filename='{0}.tar.gz'.format(vpn.name), contents=vpn.generate().getvalue() 60 | ) 61 | 62 | 63 | def update_last_ip(device, request): 64 | """ 65 | updates ``last_ip`` if necessary 66 | """ 67 | ip = request.META.get('REMOTE_ADDR') 68 | management_ip = request.GET.get('management_ip') 69 | changed = False 70 | if device.last_ip != ip: 71 | device.last_ip = ip 72 | changed = True 73 | if device.management_ip != management_ip: 74 | device.management_ip = management_ip 75 | changed = True 76 | if changed: 77 | device.save() 78 | return changed 79 | 80 | 81 | def forbid_unallowed(request, param_group, param, allowed_values=None): 82 | """ 83 | checks for malformed requests - eg: missing parameters (HTTP 400) 84 | or unauthorized requests - eg: wrong key (HTTP 403) 85 | if the request is legitimate, returns ``None`` 86 | otherwise calls ``invalid_response`` 87 | """ 88 | error = None 89 | value = getattr(request, param_group).get(param) 90 | if not value: 91 | error = 'error: missing required parameter "{}"\n'.format(param) 92 | return invalid_response(request, error, status=400) 93 | if allowed_values and not isinstance(allowed_values, list): 94 | allowed_values = [allowed_values] 95 | if allowed_values is not None and value not in allowed_values: 96 | error = 'error: wrong {}\n'.format(param) 97 | return invalid_response(request, error, status=403) 98 | 99 | 100 | def invalid_response(request, error, status, content_type='text/plain'): 101 | """ 102 | logs an invalid request and returns a ``ControllerResponse`` 103 | with the specified HTTP status code, which defaults to 403 104 | """ 105 | logger.warning(error, extra={'request': request, 'stack': True}) 106 | return ControllerResponse(error, content_type=content_type, status=status) 107 | 108 | 109 | def get_controller_urls(views_module): 110 | """ 111 | used by third party apps to reduce boilerplate 112 | """ 113 | urls = [ 114 | url( 115 | r'^controller/device/checksum/(?P[^/]+)/$', 116 | views_module.device_checksum, 117 | name='device_checksum', 118 | ), 119 | url( 120 | r'^controller/device/download-config/(?P[^/]+)/$', 121 | views_module.device_download_config, 122 | name='device_download_config', 123 | ), 124 | url( 125 | r'^controller/device/update-info/(?P[^/]+)/$', 126 | views_module.device_update_info, 127 | name='device_update_info', 128 | ), 129 | url( 130 | r'^controller/device/report-status/(?P[^/]+)/$', 131 | views_module.device_report_status, 132 | name='device_report_status', 133 | ), 134 | url( 135 | r'^controller/device/register/$', 136 | views_module.device_register, 137 | name='device_register', 138 | ), 139 | url( 140 | r'^controller/vpn/checksum/(?P[^/]+)/$', 141 | views_module.vpn_checksum, 142 | name='vpn_checksum', 143 | ), 144 | url( 145 | r'^controller/vpn/download-config/(?P[^/]+)/$', 146 | views_module.vpn_download_config, 147 | name='vpn_download_config', 148 | ), 149 | # legacy URLs 150 | url( 151 | r'^controller/checksum/(?P[^/]+)/$', 152 | views_module.device_checksum, 153 | name='checksum_legacy', 154 | ), 155 | url( 156 | r'^controller/download-config/(?P[^/]+)/$', 157 | views_module.device_download_config, 158 | name='download_config_legacy', 159 | ), 160 | url( 161 | r'^controller/update-info/(?P[^/]+)/$', 162 | views_module.device_update_info, 163 | name='update_info_legacy', 164 | ), 165 | url( 166 | r'^controller/report-status/(?P[^/]+)/$', 167 | views_module.device_report_status, 168 | name='report_status_legacy', 169 | ), 170 | url( 171 | r'^controller/register/$', 172 | views_module.device_register, 173 | name='register_legacy', 174 | ), 175 | ] 176 | return urls 177 | -------------------------------------------------------------------------------- /django_netjsonconfig/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import RegexValidator, _lazy_re_compile 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | key_validator = RegexValidator( 5 | _lazy_re_compile('^[^\s/\.]+$'), 6 | message=_('Key must not contain spaces, dots or slashes.'), 7 | code='invalid', 8 | ) 9 | 10 | mac_address_regex = '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' 11 | mac_address_validator = RegexValidator( 12 | _lazy_re_compile(mac_address_regex), 13 | message=_('Must be a valid mac address.'), 14 | code='invalid', 15 | ) 16 | 17 | # device name must either be a hostname or a valid mac address 18 | hostname_regex = ( 19 | '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}' 20 | '[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9]' 21 | '[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$' 22 | ) 23 | device_name_validator = RegexValidator( 24 | _lazy_re_compile('{0}|{1}'.format(hostname_regex, mac_address_regex)), 25 | message=_('Must be either a valid hostname or mac address.'), 26 | code='invalid', 27 | ) 28 | -------------------------------------------------------------------------------- /django_netjsonconfig/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | 4 | from django.http import HttpResponse 5 | from django.utils import timezone 6 | from django.utils.module_loading import import_string 7 | from django.utils.translation import ugettext as _ 8 | from django.views.decorators.http import last_modified 9 | 10 | from .settings import BACKENDS, VPN_BACKENDS 11 | 12 | ALL_BACKENDS = BACKENDS + VPN_BACKENDS 13 | 14 | # ``available_schemas`` and ``available_schemas_json`` 15 | # will be generated only once at startup 16 | available_schemas = {} 17 | for backend_path, label in ALL_BACKENDS: # noqa 18 | backend = import_string(backend_path) 19 | schema = deepcopy(backend.schema) 20 | # must use conditional because some custom backends might not specify an hostname 21 | if 'general' in schema['properties']: 22 | # hide hostname because it's handled via models 23 | if 'hostname' in schema['properties']['general']['properties']: 24 | del schema['properties']['general']['properties']['hostname'] 25 | # remove hosname from required properties 26 | if 'hostname' in schema['properties']['general'].get('required', []): 27 | del schema['properties']['general']['required'] 28 | # start editor empty by default, except for VPN schemas 29 | if (backend_path, label) not in VPN_BACKENDS: 30 | schema['defaultProperties'] = [] 31 | available_schemas[backend_path] = schema 32 | available_schemas_json = json.dumps(available_schemas) 33 | 34 | login_required_error = json.dumps({'error': _('login required')}) 35 | 36 | # ``start_time`` will contain the datetime of the moment in which the 37 | # application server is started and it is used in the last-modified 38 | # header of the HTTP response of ``schema`` view 39 | start_time = timezone.now() 40 | 41 | 42 | @last_modified(lambda request: start_time) 43 | def schema(request): 44 | """ 45 | returns configuration checksum 46 | """ 47 | authenticated = request.user.is_authenticated 48 | if authenticated: 49 | c = available_schemas_json 50 | status = 200 51 | else: 52 | c = login_required_error 53 | status = 403 54 | return HttpResponse(c, status=status, content_type='application/json') 55 | -------------------------------------------------------------------------------- /django_netjsonconfig/vpn_backends.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from netjsonconfig import OpenVpn as BaseOpenVpn 4 | 5 | # adapt OpenVPN schema in order to limit it to 1 item only 6 | limited_schema = deepcopy(BaseOpenVpn.schema) 7 | limited_schema['properties']['openvpn'].update( 8 | {"additionalItems": False, "minItems": 1, "maxItems": 1} 9 | ) 10 | # server mode only 11 | limited_schema['properties']['openvpn']['items'].update( 12 | { 13 | "oneOf": [ 14 | {"$ref": "#/definitions/server_bridged"}, 15 | {"$ref": "#/definitions/server_routed"}, 16 | {"$ref": "#/definitions/server_manual"}, 17 | ] 18 | } 19 | ) 20 | 21 | # default values for ca, cert and key 22 | limited_schema['definitions']['tunnel']['properties']['ca']['default'] = 'ca.pem' 23 | limited_schema['definitions']['tunnel']['properties']['cert']['default'] = 'cert.pem' 24 | limited_schema['definitions']['tunnel']['properties']['key']['default'] = 'key.pem' 25 | limited_schema['definitions']['server']['properties']['dh']['default'] = 'dh.pem' 26 | limited_schema['properties']['files']['default'] = [ 27 | {"path": "ca.pem", "mode": "0600", "contents": "{{ ca }}"}, 28 | {"path": "cert.pem", "mode": "0600", "contents": "{{ cert }}"}, 29 | {"path": "key.pem", "mode": "0600", "contents": "{{ key }}"}, 30 | {"path": "dh.pem", "mode": "0600", "contents": "{{ dh }}"}, 31 | ] 32 | 33 | 34 | class OpenVpn(BaseOpenVpn): 35 | """ 36 | modified OpenVpn backend 37 | its schema is adapted to be used as a VPN Server backend: 38 | * shows server only 39 | * allows only 1 vpn 40 | * adds default values for ca, cert, key and dh 41 | """ 42 | 43 | schema = limited_schema 44 | -------------------------------------------------------------------------------- /django_netjsonconfig/widgets.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.widgets import AdminTextareaWidget 3 | from django.templatetags.static import static 4 | from django.urls import reverse 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class JsonSchemaWidget(AdminTextareaWidget): 9 | """ 10 | JSON Schema Editor widget 11 | """ 12 | 13 | @property 14 | def media(self): 15 | prefix = 'django-netjsonconfig' 16 | js = [ 17 | static('{0}/js/{1}'.format(prefix, f)) 18 | for f in ( 19 | 'lib/utils.js', 20 | 'lib/advanced-mode.js', 21 | 'lib/tomorrow_night_bright.js', 22 | 'lib/jsonschema-ui.js', 23 | 'widget.js', 24 | ) 25 | ] 26 | css = { 27 | 'all': [ 28 | static('{0}/css/{1}'.format(prefix, f)) 29 | for f in ('lib/jsonschema-ui.css', 'lib/advanced-mode.css') 30 | ] 31 | } 32 | return forms.Media(js=js, css=css) 33 | 34 | def render(self, name, value, attrs={}, renderer=None): 35 | attrs['class'] = 'vLargeTextField jsoneditor-raw' 36 | html = """ 37 | 38 | 39 | 44 | """ 45 | html = html.format(_('Advanced mode (raw JSON)'), reverse('admin:schema')) 46 | html += super().render(name, value, attrs, renderer) 47 | return html 48 | -------------------------------------------------------------------------------- /docs/images/adhoc-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/adhoc-interface.png -------------------------------------------------------------------------------- /docs/images/bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/bridge.png -------------------------------------------------------------------------------- /docs/images/configuration-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/configuration-ui.png -------------------------------------------------------------------------------- /docs/images/device-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/device-advanced.png -------------------------------------------------------------------------------- /docs/images/device-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/device-context.png -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/preview.png -------------------------------------------------------------------------------- /docs/images/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/radio.png -------------------------------------------------------------------------------- /docs/images/template-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/template-advanced.png -------------------------------------------------------------------------------- /docs/images/template-default-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/template-default-values.png -------------------------------------------------------------------------------- /docs/images/wpa-enterprise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/docs/images/wpa-enterprise.png -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | coveralls 2 | openwisp-utils[qa]>=0.5.0 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.2,<3.1 2 | django-model-utils>=4.0 3 | netjsonconfig>=0.8.0,<0.9.0 4 | django-sortedm2m>=3.0.0,<3.1.0 5 | django-reversion>=3.0.5,<3.1.0 6 | django-x509>=0.6.2,<0.7.0 7 | django-taggit>=0.24.0,<1.3.0 8 | openwisp-utils>=0.5.0,<0.6.0 9 | -------------------------------------------------------------------------------- /run-qa-checks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | openwisp-qa-check \ 4 | --migration-path ./django_netjsonconfig/migrations \ 5 | --migration-module django_netjsonconfig 6 | 7 | jslint django_netjsonconfig/static/django-netjsonconfig/js/*.js 8 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, "tests") 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 9 | 10 | if __name__ == "__main__": 11 | from django.core.management import execute_from_command_line 12 | 13 | args = sys.argv 14 | args.insert(1, "test") 15 | args.insert(2, "django_netjsonconfig") 16 | execute_from_command_line(args) 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | exclude = *migrations*, 6 | ./tests/*settings*.py 7 | max-line-length = 110 8 | # W503: line break before or after operator 9 | # W504: line break after or after operator 10 | # W605: invalid escape sequence 11 | ignore = W605, W503, W504 12 | 13 | [isort] 14 | known_third_party = django 15 | known_first_party = netjsonconfig,openwisp_utils 16 | line_length=88 17 | default_section = THIRDPARTY 18 | skip = migrations 19 | multi_line_output=3 20 | use_parentheses=True 21 | include_trailing_comma=True 22 | force_grid_wrap=0 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from django_netjsonconfig import get_version 6 | from setuptools import find_packages, setup 7 | 8 | 9 | def get_install_requires(): 10 | """ 11 | parse requirements.txt, ignore links, exclude comments 12 | """ 13 | requirements = [] 14 | for line in open('requirements.txt').readlines(): 15 | # skip to next iteration if comment or empty line 16 | if ( 17 | line.startswith('#') 18 | or line == '' 19 | or line.startswith('http') 20 | or line.startswith('git') 21 | ): 22 | continue 23 | # add line to requirements 24 | requirements.append(line) 25 | return requirements 26 | 27 | 28 | if sys.argv[-1] == 'publish': 29 | # delete any *.pyc, *.pyo and __pycache__ 30 | os.system('find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf') 31 | os.system("python setup.py sdist bdist_wheel") 32 | os.system("twine upload -s dist/*") 33 | os.system("rm -rf dist build") 34 | args = {'version': get_version()} 35 | print("You probably want to also tag the version now:") 36 | print(" git tag -a %(version)s -m 'version %(version)s'" % args) 37 | print(" git push --tags") 38 | sys.exit() 39 | 40 | 41 | setup( 42 | name='django-netjsonconfig', 43 | version=get_version(), 44 | license='GPL3', 45 | author='Federico Capoano', 46 | author_email='federico.capoano@gmail.com', 47 | description='Configuration manager for embedded devices, implemented as a reusable django-app', 48 | long_description=open('README.rst').read(), 49 | url='http://netjson.org', 50 | download_url='https://github.com/openwisp/django-netjsonconfig/releases', 51 | platforms=['Platform Independent'], 52 | keywords=['django', 'netjson', 'openwrt', 'networking', 'openwisp'], 53 | packages=find_packages(exclude=['tests*', 'docs*']), 54 | include_package_data=True, 55 | zip_safe=False, 56 | install_requires=get_install_requires(), 57 | classifiers=[ 58 | 'Development Status :: 3 - Alpha', 59 | 'Environment :: Web Environment', 60 | 'Topic :: Internet :: WWW/HTTP', 61 | 'Intended Audience :: Developers', 62 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 63 | 'Operating System :: OS Independent', 64 | 'Framework :: Django', 65 | 'Topic :: System :: Networking', 66 | 'Programming Language :: Python :: 3', 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-netjsonconfig/55ab56245cf263f331c08e421c37c54ad44024cc/tests/__init__.py -------------------------------------------------------------------------------- /tests/local_settings.example.py: -------------------------------------------------------------------------------- 1 | # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS 2 | # BUT DO NOT COMMIT 3 | 4 | # DATABASES = { 5 | # 'default': { 6 | # 'ENGINE': 'django.db.backends.sqlite3', 7 | # 'NAME': 'netjsonconfig.db', 8 | # 'USER': '', 9 | # 'PASSWORD': '', 10 | # 'HOST': '', 11 | # 'PORT': '' 12 | # }, 13 | # } 14 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'settings') 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 4 | 5 | DEBUG = True 6 | 7 | ALLOWED_HOSTS = [] 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | 'NAME': os.path.join(BASE_DIR, 'netjsonconfig.db'), 13 | } 14 | } 15 | 16 | SECRET_KEY = 'fn)t*+$)ugeyip6-#txyy$5wf2ervc0d2n#h)qb)y5@ly$t*@w' 17 | 18 | INSTALLED_APPS = [ 19 | 'django.contrib.auth', 20 | 'django.contrib.contenttypes', 21 | 'django.contrib.sessions', 22 | 'django.contrib.messages', 23 | 'django.contrib.staticfiles', 24 | 'openwisp_utils.admin_theme', 25 | 'django_netjsonconfig', 26 | 'django.contrib.admin', 27 | 'sortedm2m', 28 | 'reversion', 29 | 'django_x509', 30 | ] 31 | 32 | STATICFILES_FINDERS = [ 33 | 'django.contrib.staticfiles.finders.FileSystemFinder', 34 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 35 | 'openwisp_utils.staticfiles.DependencyFinder', 36 | ] 37 | 38 | MIDDLEWARE = [ 39 | 'django.middleware.security.SecurityMiddleware', 40 | 'django.contrib.sessions.middleware.SessionMiddleware', 41 | 'django.middleware.common.CommonMiddleware', 42 | 'django.middleware.csrf.CsrfViewMiddleware', 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware', 45 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 46 | ] 47 | 48 | ROOT_URLCONF = 'urls' 49 | 50 | TIME_ZONE = 'Europe/Rome' 51 | LANGUAGE_CODE = 'en-gb' 52 | USE_TZ = True 53 | USE_I18N = False 54 | USE_L10N = False 55 | STATIC_URL = '/static/' 56 | CORS_ORIGIN_ALLOW_ALL = True 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | NETJSONCONFIG_SHARED_SECRET = 't3st1ng' 75 | NETJSONCONFIG_CONTEXT = {'vpnserver1': 'vpn.testdomain.com'} 76 | 77 | NETJSONCONFIG_HARDWARE_ID_ENABLED = True 78 | 79 | # local settings must be imported before test runner otherwise they'll be ignored 80 | try: 81 | from local_settings import * 82 | except ImportError: 83 | pass 84 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | 6 | urlpatterns = [ 7 | url(r'^admin/', admin.site.urls), 8 | # controller URLs 9 | # used by devices to download/update their configuration 10 | # keep the namespace argument unchanged 11 | url(r'^', include('django_netjsonconfig.controller.urls', namespace='controller')), 12 | url(r'^', include('django_x509.urls', namespace='x509')), 13 | ] 14 | 15 | urlpatterns += staticfiles_urlpatterns() 16 | 17 | if 'debug_toolbar' in settings.INSTALLED_APPS and settings.DEBUG: 18 | import debug_toolbar 19 | 20 | urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] 21 | --------------------------------------------------------------------------------