浏览代码

Closes #19977: Denormalize device relationships on component models (#19984)

* Closes #19977: Denormalize site, location, and rack for device components

* Set blank=True on denormalized ForeignKeys

* Populate denormalized field in test data

* Ignore private fields when constructing test GraphQL requests
Jeremy Stretch 6 月之前
父节点
当前提交
aa9ee0e5c6

+ 6 - 6
netbox/dcim/filtersets.py

@@ -1515,34 +1515,34 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         label=_('Site group (slug)'),
         label=_('Site group (slug)'),
     )
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__site',
+        field_name='_site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         label=_('Site (ID)'),
         label=_('Site (ID)'),
     )
     )
     site = django_filters.ModelMultipleChoiceFilter(
     site = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__site__slug',
+        field_name='_site__slug',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         label=_('Site name (slug)'),
         label=_('Site name (slug)'),
     )
     )
     location_id = django_filters.ModelMultipleChoiceFilter(
     location_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__location',
+        field_name='_location',
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
         label=_('Location (ID)'),
         label=_('Location (ID)'),
     )
     )
     location = django_filters.ModelMultipleChoiceFilter(
     location = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__location__slug',
+        field_name='_location__slug',
         queryset=Location.objects.all(),
         queryset=Location.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         label=_('Location (slug)'),
         label=_('Location (slug)'),
     )
     )
     rack_id = django_filters.ModelMultipleChoiceFilter(
     rack_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__rack',
+        field_name='_rack',
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
         label=_('Rack (ID)'),
         label=_('Rack (ID)'),
     )
     )
     rack = django_filters.ModelMultipleChoiceFilter(
     rack = django_filters.ModelMultipleChoiceFilter(
-        field_name='device__rack__name',
+        field_name='_rack__name',
         queryset=Rack.objects.all(),
         queryset=Rack.objects.all(),
         to_field_name='name',
         to_field_name='name',
         label=_('Rack (name)'),
         label=_('Rack (name)'),

+ 287 - 0
netbox/dcim/migrations/0209_device_component_denorm_site_location.py

@@ -0,0 +1,287 @@
+import django.db.models.deletion
+from django.db import migrations, models
+from django.db.models import OuterRef, Subquery
+
+
+def populate_denormalized_data(apps, schema_editor):
+    Device = apps.get_model('dcim', 'Device')
+    component_models = (
+        apps.get_model('dcim', 'ConsolePort'),
+        apps.get_model('dcim', 'ConsoleServerPort'),
+        apps.get_model('dcim', 'PowerPort'),
+        apps.get_model('dcim', 'PowerOutlet'),
+        apps.get_model('dcim', 'Interface'),
+        apps.get_model('dcim', 'FrontPort'),
+        apps.get_model('dcim', 'RearPort'),
+        apps.get_model('dcim', 'DeviceBay'),
+        apps.get_model('dcim', 'ModuleBay'),
+        apps.get_model('dcim', 'InventoryItem'),
+    )
+
+    for model in component_models:
+        subquery = Device.objects.filter(pk=OuterRef('device_id'))
+        model.objects.update(
+            _site=Subquery(subquery.values('site_id')[:1]),
+            _location=Subquery(subquery.values('location_id')[:1]),
+            _rack=Subquery(subquery.values('rack_id')[:1]),
+        )
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0208_devicerole_uniqueness'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='consoleport',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleport',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleport',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='consoleserverport',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='devicebay',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='devicebay',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='devicebay',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='modulebay',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='modulebay',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='modulebay',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='poweroutlet',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='powerport',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='powerport',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='powerport',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rearport',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name='+',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='rearport',
+            name='_rack',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
+            ),
+        ),
+        migrations.AddField(
+            model_name='rearport',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
+            ),
+        ),
+        migrations.RunPython(populate_denormalized_data),
+    ]

+ 31 - 0
netbox/dcim/models/device_components.py

@@ -65,6 +65,29 @@ class ComponentModel(NetBoxModel):
         blank=True
         blank=True
     )
     )
 
 
