From 5b3ef045506100c6fdf6ae9a15d7f717a1fd0f15 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 2 Aug 2022 12:38:16 -0500 Subject: [PATCH 1/2] #9888 - Add filter and columns for device and site --- netbox/ipam/filtersets.py | 83 +++++++++++++++++++++++++++------ netbox/ipam/forms/filtersets.py | 44 ++++++++++++++++- netbox/ipam/models/l2vpn.py | 15 ++++++ netbox/ipam/tables/l2vpn.py | 11 ++++- 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index edd1867ed..132094325 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -980,21 +980,65 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): to_field_name='slug', label='L2VPN (slug)', ) - device = MultiValueCharFilter( - method='filter_device', - field_name='name', - label='Device (name)', + region = MultiValueCharFilter( + method='filter_region', + field_name='slug', + label='Region (slug)', ) - device_id = MultiValueNumberFilter( - method='filter_device', + region_id = MultiValueNumberFilter( + method='filter_region', + field_name='pk', + label='Region (ID)', + ) + site = MultiValueCharFilter( + method='filter_site', + field_name='slug', + label='Device (slug)', + ) + site_id = MultiValueNumberFilter( + method='filter_site', field_name='pk', label='Device (ID)', ) + device = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device__name', + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) + device_id = django_filters.ModelMultipleChoiceFilter( + field_name='interface__device', + queryset=Device.objects.all(), + label='Device (ID)', + ) + virtual_machine = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__virtual_machine__name', + queryset=VirtualMachine.objects.all(), + to_field_name='name', + label='Virtual machine (name)', + ) + virtual_machine_id = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__virtual_machine', + queryset=VirtualMachine.objects.all(), + label='Virtual machine (ID)', + ) + interface = django_filters.ModelMultipleChoiceFilter( + field_name='interface__name', + queryset=Interface.objects.all(), + to_field_name='name', + label='Interface (name)', + ) interface_id = django_filters.ModelMultipleChoiceFilter( field_name='interface', queryset=Interface.objects.all(), label='Interface (ID)', ) + vminterface = django_filters.ModelMultipleChoiceFilter( + field_name='vminterface__name', + queryset=VMInterface.objects.all(), + to_field_name='name', + label='VM interface (name)', + ) vminterface_id = django_filters.ModelMultipleChoiceFilter( field_name='vminterface', queryset=VMInterface.objects.all(), @@ -1027,13 +1071,22 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): qs_filter = Q(l2vpn__name__icontains=value) return queryset.filter(qs_filter) - def filter_device(self, queryset, name, value): - devices = Device.objects.filter(**{'{}__in'.format(name): value}) - if not devices.exists(): - return queryset.none() - interface_ids = [] - for device in devices: - interface_ids.extend(device.vc_interfaces().values_list('id', flat=True)) - return queryset.filter( - interface__in=interface_ids + def filter_site(self, queryset, name, value): + qs = queryset.filter( + Q( + Q(**{'vlan__site__{}__in'.format(name): value}) | + Q(**{'interface__device__site__{}__in'.format(name): value}) | + Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value}) + ) ) + return qs + + def filter_region(self, queryset, name, value): + qs = queryset.filter( + Q( + Q(**{'vlan__site__region__{}__in'.format(name): value}) | + Q(**{'interface__device__site__region__{}__in'.format(name): value}) | + Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value}) + ) + ) + return qs diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 384a4da33..d93bd16d0 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -508,7 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - (None, ('l2vpn_id', 'assigned_object_type_id')), + (None, ('l2vpn_id', 'assigned_object_type_id', )), + ('Assigned Object', ('region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), @@ -520,3 +521,44 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): required=False, label='Object type' ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Region') + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + null_option='None', + query_params={ + 'region_id': '$region_id' + }, + label=_('Site') + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Device') + ) + vlan_id = DynamicModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('VLAN') + ) + virtual_machine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + null_option='None', + query_params={ + 'site_id': '$site_id' + }, + label=_('Virtual Machine') + ) diff --git a/netbox/ipam/models/l2vpn.py b/netbox/ipam/models/l2vpn.py index 5d85fe915..5adf5e05d 100644 --- a/netbox/ipam/models/l2vpn.py +++ b/netbox/ipam/models/l2vpn.py @@ -113,3 +113,18 @@ class L2VPNTermination(NetBoxModel): f'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already ' f'defined.' ) + + @property + def assigned_object_parent(self): + obj_type = ContentType.objects.get_for_model(self.assigned_object) + if obj_type.model == 'vminterface': + return self.assigned_object.virtual_machine + elif obj_type.model == 'interface': + return self.assigned_object.device + elif obj_type.model == 'vminterface': + return self.assigned_object.virtual_machine + return None + + @property + def assigned_object_site(self): + return self.assigned_object_parent.site diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/ipam/tables/l2vpn.py index 5be525343..e2eae7a32 100644 --- a/netbox/ipam/tables/l2vpn.py +++ b/netbox/ipam/tables/l2vpn.py @@ -53,8 +53,17 @@ class L2VPNTerminationTable(NetBoxTable): linkify=True, orderable=False ) + assigned_object_parent = tables.Column( + linkify=True, + orderable=False + ) + assigned_object_site = tables.Column( + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = L2VPNTermination - fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions') + fields = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent', + 'assigned_object_site', 'actions') default_columns = ('pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'actions') From 37c4f1a7d3509f137a5a81d1b570d10a7533ccae Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 2 Aug 2022 13:38:17 -0500 Subject: [PATCH 2/2] Fix up a few minor mistakes. Add tests. --- netbox/ipam/filtersets.py | 4 ++-- netbox/ipam/forms/filtersets.py | 11 ++++++----- netbox/ipam/tests/test_filtersets.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 132094325..49ec15fc1 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -993,12 +993,12 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet): site = MultiValueCharFilter( method='filter_site', field_name='slug', - label='Device (slug)', + label='Site (slug)', ) site_id = MultiValueNumberFilter( method='filter_site', field_name='pk', - label='Device (ID)', + label='Site (ID)', ) device = django_filters.ModelMultipleChoiceFilter( field_name='interface__device__name', diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index d93bd16d0..ecf63b49f 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, + MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, APISelectMultiple, ) from virtualization.models import VirtualMachine @@ -508,8 +508,8 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): model = L2VPNTermination fieldsets = ( - (None, ('l2vpn_id', 'assigned_object_type_id', )), - ('Assigned Object', ('region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), + (None, ('l2vpn_id', )), + ('Assigned Object', ('assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id')), ) l2vpn_id = DynamicModelChoiceField( queryset=L2VPN.objects.all(), @@ -517,9 +517,10 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm): label='L2VPN' ) assigned_object_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS), required=False, - label='Object type' + label=_('Assigned Object Type'), + limit_choices_to=L2VPN_ASSIGNMENT_MODELS ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 9106a4965..081f6e11d 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1600,3 +1600,24 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'vlan': ['VLAN 1', 'VLAN 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_site(self): + site = Site.objects.all().first() + params = {'site_id': [site.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'site': ['site-1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_device(self): + device = Device.objects.all().first() + params = {'device_id': [device.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'device': ['Device 1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_virtual_machine(self): + virtual_machine = VirtualMachine.objects.all().first() + params = {'virtual_machine_id': [virtual_machine.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'virtual_machine': ['Virtual Machine 1']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)