From 135381c1df28c6ad35c83eb43a6c1ff4831d1cc7 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 1f0fa7669e50f515a51042c251c50a29fa9dd04e 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 b00f211dff2f4f4b17a484fd8f579ea980295612 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 14b22606ae62753ccfa9cf3aa91fbca45f7b3fc2 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 4c140295c5c61d978cd588422f29559bc86bba70 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 4051046dba4ee94a11bad5774e9da96619c3bf27 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 153dfc40ca6d2056cb385c952367391b3b545be0 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 e7d84c83410a70a4515b5cdd76601a073089aef3 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 e292bd7d112a245e4b70e506f688899189117a6a 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 df4bb61c480c876e092ae6b5a82dc7ff70072060 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 4751e95602d1c4a85d5867551fce18fbcd1e33b2 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 }}