+    # Denormalized references replicated from the parent Device
+    _site = models.ForeignKey(
+        to='dcim.Site',
+        on_delete=models.SET_NULL,
+        related_name='+',
+        blank=True,
+        null=True,
+    )
+    _location = models.ForeignKey(
+        to='dcim.Location',
+        on_delete=models.SET_NULL,
+        related_name='+',
+        blank=True,
+        null=True,
+    )
+    _rack = models.ForeignKey(
+        to='dcim.Rack',
+        on_delete=models.SET_NULL,
+        related_name='+',
+        blank=True,
+        null=True,
+    )
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
         ordering = ('device', 'name')
         ordering = ('device', 'name')
@@ -100,6 +123,14 @@ class ComponentModel(NetBoxModel):
                 "device": _("Components cannot be moved to a different device.")
                 "device": _("Components cannot be moved to a different device.")
             })
             })
 
 
+    def save(self, *args, **kwargs):
+        # Save denormalized references
+        self._site = self.device.site
+        self._location = self.device.location
+        self._rack = self.device.rack
+
+        super().save(*args, **kwargs)
+
     @property
     @property
     def parent_object(self):
     def parent_object(self):
         return self.device
         return self.device

+ 31 - 2
netbox/dcim/signals.py

@@ -3,13 +3,28 @@ import logging
 from django.db.models.signals import post_save, post_delete, pre_delete
 from django.db.models.signals import post_save, post_delete, pre_delete
 from django.dispatch import receiver
 from django.dispatch import receiver
 
 
-from .choices import CableEndChoices, LinkStatusChoices
+from dcim.choices import CableEndChoices, LinkStatusChoices
 from .models import (
 from .models import (
-    Cable, CablePath, CableTermination, Device, FrontPort, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis,
+    Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
+    InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
+    VirtualChassis,
 )
 )
 from .models.cables import trace_paths
 from .models.cables import trace_paths
 from .utils import create_cablepath, rebuild_paths
 from .utils import create_cablepath, rebuild_paths
 
 
+COMPONENT_MODELS = (
+    ConsolePort,
+    ConsoleServerPort,
+    DeviceBay,
+    FrontPort,
+    Interface,
+    InventoryItem,
+    ModuleBay,
+    PowerOutlet,
+    PowerPort,
+    RearPort,
+)
+
 
 
 #
 #
 # Location/rack/device assignment
 # Location/rack/device assignment
@@ -39,6 +54,20 @@ def handle_rack_site_change(instance, created, **kwargs):
         Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
         Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
 
 
 
 
+@receiver(post_save, sender=Device)
+def handle_device_site_change(instance, created, **kwargs):
+    """
+    Update child components to update the parent Site, Location, and Rack when a Device is saved.
+    """
+    if not created:
+        for model in COMPONENT_MODELS:
+            model.objects.filter(device=instance).update(
+                _site=instance.site,
+                _location=instance.location,
+                _rack=instance.rack,
+            )
+
+
 #
 #
 # Virtual chassis
 # Virtual chassis
 #
 #

+ 188 - 17
netbox/dcim/tests/test_filtersets.py

@@ -3367,9 +3367,36 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
         ConsoleServerPort.objects.bulk_create(console_server_ports)
         ConsoleServerPort.objects.bulk_create(console_server_ports)
 
 
         console_ports = (
         console_ports = (
-            ConsolePort(device=devices[0], module=modules[0], name='Console Port 1', label='A', description='First'),
-            ConsolePort(device=devices[1], module=modules[1], name='Console Port 2', label='B', description='Second'),
-            ConsolePort(device=devices[2], module=modules[2], name='Console Port 3', label='C', description='Third'),
+            ConsolePort(
+                device=devices[0],
+                module=modules[0],
+                name='Console Port 1',
+                label='A',
+                description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
+            ),
+            ConsolePort(
+                device=devices[1],
+                module=modules[1],
+                name='Console Port 2',
+                label='B',
+                description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
+            ),
+            ConsolePort(
+                device=devices[2],
+                module=modules[2],
+                name='Console Port 3',
+                label='C',
+                description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
+            ),
         )
         )
         ConsolePort.objects.bulk_create(console_ports)
         ConsolePort.objects.bulk_create(console_ports)
 
 
