Просмотр исходного кода

feat(filtersets): Add `assigned` and `primary` filters for MACAddress (#20620)

Introduce Boolean filters `assigned` and `primary` to the MACAddress
filterset, improving filtering capabilities. Update forms, tables, and
GraphQL queries to incorporate the new filters. Add tests to validate
the correct functionality.

Fixes #20399
Martin Hauser 3 месяцев назад
Родитель
Сommit
bbb330becf

+ 34 - 3
netbox/dcim/filtersets.py

@@ -14,16 +14,16 @@ from netbox.filtersets import (
     AttributeFiltersMixin, BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
     OrganizationalModelFilterSet,
 )
-from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
+from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from tenancy.models import *
 from users.models import User
 from utilities.filters import (
     ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
     NumericArrayFilter, TreeNodeMultipleChoiceFilter,
 )
-from virtualization.models import Cluster, ClusterGroup, VMInterface, VirtualMachine
+from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
 from vpn.models import L2VPN
-from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
+from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
 from wireless.models import WirelessLAN, WirelessLink
 from .choices import *
 from .constants import *
@@ -1807,6 +1807,14 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
         queryset=VMInterface.objects.all(),
         label=_('VM interface (ID)'),
     )
+    assigned = django_filters.BooleanFilter(
+        method='filter_assigned',
+        label=_('Is assigned'),
+    )
+    primary = django_filters.BooleanFilter(
+        method='filter_primary',
+        label=_('Is primary'),
+    )
 
     class Meta:
         model = MACAddress
@@ -1843,6 +1851,29 @@ class MACAddressFilterSet(NetBoxModelFilterSet):
             vminterface__in=interface_ids
         )
 
+    def filter_assigned(self, queryset, name, value):
+        params = {
+            'assigned_object_type__isnull': True,
+            'assigned_object_id__isnull': True,
+        }
+        if value:
+            return queryset.exclude(**params)
+        else:
+            return queryset.filter(**params)
+
+    def filter_primary(self, queryset, name, value):
+        interface_mac_ids = Interface.objects.filter(primary_mac_address_id__isnull=False).values_list(
+            'primary_mac_address_id', flat=True
+        )
+        vminterface_mac_ids = VMInterface.objects.filter(primary_mac_address_id__isnull=False).values_list(
+            'primary_mac_address_id', flat=True
+        )
+        query = Q(pk__in=interface_mac_ids) | Q(pk__in=vminterface_mac_ids)
+        if value:
+            return queryset.filter(query)
+        else:
+            return queryset.exclude(query)
+
 
 class CommonInterfaceFilterSet(django_filters.FilterSet):
     mode = django_filters.MultipleChoiceFilter(

+ 20 - 2
netbox/dcim/forms/filtersets.py

@@ -1676,12 +1676,16 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
     model = MACAddress
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('mac_address', 'device_id', 'virtual_machine_id', name=_('MAC address')),
+        FieldSet('mac_address', name=_('Attributes')),
+        FieldSet(
+            'device_id', 'virtual_machine_id', 'assigned', 'primary',
+            name=_('Assignments'),
+        ),
     )
     selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id')
     mac_address = forms.CharField(
         required=False,
-        label=_('MAC address')
+        label=_('MAC address'),
     )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -1693,6 +1697,20 @@ class MACAddressFilterForm(NetBoxModelFilterSetForm):
         required=False,
         label=_('Assigned VM'),
     )
+    assigned = forms.NullBooleanField(
+        required=False,
+        label=_('Assigned to an interface'),
+        widget=forms.Select(
+            choices=BOOLEAN_WITH_BLANK_CHOICES
+        ),
+    )
+    primary = forms.NullBooleanField(
+        required=False,
+        label=_('Primary MAC of an interface'),
+        widget=forms.Select(
+            choices=BOOLEAN_WITH_BLANK_CHOICES
+        ),
+    )
     tag = TagFilterField(model)
 
 

+ 21 - 1
netbox/dcim/graphql/filters.py

