From 433df098b37b8e4379b30cf8e46f66a27328ae63 Mon Sep 17 00:00:00 2001 From: "Etienne.BRUNEL" Date: Fri, 5 Dec 2025 23:41:54 +0100 Subject: [PATCH] Add tenant filter on device components. --- netbox/dcim/filtersets.py | 11 ++++ netbox/dcim/forms/filtersets.py | 33 ++++++---- netbox/dcim/tests/test_filtersets.py | 98 ++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index f8b3ced1c..9572de966 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1626,6 +1626,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet): choices=DeviceStatusChoices, field_name='device__status', ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__tenant', + queryset=Tenant.objects.all(), + label=_('Tenant (ID)'), + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='device__tenant__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label=_('Tenant (slug)'), + ) def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index decd7f737..638f566e0 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -10,6 +10,7 @@ from ipam.models import ASN, VRF, VLANTranslationPolicy from netbox.choices import * from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm +from tenancy.models import Tenant from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField @@ -120,6 +121,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): required=False, label=_('Device role') ) + tenant_id = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False, + label=_('Tenant') + ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), required=False, @@ -128,7 +134,8 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): 'location_id': '$location_id', 'virtual_chassis_id': '$virtual_chassis_id', 'device_type_id': '$device_type_id', - 'role_id': '$role_id' + 'role_id': '$role_id', + 'tenant_id': '$tenant_id' }, label=_('Device') ) @@ -1317,7 +1324,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), ) @@ -1341,7 +1349,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), @@ -1366,7 +1374,8 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'type', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), ) @@ -1385,7 +1394,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), @@ -1418,7 +1427,8 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'vdc_id', name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), @@ -1539,7 +1549,8 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + name=_('Device') ), FieldSet('cabled', 'occupied', name=_('Cable')), ) @@ -1563,7 +1574,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), FieldSet('cabled', 'occupied', name=_('Cable')), @@ -1587,7 +1598,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm): FieldSet('name', 'label', 'position', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), ) @@ -1605,7 +1616,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm): FieldSet('name', 'label', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), ) @@ -1622,7 +1633,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): ), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( - 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', + 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 08682d635..33138066a 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -43,6 +43,13 @@ class DeviceComponentFilterSetTests: params = {'device_status': ['active', 'planned']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tenant(self): + tenants = Tenant.objects.all()[:2] + params = {'tenant_id': [tenants[0].pk, tenants[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'tenant': [tenants[0].slug, tenants[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class DeviceComponentTemplateFilterSetTests: @@ -3377,9 +3384,17 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -3389,6 +3404,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -3398,6 +3414,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -3617,9 +3634,17 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -3629,6 +3654,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -3638,6 +3664,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -3857,9 +3884,17 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -3869,6 +3904,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -3878,6 +3914,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -4111,9 +4148,17 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -4123,6 +4168,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -4132,6 +4178,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -4390,9 +4437,17 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil virtual_chassis = VirtualChassis(name='Virtual Chassis') virtual_chassis.save() + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1A', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -4405,6 +4460,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 1B', + tenant=tenants[1], device_type=device_types[2], role=roles[2], site=sites[2], @@ -4417,6 +4473,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 2', + tenant=tenants[2], device_type=device_types[1], role=roles[1], site=sites[1], @@ -4426,6 +4483,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -5011,9 +5069,17 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -5023,6 +5089,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -5032,6 +5099,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -5302,9 +5370,17 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -5314,6 +5390,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -5323,6 +5400,7 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -5579,9 +5657,17 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -5591,6 +5677,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -5600,6 +5687,7 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2], @@ -5752,9 +5840,17 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Rack.objects.bulk_create(racks) + tenants = ( + Tenant(name='Tenant 1', slug='tenant-1'), + Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), + ) + Tenant.objects.bulk_create(tenants) + devices = ( Device( name='Device 1', + tenant=tenants[0], device_type=device_types[0], role=roles[0], site=sites[0], @@ -5764,6 +5860,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 2', + tenant=tenants[1], device_type=device_types[1], role=roles[1], site=sites[1], @@ -5773,6 +5870,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ), Device( name='Device 3', + tenant=tenants[2], device_type=device_types[2], role=roles[2], site=sites[2],