@@ -3581,13 +3608,34 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
 
 
         console_server_ports = (
         console_server_ports = (
             ConsoleServerPort(
             ConsoleServerPort(
-                device=devices[0], module=modules[0], name='Console Server Port 1', label='A', description='First'
+                device=devices[0],
+                module=modules[0],
+                name='Console Server Port 1',
+                label='A',
+                description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             ConsoleServerPort(
             ConsoleServerPort(
-                device=devices[1], module=modules[1], name='Console Server Port 2', label='B', description='Second'
+                device=devices[1],
+                module=modules[1],
+                name='Console Server Port 2',
+                label='B',
+                description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             ConsoleServerPort(
             ConsoleServerPort(
-                device=devices[2], module=modules[2], name='Console Server Port 3', label='C', description='Third'
+                device=devices[2],
+                module=modules[2],
+                name='Console Server Port 3',
+                label='C',
+                description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
             ),
             ),
         )
         )
         ConsoleServerPort.objects.bulk_create(console_server_ports)
         ConsoleServerPort.objects.bulk_create(console_server_ports)
@@ -3807,6 +3855,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 maximum_draw=100,
                 maximum_draw=100,
                 allocated_draw=50,
                 allocated_draw=50,
                 description='First',
                 description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             PowerPort(
             PowerPort(
                 device=devices[1],
                 device=devices[1],
@@ -3816,6 +3867,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 maximum_draw=200,
                 maximum_draw=200,
                 allocated_draw=100,
                 allocated_draw=100,
                 description='Second',
                 description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             PowerPort(
             PowerPort(
                 device=devices[2],
                 device=devices[2],
@@ -3825,6 +3879,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 maximum_draw=300,
                 maximum_draw=300,
                 allocated_draw=150,
                 allocated_draw=150,
                 description='Third',
                 description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
             ),
             ),
         )
         )
         PowerPort.objects.bulk_create(power_ports)
         PowerPort.objects.bulk_create(power_ports)
@@ -4053,6 +4110,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 description='First',
                 description='First',
                 color='ff0000',
                 color='ff0000',
                 status=PowerOutletStatusChoices.STATUS_ENABLED,
                 status=PowerOutletStatusChoices.STATUS_ENABLED,
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             PowerOutlet(
             PowerOutlet(
                 device=devices[1],
                 device=devices[1],
@@ -4063,6 +4123,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 description='Second',
                 description='Second',
                 color='00ff00',
                 color='00ff00',
                 status=PowerOutletStatusChoices.STATUS_DISABLED,
                 status=PowerOutletStatusChoices.STATUS_DISABLED,
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             PowerOutlet(
             PowerOutlet(
                 device=devices[2],
                 device=devices[2],
@@ -4073,6 +4136,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 description='Third',
                 description='Third',
                 color='0000ff',
                 color='0000ff',
                 status=PowerOutletStatusChoices.STATUS_FAULTY,
                 status=PowerOutletStatusChoices.STATUS_FAULTY,
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
             ),
             ),
         )
         )
         PowerOutlet.objects.bulk_create(power_outlets)
         PowerOutlet.objects.bulk_create(power_outlets)
@@ -4381,13 +4447,19 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 poe_mode=InterfacePoEModeChoices.MODE_PSE,
                 poe_mode=InterfacePoEModeChoices.MODE_PSE,
                 poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
                 poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
                 vlan_translation_policy=vlan_translation_policies[0],
                 vlan_translation_policy=vlan_translation_policies[0],
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[1],
                 device=devices[1],
                 module=modules[1],
                 module=modules[1],
                 name='VC Chassis Interface',
                 name='VC Chassis Interface',
                 type=InterfaceTypeChoices.TYPE_1GE_SFP,
                 type=InterfaceTypeChoices.TYPE_1GE_SFP,
-                enabled=True
+                enabled=True,
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[2],
                 device=devices[2],
@@ -4406,6 +4478,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 poe_mode=InterfacePoEModeChoices.MODE_PD,
                 poe_mode=InterfacePoEModeChoices.MODE_PD,
                 poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
                 poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
                 vlan_translation_policy=vlan_translation_policies[0],
                 vlan_translation_policy=vlan_translation_policies[0],
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[3],
                 device=devices[3],
@@ -4424,6 +4499,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 poe_mode=InterfacePoEModeChoices.MODE_PSE,
                 poe_mode=InterfacePoEModeChoices.MODE_PSE,
                 poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
                 poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
                 vlan_translation_policy=vlan_translation_policies[1],
                 vlan_translation_policy=vlan_translation_policies[1],
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[4],
                 device=devices[4],
@@ -4440,6 +4518,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
                 qinq_svlan=vlans[0],
                 qinq_svlan=vlans[0],
                 vlan_translation_policy=vlan_translation_policies[1],
                 vlan_translation_policy=vlan_translation_policies[1],
+                _site=devices[4].site,
+                _location=devices[4].location,
+                _rack=devices[4].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[4],
                 device=devices[4],
@@ -4450,7 +4531,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 mgmt_only=True,
                 mgmt_only=True,
                 tx_power=40,
                 tx_power=40,
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
-                qinq_svlan=vlans[1]
+                qinq_svlan=vlans[1],
+                _site=devices[4].site,
+                _location=devices[4].location,
+                _rack=devices[4].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[4],
                 device=devices[4],
@@ -4461,7 +4545,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 mgmt_only=False,
                 mgmt_only=False,
                 tx_power=40,
                 tx_power=40,
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
                 mode=InterfaceModeChoices.MODE_Q_IN_Q,
-                qinq_svlan=vlans[2]
+                qinq_svlan=vlans[2],
+                _site=devices[4].site,
+                _location=devices[4].location,
+                _rack=devices[4].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[4],
                 device=devices[4],
@@ -4470,7 +4557,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 rf_role=WirelessRoleChoices.ROLE_AP,
                 rf_role=WirelessRoleChoices.ROLE_AP,
                 rf_channel=WirelessChannelChoices.CHANNEL_24G_1,
                 rf_channel=WirelessChannelChoices.CHANNEL_24G_1,
                 rf_channel_frequency=2412,
                 rf_channel_frequency=2412,
-                rf_channel_width=22
+                rf_channel_width=22,
+                _site=devices[4].site,
+                _location=devices[4].location,
+                _rack=devices[4].rack,
             ),
             ),
             Interface(
             Interface(
                 device=devices[4],
                 device=devices[4],
@@ -4479,7 +4569,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 rf_role=WirelessRoleChoices.ROLE_STATION,
                 rf_role=WirelessRoleChoices.ROLE_STATION,
                 rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
                 rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
                 rf_channel_frequency=5160,
                 rf_channel_frequency=5160,
-                rf_channel_width=20
+                rf_channel_width=20,
+                _site=devices[4].site,
+                _location=devices[4].location,
+                _rack=devices[4].rack,
             ),
             ),
         )
         )
         Interface.objects.bulk_create(interfaces)
         Interface.objects.bulk_create(interfaces)
