From e97708ada03709dc9ccf12f8c3a2c8528cfba80b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 22 Oct 2018 11:23:37 -0400 Subject: [PATCH 01/17] Fixes #2526: Bump paramiko and pycryptodome requirements due to vulnerability --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b3bee6b6d..d3fc5a561 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,10 +14,10 @@ Markdown==2.6.11 natsort==5.3.3 ncclient==0.6.0 netaddr==0.7.19 -paramiko==2.4.1 +paramiko==2.4.2 Pillow==5.2.0 psycopg2-binary==2.7.5 py-gfm==0.1.3 -pycryptodome==3.6.4 +pycryptodome==3.6.6 xmltodict==0.11.0 From 0c86fd89caa073b7c2e2dd8b87ac4570b7b27b8d Mon Sep 17 00:00:00 2001 From: knobix <43905002+knobix@users.noreply.github.com> Date: Mon, 5 Nov 2018 21:33:10 +0100 Subject: [PATCH 02/17] Update models.py (#2502) Fix the handling of shared IPs (VIP, VRRF, etc.) when unique IP space enforcement is set. Add parentheses for the logical OR-statement to make the evaluation valid. Fixes: #2501 --- netbox/ipam/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index f9170cd58..ef3bc6c30 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -596,11 +596,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): if self.address: # Enforce unique IP space (if applicable) - if self.role not in IPADDRESS_ROLES_NONUNIQUE and ( + if self.role not in IPADDRESS_ROLES_NONUNIQUE and (( self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE ) or ( self.vrf and self.vrf.enforce_unique - ): + )): duplicate_ips = self.get_duplicates() if duplicate_ips: raise ValidationError({ From f321e2c705dd75e39e2a3fcba580825c3390501c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Nov 2018 15:34:39 -0500 Subject: [PATCH 03/17] Changelog for #2501 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb2749cb..b7507840f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.4.7 (FUTURE) ## Bug Fixes +* [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled From 82b4aad585f8f3cb0c2a7f720ead8330a3a50629 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 5 Nov 2018 14:37:52 -0600 Subject: [PATCH 04/17] Fixes 2427: Added filtering interfaces by vlan id(vlan=#) and vlan pk(vlan_id=#) (#2521) --- netbox/dcim/filters.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c81af4478..efc71e5b4 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -635,6 +635,14 @@ class InterfaceFilter(django_filters.FilterSet): tag = django_filters.CharFilter( name='tags__slug', ) + vlan_id = django_filters.CharFilter( + method='filter_vlan_by_pk', + name='vlan_pk', + ) + vlan = django_filters.CharFilter( + method='filter_vlan_by_id', + name='vid', + ) class Meta: model = Interface @@ -649,6 +657,12 @@ class InterfaceFilter(django_filters.FilterSet): except Device.DoesNotExist: return queryset.none() + def filter_vlan_by_pk(self, queryset, name, value): + return queryset.filter(Q(untagged_vlan_id=value) | Q(tagged_vlans=value)) + + def filter_vlan_by_id(self, queryset, name, value): + return queryset.filter(Q(untagged_vlan_id__vid=value) | Q(tagged_vlans__vid=value)) + def filter_type(self, queryset, name, value): value = value.strip().lower() return { From ce7930abfd2718a50ebb5e74d19814e2229257b1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Nov 2018 15:40:48 -0500 Subject: [PATCH 05/17] Changelog for #2427 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7507840f..1436875e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.4.7 (FUTURE) ## Bug Fixes +* [#2427](https://github.com/digitalocean/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID * [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled From ded90df01b482490f88119ced1cc5f7d058175a3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Nov 2018 15:45:21 -0500 Subject: [PATCH 06/17] Filter cleanup --- netbox/dcim/filters.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index efc71e5b4..424e99b59 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -636,12 +636,12 @@ class InterfaceFilter(django_filters.FilterSet): name='tags__slug', ) vlan_id = django_filters.CharFilter( - method='filter_vlan_by_pk', - name='vlan_pk', + method='filter_vlan_id', + label='Assigned VLAN' ) vlan = django_filters.CharFilter( - method='filter_vlan_by_id', - name='vid', + method='filter_vlan', + label='Assigned VID' ) class Meta: @@ -657,11 +657,23 @@ class InterfaceFilter(django_filters.FilterSet): except Device.DoesNotExist: return queryset.none() - def filter_vlan_by_pk(self, queryset, name, value): - return queryset.filter(Q(untagged_vlan_id=value) | Q(tagged_vlans=value)) + def filter_vlan_id(self, queryset, name, value): + value = value.strip() + if not value: + return queryset + return queryset.filter( + Q(untagged_vlan_id=value) | + Q(tagged_vlans=value) + ) - def filter_vlan_by_id(self, queryset, name, value): - return queryset.filter(Q(untagged_vlan_id__vid=value) | Q(tagged_vlans__vid=value)) + def filter_vlan(self, queryset, name, value): + value = value.strip() + if not value: + return queryset + return queryset.filter( + Q(untagged_vlan_id__vid=value) | + Q(tagged_vlans__vid=value) + ) def filter_type(self, queryset, name, value): value = value.strip().lower() From bd3ccfe020ab9bfa96067fdf6b298e7717995e33 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Nov 2018 16:10:01 -0500 Subject: [PATCH 07/17] Fixes #2528: Enable creating circuit terminations with interface assignment via API --- CHANGELOG.md | 1 + netbox/circuits/api/serializers.py | 6 +-- netbox/circuits/tests/test_api.py | 62 ++++++++++++++++++++++-------- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1436875e5..e24dec5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v2.4.7 (FUTURE) * [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled +* [#2528](https://github.com/digitalocean/netbox/issues/2528) - --- diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 739fbf8ff..c19ab2fce 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -4,8 +4,8 @@ from rest_framework import serializers from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from circuits.constants import CIRCUIT_STATUS_CHOICES -from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer +from circuits.models import Circuit, CircuitTermination, CircuitType, Provider +from dcim.api.serializers import NestedInterfaceSerializer, NestedSiteSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.serializers import NestedTenantSerializer from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer @@ -87,7 +87,7 @@ class NestedCircuitSerializer(WritableNestedSerializer): class CircuitTerminationSerializer(ValidatedModelSerializer): circuit = NestedCircuitSerializer() site = NestedSiteSerializer() - interface = InterfaceSerializer(required=False, allow_null=True) + interface = NestedInterfaceSerializer(required=False, allow_null=True) class Meta: model = CircuitTermination diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index e6c98068f..bcaf2dee4 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -5,7 +5,7 @@ from rest_framework import status from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.models import Site +from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site from extras.constants import GRAPH_TYPE_PROVIDER from extras.models import Graph from utilities.testing import APITestCase @@ -330,21 +330,44 @@ class CircuitTerminationTest(APITestCase): super(CircuitTerminationTest, self).setUp() + self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') + self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2') + manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') + devicetype = DeviceType.objects.create( + manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True + ) + devicerole = DeviceRole.objects.create( + name='Test Device Role 1', slug='test-device-role-1', color='ff0000' + ) + device1 = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site1 + ) + device2 = Device.objects.create( + device_type=devicetype, device_role=devicerole, name='Test Device 2', site=self.site2 + ) + self.interface1 = Interface.objects.create(device=device1, name='Test Interface 1') + self.interface2 = Interface.objects.create(device=device2, name='Test Interface 2') + self.interface3 = Interface.objects.create(device=device1, name='Test Interface 3') + self.interface4 = Interface.objects.create(device=device2, name='Test Interface 4') + self.interface5 = Interface.objects.create(device=device1, name='Test Interface 5') + self.interface6 = Interface.objects.create(device=device2, name='Test Interface 6') + provider = Provider.objects.create(name='Test Provider', slug='test-provider') circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type') self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype) self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype) self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype) - self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') - self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2') self.circuittermination1 = CircuitTermination.objects.create( - circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface1, port_speed=1000000 ) self.circuittermination2 = CircuitTermination.objects.create( - circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface2, port_speed=1000000 ) self.circuittermination3 = CircuitTermination.objects.create( - circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface3, port_speed=1000000 + ) + self.circuittermination4 = CircuitTermination.objects.create( + circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, interface=self.interface4, port_speed=1000000 ) def test_get_circuittermination(self): @@ -359,14 +382,15 @@ class CircuitTerminationTest(APITestCase): url = reverse('circuits-api:circuittermination-list') response = self.client.get(url, **self.header) - self.assertEqual(response.data['count'], 3) + self.assertEqual(response.data['count'], 4) def test_create_circuittermination(self): data = { - 'circuit': self.circuit1.pk, - 'term_side': TERM_SIDE_Z, - 'site': self.site2.pk, + 'circuit': self.circuit3.pk, + 'term_side': TERM_SIDE_A, + 'site': self.site1.pk, + 'interface': self.interface5.pk, 'port_speed': 1000000, } @@ -374,31 +398,37 @@ class CircuitTerminationTest(APITestCase): response = self.client.post(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(CircuitTermination.objects.count(), 4) + self.assertEqual(CircuitTermination.objects.count(), 5) circuittermination4 = CircuitTermination.objects.get(pk=response.data['id']) self.assertEqual(circuittermination4.circuit_id, data['circuit']) self.assertEqual(circuittermination4.term_side, data['term_side']) self.assertEqual(circuittermination4.site_id, data['site']) + self.assertEqual(circuittermination4.interface_id, data['interface']) self.assertEqual(circuittermination4.port_speed, data['port_speed']) def test_update_circuittermination(self): + circuittermination5 = CircuitTermination.objects.create( + circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, interface=self.interface5, port_speed=1000000 + ) + data = { - 'circuit': self.circuit1.pk, + 'circuit': self.circuit3.pk, 'term_side': TERM_SIDE_Z, 'site': self.site2.pk, + 'interface': self.interface6.pk, 'port_speed': 1000000, } - url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk}) + url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': circuittermination5.pk}) response = self.client.put(url, data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(CircuitTermination.objects.count(), 3) + self.assertEqual(CircuitTermination.objects.count(), 5) circuittermination1 = CircuitTermination.objects.get(pk=response.data['id']) - self.assertEqual(circuittermination1.circuit_id, data['circuit']) self.assertEqual(circuittermination1.term_side, data['term_side']) self.assertEqual(circuittermination1.site_id, data['site']) + self.assertEqual(circuittermination1.interface_id, data['interface']) self.assertEqual(circuittermination1.port_speed, data['port_speed']) def test_delete_circuittermination(self): @@ -407,4 +437,4 @@ class CircuitTerminationTest(APITestCase): response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) - self.assertEqual(CircuitTermination.objects.count(), 2) + self.assertEqual(CircuitTermination.objects.count(), 3) From 4d47d848c59e6be1c8b62358d4d6b56dc9201b58 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 5 Nov 2018 16:10:33 -0500 Subject: [PATCH 08/17] Fixed changelog for #2528 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e24dec5b2..54d97ae24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ v2.4.7 (FUTURE) * [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled -* [#2528](https://github.com/digitalocean/netbox/issues/2528) - +* [#2528](https://github.com/digitalocean/netbox/issues/2528) - Enable creating circuit terminations with interface assignment via API --- From 798a87b31e4e3615298da4243e0f749c3a6ae398 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 6 Nov 2018 00:51:55 -0500 Subject: [PATCH 09/17] fixed #2549 - incorrect naming of peer-device and peer-interface --- netbox/dcim/api/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index ceec6747d..a7f018b00 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -412,13 +412,13 @@ class ConnectedDeviceViewSet(ViewSet): interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors via a protocol such as LLDP. Two query parameters must be included in the request: - * `peer-device`: The name of the peer device - * `peer-interface`: The name of the peer interface + * `peer_device`: The name of the peer device + * `peer_interface`: The name of the peer interface """ permission_classes = [IsAuthenticatedOrLoginNotRequired] - _device_param = Parameter('peer-device', 'query', + _device_param = Parameter('peer_device', 'query', description='The name of the peer device', required=True, type=openapi.TYPE_STRING) - _interface_param = Parameter('peer-interface', 'query', + _interface_param = Parameter('peer_interface', 'query', description='The name of the peer interface', required=True, type=openapi.TYPE_STRING) def get_view_name(self): @@ -431,7 +431,7 @@ class ConnectedDeviceViewSet(ViewSet): peer_device_name = request.query_params.get(self._device_param.name) peer_interface_name = request.query_params.get(self._interface_param.name) if not peer_device_name or not peer_interface_name: - raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.') + raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.') # Determine local interface from peer interface's connection peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name) From 817dc89279028441c66908be05e183865666a41b Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 6 Nov 2018 00:54:57 -0500 Subject: [PATCH 10/17] fixed test for #2549 --- netbox/dcim/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 04952a4d4..4c60e79d7 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -3053,7 +3053,7 @@ class ConnectedDeviceTest(APITestCase): def test_get_connected_device(self): url = reverse('dcim-api:connected-device-list') - response = self.client.get(url + '?peer-device=TestDevice2&peer-interface=eth0', **self.header) + response = self.client.get(url + '?peer_device=TestDevice2&peer_interface=eth0', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['name'], self.device1.name) From e243234c4e4268b8725ac98fbb11cf909640112e Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 6 Nov 2018 00:57:09 -0500 Subject: [PATCH 11/17] changelog for #2549 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d97ae24..f258406f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ v2.4.7 (FUTURE) * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled * [#2528](https://github.com/digitalocean/netbox/issues/2528) - Enable creating circuit terminations with interface assignment via API +* [#2549](https://github.com/digitalocean/netbox/issues/2549) - Changed naming of `peer_device` and `peer_interface` on API /dcim/connected-device/ endpoint to use underscores --- From fd4a9db13e27034d23ac07f014ec3f388ae258e4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Nov 2018 09:24:05 -0500 Subject: [PATCH 12/17] Closes #2512: Add device field to inventory item filter form --- CHANGELOG.md | 6 +++++- netbox/dcim/filters.py | 9 +++++++++ netbox/dcim/forms.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f258406f6..877a45a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ v2.4.7 (FUTURE) +## Enhancements + +* [#2427](https://github.com/digitalocean/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID +* [#2512](https://github.com/digitalocean/netbox/issues/2512) - Add device field to inventory item filter form + ## Bug Fixes -* [#2427](https://github.com/digitalocean/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID * [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF * [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces * [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 424e99b59..1870df762 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -707,6 +707,15 @@ class InventoryItemFilter(DeviceComponentFilterSet): method='search', label='Search', ) + device_id = django_filters.ModelChoiceFilter( + queryset=Device.objects.all(), + label='Device (ID)', + ) + device = django_filters.ModelChoiceFilter( + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) parent_id = django_filters.ModelMultipleChoiceFilter( queryset=InventoryItem.objects.all(), label='Parent inventory item (ID)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6b6ea1187..b24c979c5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2201,6 +2201,7 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): class InventoryItemFilterForm(BootstrapMixin, forms.Form): model = InventoryItem q = forms.CharField(required=False, label='Search') + device = forms.CharField(required=False, label='Device name') manufacturer = FilterChoiceField( queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')), to_field_name='slug', From 5baf86dc89b3f8e7ee35703c8cea693fb0f6e0b6 Mon Sep 17 00:00:00 2001 From: Ben Bleything Date: Tue, 6 Nov 2018 06:26:05 -0800 Subject: [PATCH 13/17] fix prefix length for 172.16.0.0/12 (#2548) --- docs/core-functionality/ipam.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md index 27bec8e8e..05b613da2 100644 --- a/docs/core-functionality/ipam.md +++ b/docs/core-functionality/ipam.md @@ -4,7 +4,7 @@ The first step to documenting your IP space is to define its scope by creating a * 10.0.0.0/8 (RFC 1918) * 100.64.0.0/10 (RFC 6598) -* 172.16.0.0/20 (RFC 1918) +* 172.16.0.0/12 (RFC 1918) * 192.168.0.0/16 (RFC 1918) * One or more /48s within fd00::/8 (IPv6 unique local addressing) From 51295389c65e05f3339d9e85a6650bc4f9eb3501 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 6 Nov 2018 10:08:00 -0500 Subject: [PATCH 14/17] add temporary support for hyphenated query params for #2549 --- netbox/dcim/api/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a7f018b00..2159661ef 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -429,7 +429,13 @@ class ConnectedDeviceViewSet(ViewSet): def list(self, request): peer_device_name = request.query_params.get(self._device_param.name) + if not peer_device_name: + # TODO: remove this after 2.4 as the switch to using underscores is a breaking change + peer_device_name = request.query_params.get('peer-device') peer_interface_name = request.query_params.get(self._interface_param.name) + if not peer_interface_name: + # TODO: remove this after 2.4 as the switch to using underscores is a breaking change + peer_interface_name = request.query_params.get('peer-interface') if not peer_device_name or not peer_interface_name: raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.') From b4998f4b01a32b56b59bc3245e985f5f5527a3a4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Nov 2018 10:31:56 -0500 Subject: [PATCH 15/17] Closes #2388: Enable filtering of devices/VMs by region --- CHANGELOG.md | 1 + netbox/dcim/filters.py | 21 +++++++++++++++++++++ netbox/dcim/forms.py | 5 +++++ netbox/virtualization/filters.py | 23 ++++++++++++++++++++++- netbox/virtualization/forms.py | 9 +++++++-- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 877a45a1e..7647a4152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.4.7 (FUTURE) ## Enhancements +* [#2388](https://github.com/digitalocean/netbox/issues/2388) - Enable filtering of devices/VMs by region * [#2427](https://github.com/digitalocean/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID * [#2512](https://github.com/digitalocean/netbox/issues/2512) - Add device field to inventory item filter form diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1870df762..689e88a5d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import django_filters from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -456,6 +457,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): ) name = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter() + region_id = django_filters.NumberFilter( + method='filter_region', + name='pk', + label='Region (ID)', + ) + region = django_filters.CharFilter( + method='filter_region', + name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -538,6 +549,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(comments__icontains=value) ).distinct() + def filter_region(self, queryset, name, value): + try: + region = Region.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + return queryset.filter( + Q(site__region=region) | + Q(site__region__in=region.get_descendants()) + ) + def _mac_address(self, queryset, name, value): value = value.strip() if not value: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b24c979c5..46e039211 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1108,6 +1108,11 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device q = forms.CharField(required=False, label='Search') + region = FilterTreeNodeMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('devices')), to_field_name='slug', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 6af4e4a22..99df19aee 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals import django_filters +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError -from dcim.models import DeviceRole, Interface, Platform, Site +from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NumericInFilter @@ -116,6 +117,16 @@ class VirtualMachineFilter(CustomFieldFilterSet): queryset=Cluster.objects.all(), label='Cluster (ID)', ) + region_id = django_filters.NumberFilter( + method='filter_region', + name='pk', + label='Region (ID)', + ) + region = django_filters.CharFilter( + method='filter_region', + name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( name='cluster__site', queryset=Site.objects.all(), @@ -173,6 +184,16 @@ class VirtualMachineFilter(CustomFieldFilterSet): Q(comments__icontains=value) ) + def filter_region(self, queryset, name, value): + try: + region = Region.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + return queryset.filter( + Q(cluster__site__region=region) | + Q(cluster__site__region__in=region.get_descendants()) + ) + class InterfaceFilter(django_filters.FilterSet): virtual_machine_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 8f973955c..6d11ed78a 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -16,8 +16,8 @@ from tenancy.models import Tenant from utilities.forms import ( AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, - add_blank_choice + ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, + JSONField, SlugField, SmallTextarea, add_blank_choice, ) from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -386,6 +386,11 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), label='Cluster' ) + region = FilterTreeNodeMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')), to_field_name='slug', From 99edb8b8d50d2d13a9672204e69603c29be643ad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Nov 2018 10:49:44 -0500 Subject: [PATCH 16/17] Release v2.4.7 --- CHANGELOG.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7647a4152..42bece984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v2.4.7 (FUTURE) +v2.4.7 (2018-11-06) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index c96503e4d..ee54ef346 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.7-dev' +VERSION = '2.4.7' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 8bad25b860db1c6815d5db73f3020d9380171c85 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Nov 2018 10:57:38 -0500 Subject: [PATCH 17/17] 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 ee54ef346..1b09989c7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.7' +VERSION = '2.4.8-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))