From 9acd0e99f99c3a5ec5214e60b491fa517f70aadf Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 28 Jun 2016 10:55:38 -0400 Subject: [PATCH 01/11] Add sanity check checklist for submitting pull requests --- CONTRIBUTING.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c36885ea..f437d2516 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,14 @@ Even if it's not quite right for NetBox, we may be able to point you to a tool b * A rough description of any changes necessary to the database schema (if applicable) * Any third-party libraries or other resources which would be involved -# Submitting Pull Requests +## Submitting Pull Requests -When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`. +* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`. In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases. + +* All code submissions should meet the following criteria (CI will enforce these checks): + + * Python syntax is valid + * All tests pass when run with `./manage.py test netbox/` + * PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length From 6c415794cd4aea385b964be8d5d87b99442775e4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 12:53:43 -0400 Subject: [PATCH 02/11] Corrected description of prefix and VLAN statuses --- docs/ipam.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/ipam.md b/docs/ipam.md index 53c5858f2..ff3810097 100644 --- a/docs/ipam.md +++ b/docs/ipam.md @@ -52,15 +52,13 @@ A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefix ### Statuses -Each prefix is assigned an operational status. This may be one of the following: +Each prefix is assigned an operational status. This is one of the following: * Container - A summary of child prefixes * Active - Provisioned and in use * Reserved - Earmarked for future use * Deprecated - No longer in use -NetBox provides several statuses by default, but you are free to change them to suit the needs of your organization. - ### Roles Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include: @@ -71,7 +69,7 @@ Whereas a status describes a prefix's operational state, a role describes its fu * Lab * Out-of-band -Role assignment is optional. And like statuses, you are free to create your own. +Role assignment is optional and you are free to create as many as you'd like. --- From ec667eeed0d4f317226cb28541f915612e0bae61 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 13:32:47 -0400 Subject: [PATCH 03/11] Fixes #84: Added IFACE_FF_10GE_COPPER --- .../migrations/0003_auto_20160628_1721.py | 25 +++++++++++++++++++ netbox/dcim/models.py | 6 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 netbox/dcim/migrations/0003_auto_20160628_1721.py diff --git a/netbox/dcim/migrations/0003_auto_20160628_1721.py b/netbox/dcim/migrations/0003_auto_20160628_1721.py new file mode 100644 index 000000000..deebc8518 --- /dev/null +++ b/netbox/dcim/migrations/0003_auto_20160628_1721.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-28 17:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0002_auto_20160622_1821'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b6ec28be2..c00a1043b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -45,14 +45,16 @@ IFACE_FF_VIRTUAL = 0 IFACE_FF_100M_COPPER = 800 IFACE_FF_1GE_COPPER = 1000 IFACE_FF_SFP = 1100 +IFACE_FF_10GE_COPPER = 1150 IFACE_FF_SFP_PLUS = 1200 IFACE_FF_XFP = 1300 IFACE_FF_QSFP_PLUS = 1400 IFACE_FF_CHOICES = [ [IFACE_FF_VIRTUAL, 'Virtual'], - [IFACE_FF_100M_COPPER, '10/100M (Copper)'], - [IFACE_FF_1GE_COPPER, '1GE (Copper)'], + [IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'], + [IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'], [IFACE_FF_SFP, '1GE (SFP)'], + [IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'], [IFACE_FF_SFP_PLUS, '10GE (SFP+)'], [IFACE_FF_XFP, '10GE (XFP)'], [IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'], From fdfc32899d12607821456d7e973d1327fef522b1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 14:10:16 -0400 Subject: [PATCH 04/11] Fixes #75: Ignore a Device's occupied rack units when relocating it within a rack --- netbox/dcim/forms.py | 7 +++++-- netbox/dcim/models.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 776306a07..b158a8349 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -386,10 +386,13 @@ class DeviceForm(forms.ModelForm, BootstrapMixin): # Rack position try: + pk = self.instance.pk if self.instance.pk else None if self.is_bound and self.data.get('rack') and str(self.data.get('face')): - position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=self.data.get('face')) + position_choices = Rack.objects.get(pk=self.data['rack'])\ + .get_rack_units(face=self.data.get('face'), exclude=pk) elif self.initial.get('rack') and str(self.initial.get('face')): - position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face')) + position_choices = Rack.objects.get(pk=self.initial['rack'])\ + .get_rack_units(face=self.initial.get('face'), exclude=pk) else: position_choices = [] except Rack.DoesNotExist: diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index c00a1043b..1e5ca1aed 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -215,12 +215,13 @@ class Rack(CreatedUpdatedModel): return "{} ({})".format(self.name, self.facility_id) return self.name - def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False): + def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) + :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack :param remove_redundant: If True, rack units occupied by a device already listed will be omitted """ @@ -231,7 +232,9 @@ class Rack(CreatedUpdatedModel): # Add devices to rack units list if self.pk: for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ - .filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)): + .exclude(pk=exclude)\ + .filter(rack=self, position__gt=0)\ + .filter(Q(face=face) | Q(device_type__is_full_depth=True)): if remove_redundant: elevation[device.position]['device'] = device for u in range(device.position + 1, device.position + device.device_type.u_height): From 8b357a311d4da557e447b1383ad8db3b3dc1d137 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 14:53:33 -0400 Subject: [PATCH 05/11] Fixes #61: Added list of RackGroups to Site view --- netbox/dcim/views.py | 2 ++ netbox/templates/dcim/site.html | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 50c3243d8..51e508e55 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -75,11 +75,13 @@ def site(request, slug): 'vlan_count': VLAN.objects.filter(site=site).count(), 'circuit_count': Circuit.objects.filter(site=site).count(), } + rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) topology_maps = TopologyMap.objects.filter(site=site) return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, + 'rack_groups': rack_groups, 'topology_maps': topology_maps, }) diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index e6425ecb9..bddc8a505 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -124,6 +124,25 @@ +
+
+ Rack Groups +
+ {% if rack_groups %} + + {% for rg in rack_groups %} + + + + + {% endfor %} +
{{ rg.name }}{{ rg.rack_count }}
+ {% else %} +
+ None +
+ {% endif %} +
Topology Maps @@ -132,7 +151,7 @@ {% for tm in topology_maps %} - + {% endfor %} From e7615cf32fbcfe1375de30225acd729d37451eb0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 15:58:50 -0400 Subject: [PATCH 06/11] Added instructions for upgrading NetBox --- docs/getting-started.md | 53 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6e9944cd3..6bc8e6a88 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -48,11 +48,12 @@ You can verify that authentication works using the following command: # NetBox -## Dependencies +## Installation + +NetBox requires following dependencies: * python2.7 * python-dev -* git * python-pip * libxml2-dev * libxslt1-dev @@ -65,7 +66,21 @@ You can verify that authentication works using the following command: *graphviz is needed to render topology maps. If you have no need for this feature, graphviz is not required. -## Clone the Git Repository +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 + +Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. + +``` +# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz +# tar -xzf vX.Y.Z.tar.gz -C /opt +# cd /opt/ +# ln -s netbox-1.0.4/ netbox +# cd /opt/netbox/ +``` + +### Option B: Clone the Git Repository Create the base directory for the NetBox installation. For this guide, we'll use `/opt/netbox`. @@ -74,6 +89,12 @@ Create the base directory for the NetBox installation. For this guide, we'll use # cd /opt/netbox/ ``` +If `git` is not already installed, install it: + +``` +# sudo apt-get install git +``` + Next, clone the NetBox git repository into the current directory: ``` @@ -87,6 +108,8 @@ Resolving deltas: 100% (1495/1495), done. Checking connectivity... done. ``` +### Install Python Packages + Install the necessary Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the required dependencies.) ``` @@ -329,4 +352,26 @@ Finally, restart the supervisor service to detect and run the gunicorn service: At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running. -Please keep in mind that the configurations provided here are a bare minimum to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. +Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment. + +# Upgrading + +As with the initial installation, you can upgrade NetBox by either downloading the lastest release package or by cloning the `master` branch of the git repository. Several important steps are required before running the new code. + +First, apply any database migrations that were included with the release. Not all releases include database migrations (in fact, most don't), so don't worry if this command returns "No migrations to apply." + +``` +# ./manage.py migrate +``` + +Second, collect any static file that have changed into the root static path. As with database migrations, not all releases will include changes to static files. + +``` +# ./manage.py collectstatic +``` + +Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: + +``` +# sudo supervisorctl restart netbox +``` From 4a04af145bc01c909adc52eda6298b00f874ae7b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 16:01:48 -0400 Subject: [PATCH 07/11] Fixed VRF filter for API --- netbox/ipam/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 4f9da2657..018e4f366 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,7 +1,7 @@ from rest_framework import generics from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN -from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter +from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter from . import serializers @@ -12,6 +12,7 @@ class VRFListView(generics.ListAPIView): """ queryset = VRF.objects.all() serializer_class = serializers.VRFSerializer + filter_class = VRFFilter class VRFDetailView(generics.RetrieveAPIView): From 945ca314609bc1de9b861cfa759003430ed133da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 17:12:09 -0400 Subject: [PATCH 08/11] Fixes #92: Redirect to module creation page on 'add another' --- netbox/dcim/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 51e508e55..1500f096e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1516,7 +1516,10 @@ def module_add(request, pk): module.device = device module.save() messages.success(request, "Added module {} to {}".format(module.name, module.device.name)) - return redirect('dcim:device_inventory', pk=module.device.pk) + if '_addanother' in request.POST: + return redirect('dcim:module_add', pk=module.device.pk) + else: + return redirect('dcim:device_inventory', pk=module.device.pk) else: form = forms.ModuleForm() From 43e030f1db46709b0d8c0522f0e7502a43d13d2b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 20:21:49 -0400 Subject: [PATCH 09/11] Fixes #83: Corrected example Apache configuration --- docs/getting-started.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6bc8e6a88..788c3a88f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -287,38 +287,38 @@ Restart the nginx service to use the new configuration. ``` ## Apache Configuration -If you're feeling adventurous, or you already have Apache installed and can't run a dual-stack on your server, the following configuration should work for Apache: +The following configuration should work for Apache. Be sure to modify the `ServerName` appropriately. ``` ProxyPreserveHost On - + ServerName netbox.example.com - Alias /static/ /opt/netbox/netbox/static + Alias /static /opt/netbox/netbox/static Options Indexes FollowSymLinks MultiViews AllowOverride None - Order allow,deny - Allow from all - # Uncomment the line below if running Apache 2.4 - #Require all granted + Require all granted ProxyPass ! - ProxyPass / http://127.0.0.1:8001 - ProxyPassReverse / http://127.0.0.1:8001 + ProxyPass / http://127.0.0.1:8001/ + ProxyPassReverse / http://127.0.0.1:8001/ ``` -Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf` and reload Apache: +Save the contents of the above example in `/etc/apache2/sites-available/netbox.conf`, enable the `proxy` and `proxy_http` modules, and reload Apache: ``` -# a2ensite netbox; service apache2 restart +# a2enmod proxy +# a2enmod proxy_http +# a2ensite netbox +# service apache2 restart ``` ## gunicorn Configuration From b02c54ce527f0aa2c8712ab444d79acd8478ebc4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Jun 2016 23:22:41 -0400 Subject: [PATCH 10/11] A modest attempt at improving interface ordering; see #9 --- netbox/dcim/models.py | 65 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1e5ca1aed..013ca927b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -85,6 +85,48 @@ RPC_CLIENT_CHOICES = [ ] +def order_interfaces(queryset, sql_col, primary_ordering=tuple()): + """ + Attempt to match interface names by their slot/position identifiers and order according. Matching is done using the + following pattern: + + {a}/{b}/{c}:{d} + + Interfaces are ordered first by field a, then b, then c, and finally d. Leading text (which typically indicates the + interface's type) is ignored. If any fields are not contained by an interface name, those fields are treated as + None. 'None' is ordered after all other values. For example: + + et-0/0/0 + et-0/0/1 + et-0/1/0 + xe-0/1/1:0 + xe-0/1/1:1 + xe-0/1/1:2 + xe-0/1/1:3 + et-0/1/2 + ... + et-0/1/9 + et-0/1/10 + et-0/1/11 + et-1/0/0 + et-1/0/1 + ... + vlan1 + vlan10 + + :param queryset: The base queryset to be ordered + :param sql_col: Table and name of the SQL column which contains the interface name (ex: ''dcim_interface.name') + :param primary_ordering: A tuple of fields which take ordering precedence before the interface name (optional) + """ + ordering = primary_ordering + ('_id1', '_id2', '_id3', '_id4') + return queryset.extra(select={ + '_id1': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_id2': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col), + '_id3': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col), + '_id4': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col), + }).order_by(*ordering) + + class Site(CreatedUpdatedModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility @@ -413,6 +455,13 @@ class PowerOutletTemplate(models.Model): return self.name +class InterfaceTemplateManager(models.Manager): + + def get_queryset(self): + qs = super(InterfaceTemplateManager, self).get_queryset() + return order_interfaces(qs, 'dcim_interfacetemplate.name', ('device_type',)) + + class InterfaceTemplate(models.Model): """ A template for a physical data interface on a new Device. @@ -422,6 +471,8 @@ class InterfaceTemplate(models.Model): form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS) mgmt_only = models.BooleanField(default=False, verbose_name='Management only') + objects = InterfaceTemplateManager() + class Meta: ordering = ['device_type', 'name'] unique_together = ['device_type', 'name'] @@ -713,18 +764,8 @@ class PowerOutlet(models.Model): class InterfaceManager(models.Manager): def get_queryset(self): - """ - Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that - interfaces are ordered numerically without regard to type. For example: - xe-0/0/0, xe-0/0/1, xe-0/0/2 ... et-0/0/47, et-0/0/48, et-0/0/49 ... - instead of: - et-0/0/48, et-0/0/49, et-0/0/50 ... et-0/0/53, xe-0/0/0, xe-0/0/1 ... - """ - return super(InterfaceManager, self).get_queryset().extra(select={ - '_id1': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)\/([0-9]+)$') AS integer)", - '_id2': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)$') AS integer)", - '_id3': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)$') AS integer)", - }).order_by('device', '_id1', '_id2', '_id3') + qs = super(InterfaceManager, self).get_queryset() + return order_interfaces(qs, 'dcim_interface.name', ('device',)) def virtual(self): return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL) From 4ed3d545669082066b7eea890bae07cc6ab3613e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Jun 2016 09:45:59 -0400 Subject: [PATCH 11/11] Fixes #103: Corrected VRF filters for Prefixes and IPAddresses --- netbox/ipam/filters.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 8aab11475..56e2cbdd2 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -46,9 +46,14 @@ class PrefixFilter(django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) + vrf = django_filters.MethodFilter( + action='_vrf', + label='VRF', + ) + # Duplicate of `vrf` for backward-compatibility vrf_id = django_filters.MethodFilter( - action='vrf', - label='VRF (ID)', + action='_vrf', + label='VRF', ) site_id = django_filters.ModelMultipleChoiceFilter( name='site', @@ -84,7 +89,7 @@ class PrefixFilter(django_filters.FilterSet): class Meta: model = Prefix - fields = ['family', 'site_id', 'site', 'vrf_id', 'vrf', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role'] + fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role'] def search(self, queryset, value): value = value.strip() @@ -104,7 +109,7 @@ class PrefixFilter(django_filters.FilterSet): except AddrFormatError: return queryset.none() - def vrf(self, queryset, value): + def _vrf(self, queryset, value): if str(value) == '': return queryset try: @@ -121,10 +126,14 @@ class IPAddressFilter(django_filters.FilterSet): action='search', label='Search', ) - vrf_id = django_filters.ModelMultipleChoiceFilter( - name='vrf', - queryset=VRF.objects.all(), - label='VRF (ID)', + vrf = django_filters.MethodFilter( + action='_vrf', + label='VRF', + ) + # Duplicate of `vrf` for backward-compatibility + vrf_id = django_filters.MethodFilter( + action='_vrf', + label='VRF', ) device_id = django_filters.ModelMultipleChoiceFilter( name='interface__device', @@ -155,6 +164,17 @@ class IPAddressFilter(django_filters.FilterSet): except AddrFormatError: return queryset.none() + def _vrf(self, queryset, value): + if str(value) == '': + return queryset + try: + vrf_id = int(value) + except ValueError: + return queryset.none() + if vrf_id == 0: + return queryset.filter(vrf__isnull=True) + return queryset.filter(vrf__pk=value) + class VLANFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter(
{{ tm }} {{ tm }} {{ tm.description }}