@@ -4906,6 +4999,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 rear_port=rear_ports[0],
                 rear_port=rear_ports[0],
                 rear_port_position=1,
                 rear_port_position=1,
                 description='First',
                 description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             FrontPort(
             FrontPort(
                 device=devices[1],
                 device=devices[1],
@@ -4917,6 +5013,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 rear_port=rear_ports[1],
                 rear_port=rear_ports[1],
                 rear_port_position=2,
                 rear_port_position=2,
                 description='Second',
                 description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             FrontPort(
             FrontPort(
                 device=devices[2],
                 device=devices[2],
@@ -4928,6 +5027,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 rear_port=rear_ports[2],
                 rear_port=rear_ports[2],
                 rear_port_position=3,
                 rear_port_position=3,
                 description='Third',
                 description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
             ),
             ),
             FrontPort(
             FrontPort(
                 device=devices[3],
                 device=devices[3],
@@ -4936,6 +5038,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 type=PortTypeChoices.TYPE_FC,
                 type=PortTypeChoices.TYPE_FC,
                 rear_port=rear_ports[3],
                 rear_port=rear_ports[3],
                 rear_port_position=1,
                 rear_port_position=1,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
             ),
             ),
             FrontPort(
             FrontPort(
                 device=devices[3],
                 device=devices[3],
@@ -4944,6 +5049,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 type=PortTypeChoices.TYPE_FC,
                 type=PortTypeChoices.TYPE_FC,
                 rear_port=rear_ports[4],
                 rear_port=rear_ports[4],
                 rear_port_position=1,
                 rear_port_position=1,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
             ),
             ),
             FrontPort(
             FrontPort(
                 device=devices[3],
                 device=devices[3],
@@ -4952,6 +5060,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
                 type=PortTypeChoices.TYPE_FC,
                 type=PortTypeChoices.TYPE_FC,
                 rear_port=rear_ports[5],
                 rear_port=rear_ports[5],
                 rear_port_position=1,
                 rear_port_position=1,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
             ),
             ),
         )
         )
         FrontPort.objects.bulk_create(front_ports)
         FrontPort.objects.bulk_create(front_ports)
