import logging from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save, post_delete, pre_delete from django.dispatch import receiver from .choices import LinkStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis from .utils import create_cablepath, rebuild_paths # # Location/rack/device assignment # @receiver(post_save, sender=Location) def handle_location_site_change(instance, created, **kwargs): """ Update child objects if Site assignment has changed. We intentionally recurse through each child object instead of calling update() on the QuerySet to ensure the proper change records get created for each. """ if not created: instance.get_descendants().update(site=instance.site) locations = instance.get_descendants(include_self=True).values_list('pk', flat=True) Rack.objects.filter(location__in=locations).update(site=instance.site) Device.objects.filter(location__in=locations).update(site=instance.site) PowerPanel.objects.filter(location__in=locations).update(site=instance.site) @receiver(post_save, sender=Rack) def handle_rack_site_change(instance, created, **kwargs): """ Update child Devices if Site or Location assignment has changed. """ if not created: Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) # # Virtual chassis # @receiver(post_save, sender=VirtualChassis) def assign_virtualchassis_master(instance, created, **kwargs): """ When a VirtualChassis is created, automatically assign its master device (if any) to the VC. """ if created and instance.master: master = Device.objects.get(pk=instance.master.pk) master.virtual_chassis = instance master.vc_position = 1 master.save() @receiver(pre_delete, sender=VirtualChassis) def clear_virtualchassis_members(instance, **kwargs): """ When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members. """ devices = Device.objects.filter(virtual_chassis=instance.pk) for device in devices: device.vc_position = None device.vc_priority = None device.save() # # Cables # @receiver(post_save, sender=Cable) def update_connected_endpoints(instance, created, raw=False, **kwargs): """ When a Cable is saved, check for and update its two connected endpoints """ logger = logging.getLogger('netbox.dcim.cable') if raw: logger.debug(f"Skipping endpoint updates for imported cable {instance}") return # Cache the Cable on its two termination points if instance.termination_a.cable != instance: logger.debug(f"Updating termination A for cable {instance}") instance.termination_a.cable = instance instance.termination_a._link_peer = instance.termination_b instance.termination_a.save() if instance.termination_b.cable != instance: logger.debug(f"Updating termination B for cable {instance}") instance.termination_b.cable = instance instance.termination_b._link_peer = instance.termination_a instance.termination_b.save() # Create/update cable paths if created: for termination in (instance.termination_a, instance.termination_b): if isinstance(termination, PathEndpoint): create_cablepath(termination) else: rebuild_paths(termination) elif instance.status != instance._orig_status: # We currently don't support modifying either termination of an existing Cable. (This # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. if instance.status != LinkStatusChoices.STATUS_CONNECTED: CablePath.objects.filter(path__contains=instance).update(is_active=False) else: rebuild_paths(instance) @receiver(post_delete, sender=Cable) def nullify_connected_endpoints(instance, **kwargs): """ When a Cable is deleted, check for and update its two connected endpoints """ logger = logging.getLogger('netbox.dcim.cable') # Disassociate the Cable from its termination points if instance.termination_a is not None: logger.debug(f"Nullifying termination A for cable {instance}") model = instance.termination_a._meta.model model.objects.filter(pk=instance.termination_a.pk).update(_link_peer_type=None, _link_peer_id=None) if instance.termination_b is not None: logger.debug(f"Nullifying termination B for cable {instance}") model = instance.termination_b._meta.model model.objects.filter(pk=instance.termination_b.pk).update(_link_peer_type=None, _link_peer_id=None) # Delete and retrace any dependent cable paths for cablepath in CablePath.objects.filter(path__contains=instance): cp = CablePath.from_origin(cablepath.origin) if cp: CablePath.objects.filter(pk=cablepath.pk).update( path=cp.path, destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None, destination_id=cp.destination.pk if cp.destination else None, is_active=cp.is_active, is_split=cp.is_split ) else: cablepath.delete()