@@ -18,7 +18,9 @@ from netbox.graphql.filter_mixins import (
     ImageAttachmentFilterMixin,
     WeightFilterMixin,
 )
-from tenancy.graphql.filter_mixins import TenancyFilterMixin, ContactFilterMixin
+from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
+from virtualization.models import VMInterface
+
 from .filter_mixins import (
     CabledObjectModelFilterMixin,
     ComponentModelFilterMixin,
@@ -419,6 +421,24 @@ class MACAddressFilter(PrimaryModelFilterMixin):
     )
     assigned_object_id: ID | None = strawberry_django.filter_field()
 
+    @strawberry_django.filter_field()
+    def assigned(self, value: bool, prefix) -> Q:
+        return Q(**{f'{prefix}assigned_object_id__isnull': (not value)})
+
+    @strawberry_django.filter_field()
+    def primary(self, value: bool, prefix) -> Q:
+        interface_mac_ids = models.Interface.objects.filter(primary_mac_address_id__isnull=False).values_list(
+            'primary_mac_address_id', flat=True
+        )
+        vminterface_mac_ids = VMInterface.objects.filter(primary_mac_address_id__isnull=False).values_list(
+            'primary_mac_address_id', flat=True
+        )
+        query = Q(**{f'{prefix}pk__in': interface_mac_ids}) | Q(**{f'{prefix}pk__in': vminterface_mac_ids})
+        if value:
+            return Q(query)
+        else:
+            return ~Q(query)
+
 
 @strawberry_django.filter_type(models.Interface, lookups=True)
 class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):

+ 5 - 2
netbox/dcim/tables/devices.py

@@ -1174,6 +1174,9 @@ class MACAddressTable(NetBoxTable):
         orderable=False,
         verbose_name=_('Parent')
     )
+    is_primary = columns.BooleanColumn(
+        verbose_name=_('Primary')
+    )
     tags = columns.TagColumn(
         url_name='dcim:macaddress_list'
     )
@@ -1184,7 +1187,7 @@ class MACAddressTable(NetBoxTable):
     class Meta(DeviceComponentTable.Meta):
         model = models.MACAddress
         fields = (
-            'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'comments', 'tags',
-            'created', 'last_updated',
+            'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary',
+            'comments', 'tags', 'created', 'last_updated',
         )
         default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description')

+ 24 - 1
netbox/dcim/tests/test_filtersets.py

@@ -10,7 +10,7 @@ from netbox.choices import ColorChoices, WeightUnitChoices
 from tenancy.models import Tenant, TenantGroup
 from users.models import User
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
-from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
+from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
 from wireless.models import WirelessLink
 
@@ -7164,9 +7164,20 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
             MACAddress(mac_address='00-00-00-05-01-01', assigned_object=vm_interfaces[1]),
             MACAddress(mac_address='00-00-00-06-01-01', assigned_object=vm_interfaces[2]),
             MACAddress(mac_address='00-00-00-06-01-02', assigned_object=vm_interfaces[2]),
+            # unassigned
+            MACAddress(mac_address='00-00-00-07-01-01'),
         )
         MACAddress.objects.bulk_create(mac_addresses)
 
+        # Set MAC addresses as primary
+        for idx, interface in enumerate(interfaces):
+            interface.primary_mac_address = mac_addresses[idx]
+            interface.save()
+        for idx, vm_interface in enumerate(vm_interfaces):
+            # Offset by 4 for device MACs
+            vm_interface.primary_mac_address = mac_addresses[idx + 4]
+            vm_interface.save()
+
     def test_mac_address(self):
         params = {'mac_address': ['00-00-00-01-01-01', '00-00-00-02-01-01']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -7198,3 +7209,15 @@ class MACAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         params = {'vminterface': [vm_interfaces[0].name, vm_interfaces[1].name]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_assigned(self):
+        params = {'assigned': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+        params = {'assigned': False}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_primary(self):
+        params = {'primary': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+        params = {'primary': False}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)