@@ -5168,6 +5279,9 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
                 color=ColorChoices.COLOR_RED,
                 color=ColorChoices.COLOR_RED,
                 positions=1,
                 positions=1,
                 description='First',
                 description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
             ),
             ),
             RearPort(
             RearPort(
                 device=devices[1],
                 device=devices[1],
@@ -5178,6 +5292,9 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
                 color=ColorChoices.COLOR_GREEN,
                 color=ColorChoices.COLOR_GREEN,
                 positions=2,
                 positions=2,
                 description='Second',
                 description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
             ),
             ),
             RearPort(
             RearPort(
                 device=devices[2],
                 device=devices[2],
@@ -5188,10 +5305,40 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
                 color=ColorChoices.COLOR_BLUE,
                 color=ColorChoices.COLOR_BLUE,
                 positions=3,
                 positions=3,
                 description='Third',
                 description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
+            ),
+            RearPort(
+                device=devices[3],
+                name='Rear Port 4',
+                label='D',
+                type=PortTypeChoices.TYPE_FC,
+                positions=4,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
+            ),
+            RearPort(
+                device=devices[3],
+                name='Rear Port 5',
+                label='E',
+                type=PortTypeChoices.TYPE_FC,
+                positions=5,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
+            ),
+            RearPort(
+                device=devices[3],
+                name='Rear Port 6',
+                label='F',
+                type=PortTypeChoices.TYPE_FC,
+                positions=6,
+                _site=devices[3].site,
+                _location=devices[3].location,
+                _rack=devices[3].rack,
             ),
             ),
-            RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
-            RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
-            RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
         )
         )
         RearPort.objects.bulk_create(rear_ports)
         RearPort.objects.bulk_create(rear_ports)
 
 
@@ -5550,9 +5697,33 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
         device_bays = (
         device_bays = (
-            DeviceBay(device=devices[0], name='Device Bay 1', label='A', description='First'),
-            DeviceBay(device=devices[1], name='Device Bay 2', label='B', description='Second'),
-            DeviceBay(device=devices[2], name='Device Bay 3', label='C', description='Third'),
+            DeviceBay(
+                device=devices[0],
+                name='Device Bay 1',
+                label='A',
+                description='First',
+                _site=devices[0].site,
+                _location=devices[0].location,
+                _rack=devices[0].rack,
+            ),
+            DeviceBay(
+                device=devices[1],
+                name='Device Bay 2',
+                label='B',
+                description='Second',
+                _site=devices[1].site,
+                _location=devices[1].location,
+                _rack=devices[1].rack,
+            ),
+            DeviceBay(
+                device=devices[2],
+                name='Device Bay 3',
+                label='C',
+                description='Third',
+                _site=devices[2].site,
+                _location=devices[2].location,
+                _rack=devices[2].rack,
+            ),
         )
         )
         DeviceBay.objects.bulk_create(device_bays)
         DeviceBay.objects.bulk_create(device_bays)
 
 

+ 3 - 0
netbox/utilities/testing/api.py

@@ -470,6 +470,9 @@ class APIViewTestCases:
                 elif type(field.type) is StrawberryOptional and type(field.type.of_type) is LazyType:
                 elif type(field.type) is StrawberryOptional and type(field.type.of_type) is LazyType:
                     fields_string += f'{field.name} {{ id }}\n'
                     fields_string += f'{field.name} {{ id }}\n'
                 elif hasattr(field, 'is_relation') and field.is_relation:
                 elif hasattr(field, 'is_relation') and field.is_relation:
+                    # Ignore private fields
+                    if field.name.startswith('_'):
+                        continue
                     # Note: StrawberryField types do not have is_relation
                     # Note: StrawberryField types do not have is_relation
                     fields_string += f'{field.name} {{ id }}\n'
                     fields_string += f'{field.name} {{ id }}\n'
                 elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType):
                 elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType):