diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 8d5baf6d1..d4fb6f8f6 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -6,6 +6,7 @@ from django.dispatch import receiver from dcim.choices import CableEndChoices, LinkStatusChoices from ipam.models import Prefix +from netbox.search.backends import search_backend from virtualization.models import Cluster, VMInterface from wireless.models import WirelessLAN @@ -33,6 +34,7 @@ from .models import ( VirtualChassis, ) from .models.cables import trace_paths +from .search import DeviceIndex from .utils import create_cablepaths, rebuild_paths COMPONENT_MODELS = ( @@ -116,6 +118,24 @@ def assign_virtualchassis_master(instance, created, **kwargs): master.save() +@receiver(post_save, sender=VirtualChassis) +def update_virtualchassis_member_search_cache(instance, created, raw=False, update_fields=None, **kwargs): + """ + Refresh the search cache for member Devices when a VirtualChassis is renamed. DeviceIndex caches + virtual_chassis as its string value, so a rename would otherwise leave stale CachedValue entries. + """ + if raw or created: + return + # The VC name is the only VC attribute cached on member Devices; skip saves that can't change it. + if update_fields is not None and 'name' not in update_fields: + return + search_backend.cache( + Device.objects.filter(virtual_chassis=instance).select_related('virtual_chassis'), + indexer=DeviceIndex, + remove_existing=True + ) + + # # Cables # diff --git a/netbox/dcim/tests/test_search.py b/netbox/dcim/tests/test_search.py new file mode 100644 index 000000000..66a2ba95b --- /dev/null +++ b/netbox/dcim/tests/test_search.py @@ -0,0 +1,66 @@ +from django.test import TestCase + +from core.models import ObjectType +from dcim.models import Device, VirtualChassis +from extras.models import CachedValue +from utilities.testing import create_test_device + + +class VirtualChassisSearchCacheTestCase(TestCase): + + def setUp(self): + self.vc = VirtualChassis.objects.create(name='VC1') + self.device = create_test_device('Switch-1', virtual_chassis=self.vc, vc_position=1) + self.object_type = ObjectType.objects.get_for_model(Device) + + def test_renaming_virtual_chassis_refreshes_member_search_cache(self): + """ + Renaming a VirtualChassis updates the cached virtual_chassis value for its member Devices. + """ + # The member device is initially cached under the original VC name + self.assertTrue( + CachedValue.objects.filter( + object_type=self.object_type, + object_id=self.device.pk, + field='virtual_chassis', + value='VC1', + ).exists() + ) + + # Rename the VirtualChassis + self.vc.name = 'VC-test' + self.vc.save() + + # The stale entry is gone and a fresh one reflects the new name + self.assertFalse( + CachedValue.objects.filter( + object_type=self.object_type, + object_id=self.device.pk, + field='virtual_chassis', + value='VC1', + ).exists() + ) + self.assertTrue( + CachedValue.objects.filter( + object_type=self.object_type, + object_id=self.device.pk, + field='virtual_chassis', + value='VC-test', + ).exists() + ) + + def test_updating_virtual_chassis_without_name_change_keeps_member_cache(self): + """ + A targeted save excluding 'name' leaves the member Device search cache untouched. + """ + self.vc.domain = 'example' + self.vc.save(update_fields=['domain']) + + self.assertTrue( + CachedValue.objects.filter( + object_type=self.object_type, + object_id=self.device.pk, + field='virtual_chassis', + value='VC1', + ).exists() + )