From 85ca55e9e7a1e0f52a4e0dc9aa5131e2574a9ee9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 9 Dec 2025 18:51:14 -0800 Subject: [PATCH 1/3] #20875 fix updating of denormalized fields (_site, _location, _rack) for all component models --- netbox/dcim/models/devices.py | 3 +++ netbox/dcim/models/modules.py | 3 +++ netbox/dcim/signals.py | 31 +++++++++---------------------- netbox/dcim/utils.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 64d2e346c..dcc8940cc 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -33,6 +33,7 @@ from utilities.tracking import TrackingModelMixin from .device_components import * from .mixins import RenderConfigMixin from .modules import Module +from ..utils import update_device_components __all__ = ( @@ -1012,6 +1013,8 @@ class Device( self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False) # Interface bridges have to be set after interface instantiation update_interface_bridges(self, self.device_type.interfacetemplates.all()) + # Update denormalized fields for all components + update_device_components(self) # Update Site and Rack assignment for any child Devices devices = Device.objects.filter(parent_bay__device=self) diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 1c3f9f730..b2792a070 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -15,6 +15,7 @@ from netbox.models.mixins import WeightMixin from utilities.jsonschema import validate_schema from utilities.string import title from .device_components import * +from ..utils import update_device_components __all__ = ( 'Module', @@ -347,3 +348,5 @@ class Module(PrimaryModel, ConfigContextModel): # Interface bridges have to be set after interface instantiation update_interface_bridges(self.device, self.module_type.interfacetemplates, self) + # Update denormalized fields for all components + update_device_components(self.device) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 9295ddbdb..0fd00687c 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -6,25 +6,11 @@ from django.dispatch import receiver from dcim.choices import CableEndChoices, LinkStatusChoices from virtualization.models import VMInterface from .models import ( - Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface, - InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location, + Cable, CablePath, CableTermination, Device, FrontPort, Interface, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis, ) from .models.cables import trace_paths -from .utils import create_cablepath, rebuild_paths - -COMPONENT_MODELS = ( - ConsolePort, - ConsoleServerPort, - DeviceBay, - FrontPort, - Interface, - InventoryItem, - ModuleBay, - PowerOutlet, - PowerPort, - RearPort, -) +from .utils import create_cablepath, rebuild_paths, update_device_components # @@ -44,6 +30,9 @@ def handle_location_site_change(instance, created, **kwargs): Device.objects.filter(location__in=locations).update(site=instance.site) PowerPanel.objects.filter(location__in=locations).update(site=instance.site) CableTermination.objects.filter(_location__in=locations).update(_site=instance.site) + # Update component models for devices in these locations + for device in Device.objects.filter(location__in=locations): + update_device_components(device) @receiver(post_save, sender=Rack) @@ -53,6 +42,9 @@ def handle_rack_site_change(instance, created, **kwargs): """ if not created: Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) + # Update component models for devices in this rack + for device in Device.objects.filter(rack=instance): + update_device_components(device) @receiver(post_save, sender=Device) @@ -61,12 +53,7 @@ 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, - ) + update_device_components(instance) # diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index a03790ea2..e6f7fd98c 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -76,3 +76,36 @@ def update_interface_bridges(device, interface_templates, module=None): ) interface.full_clean() interface.save() + + +def update_device_components(device): + """ + Update denormalized fields (_site, _location, _rack) for all component models + associated with the specified device. + + :param device: Device instance whose components should be updated + """ + from dcim.models import ( + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, + InventoryItem, ModuleBay, PowerOutlet, PowerPort, RearPort, + ) + + COMPONENT_MODELS = ( + ConsolePort, + ConsoleServerPort, + DeviceBay, + FrontPort, + Interface, + InventoryItem, + ModuleBay, + PowerOutlet, + PowerPort, + RearPort, + ) + + for model in COMPONENT_MODELS: + model.objects.filter(device=device).update( + _site=device.site, + _location=device.location, + _rack=device.rack, + ) From 7ad3e9daf385a269d43c120970ff648a7935c66e Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 12 Dec 2025 09:20:04 -0800 Subject: [PATCH 2/3] remove common function for update --- netbox/dcim/models/devices.py | 9 +++++--- netbox/dcim/models/modules.py | 10 ++++++--- netbox/dcim/signals.py | 39 ++++++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index dcc8940cc..950a0cf31 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -33,7 +33,6 @@ from utilities.tracking import TrackingModelMixin from .device_components import * from .mixins import RenderConfigMixin from .modules import Module -from ..utils import update_device_components __all__ = ( @@ -958,6 +957,12 @@ class Device( if cf_defaults := CustomField.objects.get_defaults_for_model(model): for component in components: component.custom_field_data = cf_defaults + # Set denormalized references (_site, _location, _rack) before bulk_create + # since bulk_create bypasses the save() method + for component in components: + component._site = self.site + component._location = self.location + component._rack = self.rack components = model.objects.bulk_create(components) # Prefetch related objects to minimize queries needed during post_save prefetch_fields = get_prefetchable_fields(model) @@ -1013,8 +1018,6 @@ class Device( self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False) # Interface bridges have to be set after interface instantiation update_interface_bridges(self, self.device_type.interfacetemplates.all()) - # Update denormalized fields for all components - update_device_components(self) # Update Site and Rack assignment for any child Devices devices = Device.objects.filter(parent_bay__device=self) diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index b2792a070..e3934da00 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -15,7 +15,6 @@ from netbox.models.mixins import WeightMixin from utilities.jsonschema import validate_schema from utilities.string import title from .device_components import * -from ..utils import update_device_components __all__ = ( 'Module', @@ -316,6 +315,13 @@ class Module(PrimaryModel, ConfigContextModel): for component in create_instances: component.custom_field_data = cf_defaults + # Set denormalized references (_site, _location, _rack) before bulk_create + # since bulk_create bypasses the save() method + for component in create_instances: + component._site = self.device.site + component._location = self.device.location + component._rack = self.device.rack + if component_model is not ModuleBay: component_model.objects.bulk_create(create_instances) # Emit the post_save signal for each newly created object @@ -348,5 +354,3 @@ class Module(PrimaryModel, ConfigContextModel): # Interface bridges have to be set after interface instantiation update_interface_bridges(self.device, self.module_type.interfacetemplates, self) - # Update denormalized fields for all components - update_device_components(self.device) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 0fd00687c..758cf4821 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -6,11 +6,25 @@ from django.dispatch import receiver from dcim.choices import CableEndChoices, LinkStatusChoices from virtualization.models import VMInterface from .models import ( - Cable, CablePath, CableTermination, Device, FrontPort, Interface, PathEndpoint, PowerPanel, Rack, Location, + 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 .utils import create_cablepath, rebuild_paths, update_device_components +from .utils import create_cablepath, rebuild_paths + +COMPONENT_MODELS = ( + ConsolePort, + ConsoleServerPort, + DeviceBay, + FrontPort, + Interface, + InventoryItem, + ModuleBay, + PowerOutlet, + PowerPort, + RearPort, +) # @@ -31,8 +45,9 @@ def handle_location_site_change(instance, created, **kwargs): PowerPanel.objects.filter(location__in=locations).update(site=instance.site) CableTermination.objects.filter(_location__in=locations).update(_site=instance.site) # Update component models for devices in these locations - for device in Device.objects.filter(location__in=locations): - update_device_components(device) + # (since Device.objects.update() doesn't trigger post_save signals) + for model in COMPONENT_MODELS: + model.objects.filter(device__location__in=locations).update(_site=instance.site) @receiver(post_save, sender=Rack) @@ -43,8 +58,13 @@ def handle_rack_site_change(instance, created, **kwargs): if not created: Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) # Update component models for devices in this rack - for device in Device.objects.filter(rack=instance): - update_device_components(device) + # (since Device.objects.update() doesn't trigger post_save signals) + for model in COMPONENT_MODELS: + model.objects.filter(device__rack=instance).update( + _site=instance.site, + _location=instance.location, + _rack=instance, + ) @receiver(post_save, sender=Device) @@ -53,7 +73,12 @@ 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: - update_device_components(instance) + for model in COMPONENT_MODELS: + model.objects.filter(device=instance).update( + _site=instance.site, + _location=instance.location, + _rack=instance.rack, + ) # From e47eea9bf4920ef87c1c4f775d54f7f5d364f36e Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 12 Dec 2025 09:24:39 -0800 Subject: [PATCH 3/3] cleanup --- netbox/dcim/models/devices.py | 3 +-- netbox/dcim/models/modules.py | 3 +-- netbox/dcim/signals.py | 2 -- netbox/dcim/utils.py | 33 --------------------------------- 4 files changed, 2 insertions(+), 39 deletions(-) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 950a0cf31..6c5a2d85d 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -957,8 +957,7 @@ class Device( if cf_defaults := CustomField.objects.get_defaults_for_model(model): for component in components: component.custom_field_data = cf_defaults - # Set denormalized references (_site, _location, _rack) before bulk_create - # since bulk_create bypasses the save() method + # Set denormalized references for component in components: component._site = self.site component._location = self.location diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index e3934da00..b5071794a 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -315,8 +315,7 @@ class Module(PrimaryModel, ConfigContextModel): for component in create_instances: component.custom_field_data = cf_defaults - # Set denormalized references (_site, _location, _rack) before bulk_create - # since bulk_create bypasses the save() method + # Set denormalized references for component in create_instances: component._site = self.device.site component._location = self.device.location diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 758cf4821..daff68599 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -45,7 +45,6 @@ def handle_location_site_change(instance, created, **kwargs): PowerPanel.objects.filter(location__in=locations).update(site=instance.site) CableTermination.objects.filter(_location__in=locations).update(_site=instance.site) # Update component models for devices in these locations - # (since Device.objects.update() doesn't trigger post_save signals) for model in COMPONENT_MODELS: model.objects.filter(device__location__in=locations).update(_site=instance.site) @@ -58,7 +57,6 @@ def handle_rack_site_change(instance, created, **kwargs): if not created: Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) # Update component models for devices in this rack - # (since Device.objects.update() doesn't trigger post_save signals) for model in COMPONENT_MODELS: model.objects.filter(device__rack=instance).update( _site=instance.site, diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index e6f7fd98c..a03790ea2 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -76,36 +76,3 @@ def update_interface_bridges(device, interface_templates, module=None): ) interface.full_clean() interface.save() - - -def update_device_components(device): - """ - Update denormalized fields (_site, _location, _rack) for all component models - associated with the specified device. - - :param device: Device instance whose components should be updated - """ - from dcim.models import ( - ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, - InventoryItem, ModuleBay, PowerOutlet, PowerPort, RearPort, - ) - - COMPONENT_MODELS = ( - ConsolePort, - ConsoleServerPort, - DeviceBay, - FrontPort, - Interface, - InventoryItem, - ModuleBay, - PowerOutlet, - PowerPort, - RearPort, - ) - - for model in COMPONENT_MODELS: - model.objects.filter(device=device).update( - _site=device.site, - _location=device.location, - _rack=device.rack, - )