From 106627da04c17788fb058a41b62b502707e8c4eb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Jul 2017 10:39:09 -0400 Subject: [PATCH 01/92] Fixes #1358: Correct VRF example values in IP/prefix import forms --- netbox/templates/utilities/obj_import.html | 2 +- netbox/utilities/templatetags/helpers.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/netbox/templates/utilities/obj_import.html b/netbox/templates/utilities/obj_import.html index 7c331a2ef..90cb81a61 100644 --- a/netbox/templates/utilities/obj_import.html +++ b/netbox/templates/utilities/obj_import.html @@ -44,7 +44,7 @@ {{ field.help_text|default:field.label }} {% if field.choices %} -
Choices: {{ field.choices|example_choices }} +
Choices: {{ field|example_choices }} {% elif field|widget_type == 'dateinput' %}
Format: YYYY-MM-DD {% elif field|widget_type == 'checkboxinput' %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 42e8a277f..b7a40d018 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -63,19 +63,23 @@ def bettertitle(value): @register.filter() -def example_choices(value, arg=3): +def example_choices(field, arg=3): """ Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms). """ - choices = [] - for id, label in value: - if len(choices) == arg: - choices.append('etc.') + examples = [] + if hasattr(field, 'queryset'): + choices = [(obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg+1]] + else: + choices = field.choices + for id, label in choices: + if len(examples) == arg: + examples.append('etc.') break if not id: continue - choices.append(label) - return ', '.join(choices) or 'None' + examples.append(label) + return ', '.join(examples) or 'None' # From 5885b833cdf27e3ad4866ccdfdbfed0fee40ef76 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 19 Jul 2017 11:03:13 -0400 Subject: [PATCH 02/92] Fixes #1362: Raise validation error when attempting to create an API key that's too short --- netbox/users/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/users/views.py b/netbox/users/views.py index 88b6ebd32..5b12ad325 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -244,6 +244,7 @@ class TokenEditView(LoginRequiredMixin, View): token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) form = TokenForm(request.POST, instance=token) else: + token = Token() form = TokenForm(request.POST) if form.is_valid(): @@ -259,6 +260,13 @@ class TokenEditView(LoginRequiredMixin, View): else: return redirect('user:token_list') + return render(request, 'utilities/obj_edit.html', { + 'obj': token, + 'obj_type': token._meta.verbose_name, + 'form': form, + 'return_url': reverse('user:token_list'), + }) + class TokenDeleteView(LoginRequiredMixin, View): From 05aaafc1cf3865437b46d8a4b7f2d27f90478b99 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 24 Jul 2017 13:26:31 -0400 Subject: [PATCH 03/92] Added docs for using the NetBox shell --- docs/shell/intro.md | 194 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 + 2 files changed, 196 insertions(+) create mode 100644 docs/shell/intro.md diff --git a/docs/shell/intro.md b/docs/shell/intro.md new file mode 100644 index 000000000..df92cb7cd --- /dev/null +++ b/docs/shell/intro.md @@ -0,0 +1,194 @@ +NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command: + +``` +./manage.py nbshell +``` + +This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.) + +``` +$ ./manage.py nbshell +### NetBox interactive shell (jstretch-laptop) +### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev +### lsmodels() will show available models. Use help() for more info. +``` + +The function `lsmodels()` will print a list of all available NetBox models: + +``` +>>> lsmodels() +DCIM: + ConsolePort + ConsolePortTemplate + ConsoleServerPort + ConsoleServerPortTemplate + Device + ... +``` + +## Querying Objects + +Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `.objects.all()`, which will return a (truncated) list of all objects of that type. + +``` +>>> Device.objects.all() +, , , , , '...(remaining elements truncated)...']> +``` + +Use a `for` loop to cycle through all objects in the list: + +``` +>>> for device in Device.objects.all(): +... print(device.name, device.device_type) +... +(u'TestDevice1', ) +(u'TestDevice2', ) +(u'TestDevice3', ) +(u'TestDevice4', ) +(u'TestDevice5', ) +... +``` + +To count all objects matching the query, replace `all()` with `count()`: + +``` +>>> Device.objects.count() +1274 +``` + +To retrieve a particular object (typically by its primary key or other unique field), use `get()`: + +``` +>>> Site.objects.get(pk=7) + +``` + +### Filtering Querysets + +In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example: + +``` +>>> Device.objects.filter(status=STATUS_ACTIVE) +, , , , , '...(remaining elements truncated)...']> +``` + +Querysets support slicing to return a specific range of objects. + +``` +>>> Device.objects.filter(status=STATUS_ACTIVE)[:3] +, , ]> +``` + +The `count()` method can be appended to the queryset to return a count of objects rather than the full list. + +``` +>>> Device.objects.filter(status=STATUS_ACTIVE).count() +982 +``` + +Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper." + +``` +>>> Device.objects.filter(tenant__name='Pied Piper') +``` + +This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America: + +``` +>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america') +``` + +!!! note + While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation. + +Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0": + +``` +>>> Device.objects.filter(interfaces__name='em0') +``` + +Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive). + +``` +>>> Device.objects.filter(name__icontains='testdevice') +``` + +Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value. + +``` +>>> VLAN.objects.filter(vid__gt=2000) +``` + +Multiple filters can be combined to further refine a queryset. + +``` +>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering') +``` + +To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`. + +``` +>>> Device.objects.count() +4479 +>>> Device.objects.filter(status=STATUS_ACTIVE).count() +4133 +>>> Device.objects.exclude(status=STATUS_ACTIVE).count() +346 +``` + +!!! info + The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/). + +## Creating and Updating Objects + +New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance. + +``` +>>> lab1 = Site.objects.get(pk=7) +>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1) +>>> myvlan.save() +``` + +Alternatively, the above can be performed as a single operation: + +``` +>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save() +``` + +To modify an object, retrieve it, update the desired field(s), and call `save()` again. + +``` +>>> vlan = VLAN.objects.get(pk=1280) +>>> vlan.name +u'MyNewVLAN' +>>> vlan.name = 'BetterName' +>>> vlan.save() +>>> VLAN.objects.get(pk=1280).name +u'BetterName' +``` + +!!! warning + The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully. + +## Deleting Objects + +To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation. + +``` +>>> vlan + +>>> vlan.delete() +(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1}) +``` + +To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them. + +``` +>>> Device.objects.filter(name__icontains='test').count() +27 +>>> Device.objects.filter(name__icontains='test').delete() +(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0}) +``` + +!!! warning + Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset. diff --git a/mkdocs.yml b/mkdocs.yml index 8b77b289d..a8be998e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,8 @@ pages: - 'Authentication': 'api/authentication.md' - 'Working with Secrets': 'api/working-with-secrets.md' - 'Examples': 'api/examples.md' + - 'Shell': + - 'Introduction': 'shell/intro.md' markdown_extensions: - admonition: From 091cf390d28ea6d1fbb59372f8ca5b34a1d84ef0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 24 Jul 2017 14:22:07 -0400 Subject: [PATCH 04/92] Import constants from each app --- netbox/extras/management/commands/nbshell.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 48448c16f..918b97ef6 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/netbox/extras/management/commands/nbshell.py @@ -37,9 +37,11 @@ class Command(BaseCommand): def get_namespace(self): namespace = {} - # Gather Django models from each app + # Gather Django models and constants from each app for app in APPS: self.django_models[app] = [] + + # Models app_models = sys.modules['{}.models'.format(app)] for name in dir(app_models): model = getattr(app_models, name) @@ -50,6 +52,15 @@ class Command(BaseCommand): except TypeError: pass + # Constants + try: + app_constants = sys.modules['{}.constants'.format(app)] + for name in dir(app_constants): + namespace[name] = getattr(app_constants, name) + except KeyError: + pass + + # Load convenience commands namespace.update({ 'lsmodels': self._lsmodels, From 4047c1a4e4a4c167f1768c8c4bc099892ceb0831 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 24 Jul 2017 14:34:01 -0400 Subject: [PATCH 05/92] lsmodules() should only return native models --- netbox/extras/management/commands/nbshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 918b97ef6..9762f0cbd 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/netbox/extras/management/commands/nbshell.py @@ -46,7 +46,7 @@ class Command(BaseCommand): for name in dir(app_models): model = getattr(app_models, name) try: - if issubclass(model, Model): + if issubclass(model, Model) and model._meta.app_label == app: namespace[name] = model self.django_models[app].append(name) except TypeError: From 336cdcddc55c8cc7d5635b8aaa7e23a15fb356c1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 24 Jul 2017 14:51:00 -0400 Subject: [PATCH 06/92] PEP8 fix --- netbox/extras/management/commands/nbshell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 9762f0cbd..a50b1384d 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/netbox/extras/management/commands/nbshell.py @@ -60,7 +60,6 @@ class Command(BaseCommand): except KeyError: pass - # Load convenience commands namespace.update({ 'lsmodels': self._lsmodels, From 0991f94d0604f9f6fe48a2420ccf675606856f05 Mon Sep 17 00:00:00 2001 From: vanderaaj Date: Tue, 25 Jul 2017 08:40:51 -0500 Subject: [PATCH 07/92] How to migrate from Py2 to Py3 (#1355) * How to migrate from Py2 to Py3 The commands done to migrate Ubuntu from Py2 to Py3. * Update Migrating-to-Python3 --- docs/installation/Migrating-to-Python3 | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/installation/Migrating-to-Python3 diff --git a/docs/installation/Migrating-to-Python3 b/docs/installation/Migrating-to-Python3 new file mode 100644 index 000000000..09fe3657b --- /dev/null +++ b/docs/installation/Migrating-to-Python3 @@ -0,0 +1,27 @@ +# Migration +Remove Python 2 packages +```no-highlight +# apt-get remove --purge -y python-dev python-pip +``` + +Install Python 3 packages +```no-highlight +# apt-get install -y python3 python3-dev python3-pip +``` + +Install Python Packages +```no-highlight +# cd /opt/netbox +# pip3 install -r requirements.txt +``` + +Gunicorn Update +``` +# pip uninstall gunicorn +# pip3 install gunicorn +``` + +Re-install LDAP Module (Optional if using LDAP for auth) +``` +sudo pip3 install django-auth-ldap +``` From f9b6ddc230e457f68b609e0ab9df09c4c0534a28 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 09:44:15 -0400 Subject: [PATCH 08/92] Added "Migrating to Python3" to the docs index --- ...{Migrating-to-Python3 => migrating-to-python3.md} | 12 +++++++++--- mkdocs.yml | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) rename docs/installation/{Migrating-to-Python3 => migrating-to-python3.md} (81%) diff --git a/docs/installation/Migrating-to-Python3 b/docs/installation/migrating-to-python3.md similarity index 81% rename from docs/installation/Migrating-to-Python3 rename to docs/installation/migrating-to-python3.md index 09fe3657b..e99018252 100644 --- a/docs/installation/Migrating-to-Python3 +++ b/docs/installation/migrating-to-python3.md @@ -1,27 +1,33 @@ # Migration + Remove Python 2 packages + ```no-highlight # apt-get remove --purge -y python-dev python-pip ``` Install Python 3 packages + ```no-highlight # apt-get install -y python3 python3-dev python3-pip ``` Install Python Packages + ```no-highlight # cd /opt/netbox # pip3 install -r requirements.txt ``` Gunicorn Update -``` + +```no-highlight # pip uninstall gunicorn # pip3 install gunicorn ``` -Re-install LDAP Module (Optional if using LDAP for auth) -``` +Re-install LDAP Module (optional if using LDAP for auth) + +```no-highlight sudo pip3 install django-auth-ldap ``` diff --git a/mkdocs.yml b/mkdocs.yml index a8be998e4..f204749d5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ pages: - 'Web Server': 'installation/web-server.md' - 'LDAP (Optional)': 'installation/ldap.md' - 'Upgrading': 'installation/upgrading.md' + - 'Migrating to Python3': 'installation/migrating-to-python3.md' - 'Configuration': - 'Mandatory Settings': 'configuration/mandatory-settings.md' - 'Optional Settings': 'configuration/optional-settings.md' From 9e26198afe61c6d31a2b94114c250533a8a8e090 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 10:09:44 -0400 Subject: [PATCH 09/92] Added note about NAPALM integration --- docs/installation/netbox.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index c7c2eb8ed..c6c492626 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -32,6 +32,11 @@ Python 2: # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel ``` +!!! info + As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to retrieve live data from devices and return it to a requester via its REST API. + + Installation of NAPALM is optional. To enable it, simply install the "napalm" package using your distribution's package manager. + You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. ## Option A: Download a Release From e364659c6e85ba78653f9be372776daa8dc891fc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 10:17:28 -0400 Subject: [PATCH 10/92] Tweaked NAPALM integration instructions --- docs/installation/netbox.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index c6c492626..4befbeefc 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -32,11 +32,6 @@ Python 2: # yum install -y gcc python2 python-devel python-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel ``` -!!! info - As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to retrieve live data from devices and return it to a requester via its REST API. - - Installation of NAPALM is optional. To enable it, simply install the "napalm" package using your distribution's package manager. - You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. ## Option A: Download a Release @@ -102,6 +97,14 @@ Python 2: # pip install -r requirements.txt ``` +### NAPALM Automation + +As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3: + +```no-highlight +# pip install napalm +``` + # Configuration Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. From 1770c8568940cd801d0d2b6c7948f778043fec51 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 10:56:23 -0400 Subject: [PATCH 11/92] Fixes #1371: Extend DeviceSerializer.parent_device to include standard fields --- netbox/dcim/api/serializers.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 09da3ced7..50bf756e3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -473,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer): device_bay = obj.parent_bay except DeviceBay.DoesNotExist: return None - return { - 'id': device_bay.device.pk, - 'name': device_bay.device.name, - 'device_bay': { - 'id': device_bay.pk, - 'name': device_bay.name, - } - } + context = {'request': self.context['request']} + data = NestedDeviceSerializer(instance=device_bay.device, context=context).data + data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data + return data class WritableDeviceSerializer(CustomFieldModelSerializer): @@ -690,6 +686,14 @@ class DeviceBaySerializer(serializers.ModelSerializer): fields = ['id', 'device', 'name', 'installed_device'] +class NestedDeviceBaySerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') + + class Meta: + model = DeviceBay + fields = ['id', 'url', 'name'] + + class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: From 7476194bd149a82aa3fbc3bc66990c1f118a55f7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 10:58:28 -0400 Subject: [PATCH 12/92] PEP8 fix --- netbox/utilities/templatetags/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index b7a40d018..7cb2753e7 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -69,7 +69,7 @@ def example_choices(field, arg=3): """ examples = [] if hasattr(field, 'queryset'): - choices = [(obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg+1]] + choices = [(obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg + 1]] else: choices = field.choices for id, label in choices: From c7e9d90321b1566b0ab191adfddcc95673b54671 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 11:19:33 -0400 Subject: [PATCH 13/92] Release v2.1.0 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b3f0b5187..28d98acf1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.1.0-dev' +VERSION = '2.1.0' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From f4a873745f48f2c53bca0cd95a7198b3daea8341 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2017 11:26:50 -0400 Subject: [PATCH 14/92] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 28d98acf1..d48b72c1f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.1.0' +VERSION = '2.1.1-dev' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From d2c6d795894a0586ffb4d56c020bdd043abe39c8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 26 Jul 2017 11:24:19 -0400 Subject: [PATCH 15/92] Closes #1375: Renamed NETBOX_USERNAME and NETBOX_PASSWORD configuration parameters to NAPALM_USERNAME and NAPALM_PASSWORD --- docs/configuration/optional-settings.md | 16 +++++++++--- netbox/dcim/api/views.py | 4 +-- .../management/commands/run_inventory.py | 4 +-- netbox/netbox/configuration.docker.py | 4 +-- netbox/netbox/configuration.example.py | 6 ++--- netbox/netbox/settings.py | 25 +++++++++++++++---- netbox/templates/home.html | 8 ++++++ 7 files changed, 50 insertions(+), 17 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 05e60dcac..aacb50410 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -135,11 +135,21 @@ An API consumer can request an arbitrary number of objects by appending the "lim --- -## NETBOX_USERNAME +## NAPALM_USERNAME -## NETBOX_PASSWORD +## NAPALM_PASSWORD -If provided, NetBox will use these credentials to authenticate against devices when collecting data. +NetBox will use these credentials when authenticating to remote devices via the NAPALM library. Both parameters are optional. + +Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed. + +--- + +## NETBOX_USERNAME (Deprecated) + +## NETBOX_PASSWORD (Deprecated) + +These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead. --- diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d32c63bfa..bfbe3c549 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -272,8 +272,8 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): ip_address = str(device.primary_ip.address.ip) d = driver( hostname=ip_address, - username=settings.NETBOX_USERNAME, - password=settings.NETBOX_PASSWORD + username=settings.NAPALM_USERNAME, + password=settings.NAPALM_PASSWORD ) try: d.open() diff --git a/netbox/extras/management/commands/run_inventory.py b/netbox/extras/management/commands/run_inventory.py index 1e52b5c8f..335cdb783 100644 --- a/netbox/extras/management/commands/run_inventory.py +++ b/netbox/extras/management/commands/run_inventory.py @@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE class Command(BaseCommand): help = "Update inventory information for specified devices" - username = settings.NETBOX_USERNAME - password = settings.NETBOX_PASSWORD + username = settings.NAPALM_USERNAME + password = settings.NAPALM_PASSWORD def add_arguments(self, parser): parser.add_argument('-u', '--username', dest='username', help="Specify the username to use") diff --git a/netbox/netbox/configuration.docker.py b/netbox/netbox/configuration.docker.py index c57aca6f4..56f9da366 100644 --- a/netbox/netbox/configuration.docker.py +++ b/netbox/netbox/configuration.docker.py @@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '') MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False) # Credentials that NetBox will use to access live devices. -NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '') -NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '') +NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '') +NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '') # Determine how many objects to display per page within a list. (Default: 50) PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 2e08090c7..834f322bb 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -93,9 +93,9 @@ MAINTENANCE_MODE = False # all objects by specifying "?limit=0". MAX_PAGE_SIZE = 1000 -# Credentials that NetBox will use to access live devices (future use). -NETBOX_USERNAME = '' -NETBOX_PASSWORD = '' +# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM. +NAPALM_USERNAME = '' +NAPALM_PASSWORD = '' # Determine how many objects to display per page within a list. (Default: 50) PAGINATE_COUNT = 50 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d48b72c1f..fe863833f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -46,8 +46,10 @@ MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False) MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000) PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) -NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') -NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') +NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') +NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') +NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated +NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') @@ -56,6 +58,19 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC') CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS +# Check for deprecated configuration parameters +config_logger = logging.getLogger('configuration') +config_logger.addHandler(logging.StreamHandler()) +config_logger.setLevel(logging.WARNING) +if NETBOX_USERNAME: + config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.') + if not NAPALM_USERNAME: + NAPALM_USERNAME = NETBOX_USERNAME +if NETBOX_PASSWORD: + config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.') + if not NAPALM_PASSWORD: + NAPALM_PASSWORD = NETBOX_PASSWORD + # Attempt to import LDAP configuration if it has been defined LDAP_IGNORE_CERT_ERRORS = False try: @@ -78,9 +93,9 @@ if LDAP_CONFIGURED: if LDAP_IGNORE_CERT_ERRORS: ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) # Enable logging for django_auth_ldap - logger = logging.getLogger('django_auth_ldap') - logger.addHandler(logging.StreamHandler()) - logger.setLevel(logging.DEBUG) + ldap_logger = logging.getLogger('django_auth_ldap') + ldap_logger.addHandler(logging.StreamHandler()) + ldap_logger.setLevel(logging.DEBUG) except ImportError: raise ImproperlyConfigured( "LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove " diff --git a/netbox/templates/home.html b/netbox/templates/home.html index f553ed9c8..0c3c7d13b 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -1,6 +1,14 @@ {% extends '_base.html' %} {% block content %} +{% if settings.NETBOX_USERNAME or settings.NETBOX_PASSWORD %} + +{% endif %} {% include 'search_form.html' %}
From 65a633f42dbde39ec9de681cce108079155100d1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 26 Jul 2017 11:47:59 -0400 Subject: [PATCH 16/92] Closes #1374: Added NAPALM_ARGS and NAPALM_TIMEOUT configiuration parameters --- docs/configuration/optional-settings.md | 34 ++++++++++++++++++++++++- netbox/dcim/api/views.py | 6 +++-- netbox/netbox/configuration.example.py | 7 +++++ netbox/netbox/settings.py | 2 ++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index aacb50410..83b289b9a 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -139,12 +139,44 @@ An API consumer can request an arbitrary number of objects by appending the "lim ## NAPALM_PASSWORD -NetBox will use these credentials when authenticating to remote devices via the NAPALM library. Both parameters are optional. +NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional. Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed. --- +## NAPALM_ARGS + +A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example: + +``` +NAPALM_ARGS = { + 'api_key': '472071a93b60a1bd1fafb401d9f8ef41', + 'port': 2222, +} +``` + +Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument: + +``` +NAPALM_USERNAME = 'username' +NAPALM_PASSWORD = 'MySecretPassword' +NAPALM_ARGS = { + 'secret': NAPALM_PASSWORD, + # Include any additional args here +} +``` + +--- + +## NAPALM_TIMEOUT + +Default: 30 seconds + +The amount of time (in seconds) to wait for NAPALM to connect to a device. + +--- + ## NETBOX_USERNAME (Deprecated) ## NETBOX_PASSWORD (Deprecated) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index bfbe3c549..116c8db00 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -273,14 +273,16 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): d = driver( hostname=ip_address, username=settings.NAPALM_USERNAME, - password=settings.NAPALM_PASSWORD + password=settings.NAPALM_PASSWORD, + timeout=settings.NAPALM_TIMEOUT, + optional_args=settings.NAPALM_ARGS ) try: d.open() for method in napalm_methods: response[method] = getattr(d, method)() except Exception as e: - raise ServiceUnavailable("Error connecting to the device: {}".format(e)) + raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e)) d.close() return Response(response) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 834f322bb..78e870072 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -97,6 +97,13 @@ MAX_PAGE_SIZE = 1000 NAPALM_USERNAME = '' NAPALM_PASSWORD = '' +# NAPALM timeout (in seconds). (Default: 30) +NAPALM_TIMEOUT = 30 + +# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must +# be provided as a dictionary. +NAPALM_ARGS = {} + # Determine how many objects to display per page within a list. (Default: 50) PAGINATE_COUNT = 50 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index fe863833f..2cac4ad7d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -48,6 +48,8 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') +NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30) +NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {}) NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') From f2c16fbf3c318f0aac8045b9fa5fa443e2d9f516 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 27 Jul 2017 09:53:41 -0400 Subject: [PATCH 17/92] Closes #893: Allow filtering by null values for NullCharacterFields (e.g. return only unnamed devices) --- netbox/dcim/filters.py | 12 ++++++++---- netbox/utilities/filters.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e3579085a..ed6106c86 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,7 +8,7 @@ from django.db.models import Q from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter +from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection, @@ -113,6 +113,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): method='search', label='Search', ) + facility_id = NullableCharFieldFilter() site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -156,7 +157,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Rack - fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units'] + fields = ['type', 'width', 'u_height', 'desc_units'] def search(self, queryset, name, value): if not value.strip(): @@ -383,6 +384,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Platform (slug)', ) + name = NullableCharFieldFilter() + asset_tag = NullableCharFieldFilter() site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -439,7 +442,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Device - fields = ['name', 'serial', 'asset_tag'] + fields = ['serial'] def search(self, queryset, name, value): if not value.strip(): @@ -596,10 +599,11 @@ class InventoryItemFilter(DeviceComponentFilterSet): to_field_name='slug', label='Manufacturer (slug)', ) + asset_tag = NullableCharFieldFilter() class Meta: model = InventoryItem - fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered'] + fields = ['name', 'part_id', 'serial', 'discovered'] class ConsoleConnectionFilter(django_filters.FilterSet): diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 5929c3ff1..5bd635a46 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -19,6 +19,16 @@ class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): pass +class NullableCharFieldFilter(django_filters.CharFilter): + null_value = 'NULL' + + def filter(self, qs, value): + if value != self.null_value: + return super(NullableCharFieldFilter, self).filter(qs, value) + qs = self.get_method(qs)(**{'{}__isnull'.format(self.name): True}) + return qs.distinct() if self.distinct else qs + + class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField): """ This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is From 52a490bf5d86d92957f0409114c4d69c1ed8a6d8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Jul 2017 10:05:51 -0400 Subject: [PATCH 18/92] Fixes #461: Display a validation error when attempting to assigning a new child device to a rack face/position --- netbox/templates/dcim/device_edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 7bfec5469..07206ca27 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -45,7 +45,7 @@

- {% elif not obj.device_type.is_child_device %} + {% else %} {% render_field form.face %} {% render_field form.position %} {% endif %} From cab2929c108a45a39cdaf74b6b6b5859eeda1e9d Mon Sep 17 00:00:00 2001 From: bdlamprecht Date: Wed, 2 Aug 2017 08:45:00 -0600 Subject: [PATCH 19/92] Updated ldap.md Add mapping of `email` field to `mail` value in LDAP. Also remove duplicate entries of such mapping information. --- docs/installation/ldap.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/installation/ldap.md b/docs/installation/ldap.md index 0d546863e..b43105b8b 100644 --- a/docs/installation/ldap.md +++ b/docs/installation/ldap.md @@ -72,7 +72,8 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" # You can map user attributes to Django attributes as so. AUTH_LDAP_USER_ATTR_MAP = { "first_name": "givenName", - "last_name": "sn" + "last_name": "sn", + "email": "mail" } ``` @@ -108,12 +109,3 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. * `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions. - -It is also possible map user attributes to Django attributes: - -```python -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn", -} -``` From fd6df8e52a0876780b1c223a4cc129bb930b0b72 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Aug 2017 11:17:57 -0400 Subject: [PATCH 20/92] Fixes #1385: Connected device API endpoint no longer requires authentication if LOGIN_REQUIRED=False --- netbox/dcim/api/views.py | 5 ++--- netbox/utilities/api.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 116c8db00..56d4221da 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,7 +3,6 @@ from collections import OrderedDict from rest_framework.decorators import detail_route from rest_framework.mixins import ListModelMixin -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet @@ -21,7 +20,7 @@ from dcim import filters from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from utilities.api import ServiceUnavailable, WritableSerializerMixin +from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin from .exceptions import MissingFilterException from . import serializers @@ -387,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet): * `peer-device`: The name of the peer device * `peer-interface`: The name of the peer interface """ - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticatedOrLoginNotRequired] def get_view_name(self): return "Connected Device Locator" diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 5774584a6..6a515b21d 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -4,9 +4,10 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from rest_framework import authentication, exceptions +from rest_framework.compat import is_authenticated from rest_framework.exceptions import APIException from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS +from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS from rest_framework.serializers import Field, ValidationError from users.models import Token @@ -20,6 +21,10 @@ class ServiceUnavailable(APIException): default_detail = "Service temporarily unavailable, please try again later." +# +# Authentication +# + class TokenAuthentication(authentication.TokenAuthentication): """ A custom authentication scheme which enforces Token expiration times. @@ -61,6 +66,20 @@ class TokenPermissions(DjangoModelPermissions): return super(TokenPermissions, self).has_permission(request, view) +class IsAuthenticatedOrLoginNotRequired(BasePermission): + """ + Returns True if the user is authenticated or LOGIN_REQUIRED is False. + """ + def has_permission(self, request, view): + if not settings.LOGIN_REQUIRED: + return True + return request.user and is_authenticated(request.user) + + +# +# Serializers +# + class ChoiceFieldSerializer(Field): """ Represent a ChoiceField as {'value': , 'label': }. @@ -98,6 +117,10 @@ class ContentTypeFieldSerializer(Field): raise ValidationError("Invalid content type") +# +# Mixins +# + class ModelValidationMixin(object): """ Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're @@ -119,6 +142,10 @@ class WritableSerializerMixin(object): return self.serializer_class +# +# Pagination +# + class OptionalLimitOffsetPagination(LimitOffsetPagination): """ Override the stock paginator to allow setting limit=0 to disable pagination for a request. This returns all objects From e50f234ba3aac2ced950db37a7ec66aeed1d9530 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Aug 2017 11:54:53 -0400 Subject: [PATCH 21/92] Closes #1379: Allow searching devices by interface MAC address in global search --- netbox/dcim/filters.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index ed6106c86..dcd6c6d2e 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import django_filters +from netaddr import EUI from netaddr.core import AddrFormatError from django.contrib.auth.models import User @@ -447,20 +448,28 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): def search(self, queryset, name, value): if not value.strip(): return queryset - return queryset.filter( + qs_filter = ( Q(name__icontains=value) | Q(serial__icontains=value.strip()) | Q(inventory_items__serial__icontains=value.strip()) | Q(asset_tag=value.strip()) | Q(comments__icontains=value) - ).distinct() + ) + # If the query value looks like a MAC address, search interfaces as well. + try: + mac = EUI(value.strip()) + qs_filter |= Q(interfaces__mac_address=mac) + except AddrFormatError: + pass + return queryset.filter(qs_filter).distinct() def _mac_address(self, queryset, name, value): value = value.strip() if not value: return queryset try: - return queryset.filter(interfaces__mac_address=value).distinct() + mac = EUI(value.strip()) + return queryset.filter(interfaces__mac_address=mac).distinct() except AddrFormatError: return queryset.none() @@ -572,7 +581,8 @@ class InterfaceFilter(django_filters.FilterSet): if not value: return queryset try: - return queryset.filter(mac_address=value) + mac = EUI(value.strip()) + return queryset.filter(mac_address=mac) except AddrFormatError: return queryset.none() From 3876a96b67f0ae5b091ae45c3e6fabf540ca1727 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Aug 2017 12:55:21 -0400 Subject: [PATCH 22/92] Tweaked IP address layout --- netbox/templates/ipam/ipaddress.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 44c5ec5ff..f386c89bf 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -43,7 +43,7 @@

{{ ipaddress }}

{% include 'inc/created_updated.html' with obj=ipaddress %}
-
+
IP Address @@ -137,7 +137,7 @@ {% include 'inc/custom_fields_panel.html' %} {% endwith %}
-
+
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %} {% if duplicate_ips_table.rows %} {% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %} From 12d643694f7bbab55ccf5c0e567fefa6f6dbe42a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Aug 2017 13:15:28 -0400 Subject: [PATCH 23/92] Cleaned up title blocks --- netbox/templates/circuits/circuit.html | 4 +--- netbox/templates/circuits/circuit_list.html | 4 +--- netbox/templates/circuits/circuittermination_edit.html | 6 +----- netbox/templates/circuits/circuittype_list.html | 4 +--- netbox/templates/circuits/provider.html | 4 +--- netbox/templates/circuits/provider_list.html | 4 +--- netbox/templates/dcim/console_connections_list.html | 4 +--- netbox/templates/dcim/consoleport_connect.html | 4 +--- netbox/templates/dcim/consoleserverport_connect.html | 4 +--- netbox/templates/dcim/device_list.html | 4 +--- netbox/templates/dcim/devicebay_populate.html | 4 +--- netbox/templates/dcim/devicerole_list.html | 4 +--- netbox/templates/dcim/devicetype.html | 4 +--- netbox/templates/dcim/devicetype_list.html | 4 +--- netbox/templates/dcim/interface_connections_list.html | 4 +--- netbox/templates/dcim/manufacturer_list.html | 4 +--- netbox/templates/dcim/platform_list.html | 4 +--- netbox/templates/dcim/power_connections_list.html | 4 +--- netbox/templates/dcim/poweroutlet_connect.html | 4 +--- netbox/templates/dcim/powerport_connect.html | 4 +--- netbox/templates/dcim/rack.html | 4 +--- netbox/templates/dcim/rack_list.html | 4 +--- netbox/templates/dcim/rackgroup_list.html | 4 +--- netbox/templates/dcim/rackrole_list.html | 4 +--- netbox/templates/dcim/region_list.html | 4 +--- netbox/templates/dcim/site.html | 4 +--- netbox/templates/dcim/site_list.html | 4 +--- netbox/templates/ipam/aggregate.html | 4 +--- netbox/templates/ipam/aggregate_list.html | 4 +--- netbox/templates/ipam/ipaddress.html | 4 +--- netbox/templates/ipam/ipaddress_assign.html | 4 +--- netbox/templates/ipam/ipaddress_list.html | 4 +--- netbox/templates/ipam/prefix_ipaddresses.html | 2 +- netbox/templates/ipam/prefix_list.html | 4 +--- netbox/templates/ipam/rir_list.html | 4 +--- netbox/templates/ipam/role_list.html | 4 +--- netbox/templates/ipam/vlan.html | 4 +--- netbox/templates/ipam/vlan_list.html | 4 +--- netbox/templates/ipam/vlangroup_list.html | 4 +--- netbox/templates/ipam/vrf.html | 4 +--- netbox/templates/ipam/vrf_list.html | 4 +--- netbox/templates/secrets/secret.html | 4 +--- netbox/templates/secrets/secret_edit.html | 4 +--- netbox/templates/secrets/secret_import.html | 4 +--- netbox/templates/secrets/secret_list.html | 4 +--- netbox/templates/secrets/secretrole_list.html | 4 +--- netbox/templates/tenancy/tenant.html | 4 +--- netbox/templates/tenancy/tenant_list.html | 4 +--- netbox/templates/tenancy/tenantgroup_list.html | 4 +--- 49 files changed, 49 insertions(+), 147 deletions(-) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index f311ccb73..383d3bb7a 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -1,8 +1,6 @@ {% extends '_base.html' %} {% load helpers %} -{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %} - {% block content %}
@@ -39,7 +37,7 @@ {% endif %}
-

{{ circuit.provider }} - {{ circuit.cid }}

+

{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}

{% include 'inc/created_updated.html' with obj=circuit %}
diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index 63ee92f2d..418459a15 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -1,8 +1,6 @@ {% extends '_base.html' %} {% load helpers %} -{% block title %}Circuits{% endblock %} - {% block content %}
{% if perms.circuits.add_circuit %} @@ -17,7 +15,7 @@ {% endif %} {% include 'inc/export_button.html' with obj_type='circuits' %}
-

Circuits

+

{% block title %}Circuits{% endblock %}

{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %} diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 13aa7e5b6..13052966b 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -2,10 +2,6 @@ {% load staticfiles %} {% load form_helpers %} -{% block title %} - Circuit {{ obj.circuit }} - Side {{ form.term_side.value }} -{% endblock %} - {% block content %}
{% csrf_token %} @@ -14,7 +10,7 @@ {% endfor %}
-

Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}

+

{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}

{% if form.non_field_errors %}
Errors
diff --git a/netbox/templates/circuits/circuittype_list.html b/netbox/templates/circuits/circuittype_list.html index d59a5b82f..f545b1a1e 100644 --- a/netbox/templates/circuits/circuittype_list.html +++ b/netbox/templates/circuits/circuittype_list.html @@ -1,8 +1,6 @@ {% extends '_base.html' %} {% load helpers %} -{% block title %}Circuit Types{% endblock %} - {% block content %}
{% if perms.circuits.add_circuittype %} @@ -12,7 +10,7 @@ {% endif %}
-

Circuit Types

+

{% block title %}Circuit Types{% endblock %}

{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 35562a7a3..6dcccfd8d 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -2,8 +2,6 @@ {% load static from staticfiles %} {% load helpers %} -{% block title %}{{ provider }}{% endblock %} - {% block content %}
@@ -45,7 +43,7 @@ {% endif %}
-

{{ provider }}

+

{% block title %}{{ provider }}{% endblock %}

{% include 'inc/created_updated.html' with obj=provider %}
diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index 36438d66b..9ba8bb838 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -1,7 +1,5 @@ {% extends '_base.html' %} -{% block title %}Providers{% endblock %} - {% block content %}
{% if perms.circuits.add_provider %} @@ -16,7 +14,7 @@ {% endif %} {% include 'inc/export_button.html' with obj_type='providers' %}
-

Providers

+

{% block title %}Providers{% endblock %}

{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %} diff --git a/netbox/templates/dcim/console_connections_list.html b/netbox/templates/dcim/console_connections_list.html index fc2cbb762..57d3435e5 100644 --- a/netbox/templates/dcim/console_connections_list.html +++ b/netbox/templates/dcim/console_connections_list.html @@ -1,7 +1,5 @@ {% extends '_base.html' %} -{% block title %}Console Connections{% endblock %} - {% block content %}
{% if perms.dcim.change_consoleport %} @@ -12,7 +10,7 @@ {% endif %} {% include 'inc/export_button.html' with obj_type='connections' %}
-

Console Connections

+

{% block title %}Console Connections{% endblock %}

{% include 'responsive_table.html' %} diff --git a/netbox/templates/dcim/consoleport_connect.html b/netbox/templates/dcim/consoleport_connect.html index e06bf45ec..679540960 100644 --- a/netbox/templates/dcim/consoleport_connect.html +++ b/netbox/templates/dcim/consoleport_connect.html @@ -2,8 +2,6 @@ {% load static from staticfiles %} {% load form_helpers %} -{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %} - {% block content %} {% csrf_token %} @@ -21,7 +19,7 @@
{% endif %}
-
Connect {{ consoleport.device }} {{ consoleport }}
+
{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
{% endif %}
-
Connect {{ consoleserverport.device }} {{ consoleserverport }}
+
{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}