signals.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import logging
  2. from django.db.models.signals import post_save, post_delete, pre_delete
  3. from django.dispatch import receiver
  4. from .choices import CableEndChoices, LinkStatusChoices
  5. from .models import (
  6. Cable, CablePath, CableTermination, Device, FrontPort, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis,
  7. )
  8. from .models.cables import trace_paths
  9. from .utils import create_cablepath, rebuild_paths
  10. #
  11. # Location/rack/device assignment
  12. #
  13. @receiver(post_save, sender=Location)
  14. def handle_location_site_change(instance, created, **kwargs):
  15. """
  16. Update child objects if Site assignment has changed. We intentionally recurse through each child
  17. object instead of calling update() on the QuerySet to ensure the proper change records get created for each.
  18. """
  19. if not created:
  20. instance.get_descendants().update(site=instance.site)
  21. locations = instance.get_descendants(include_self=True).values_list('pk', flat=True)
  22. Rack.objects.filter(location__in=locations).update(site=instance.site)
  23. Device.objects.filter(location__in=locations).update(site=instance.site)
  24. PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
  25. CableTermination.objects.filter(_location__in=locations).update(_site=instance.site)
  26. @receiver(post_save, sender=Rack)
  27. def handle_rack_site_change(instance, created, **kwargs):
  28. """
  29. Update child Devices if Site or Location assignment has changed.
  30. """
  31. if not created:
  32. Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
  33. #
  34. # Virtual chassis
  35. #
  36. @receiver(post_save, sender=VirtualChassis)
  37. def assign_virtualchassis_master(instance, created, **kwargs):
  38. """
  39. When a VirtualChassis is created, automatically assign its master device (if any) to the VC.
  40. """
  41. if created and instance.master:
  42. master = Device.objects.get(pk=instance.master.pk)
  43. master.virtual_chassis = instance
  44. master.vc_position = 1
  45. master.save()
  46. @receiver(pre_delete, sender=VirtualChassis)
  47. def clear_virtualchassis_members(instance, **kwargs):
  48. """
  49. When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
  50. """
  51. devices = Device.objects.filter(virtual_chassis=instance.pk)
  52. for device in devices:
  53. device.vc_position = None
  54. device.vc_priority = None
  55. device.save()
  56. #
  57. # Cables
  58. #
  59. @receiver(trace_paths, sender=Cable)
  60. def update_connected_endpoints(instance, created, raw=False, **kwargs):
  61. """
  62. When a Cable is saved with new terminations, retrace any affected cable paths.
  63. """
  64. logger = logging.getLogger('netbox.dcim.cable')
  65. if raw:
  66. logger.debug(f"Skipping endpoint updates for imported cable {instance}")
  67. return
  68. # Update cable paths if new terminations have been set
  69. if instance._terminations_modified:
  70. a_terminations = []
  71. b_terminations = []
  72. # Note: instance.terminations.all() is not safe to use here as it might be stale
  73. for t in CableTermination.objects.filter(cable=instance):
  74. if t.cable_end == CableEndChoices.SIDE_A:
  75. a_terminations.append(t.termination)
  76. else:
  77. b_terminations.append(t.termination)
  78. for nodes in [a_terminations, b_terminations]:
  79. # Examine type of first termination to determine object type (all must be the same)
  80. if not nodes:
  81. continue
  82. if isinstance(nodes[0], PathEndpoint):
  83. create_cablepath(nodes)
  84. else:
  85. rebuild_paths(nodes)
  86. # Update status of CablePaths if Cable status has been changed
  87. elif instance.status != instance._orig_status:
  88. if instance.status != LinkStatusChoices.STATUS_CONNECTED:
  89. CablePath.objects.filter(_nodes__contains=instance).update(is_active=False)
  90. else:
  91. rebuild_paths([instance])
  92. @receiver(post_delete, sender=Cable)
  93. def retrace_cable_paths(instance, **kwargs):
  94. """
  95. When a Cable is deleted, check for and update its connected endpoints
  96. """
  97. for cablepath in CablePath.objects.filter(_nodes__contains=instance):
  98. cablepath.retrace()
  99. @receiver(post_delete, sender=CableTermination)
  100. def nullify_connected_endpoints(instance, **kwargs):
  101. """
  102. Disassociate the Cable from the termination object, and retrace any affected CablePaths.
  103. """
  104. model = instance.termination_type.model_class()
  105. model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
  106. for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
  107. # Remove the deleted CableTermination if it's one of the path's originating nodes
  108. if instance.termination in cablepath.origins:
  109. cablepath.origins.remove(instance.termination)
  110. cablepath.retrace()
  111. @receiver(post_save, sender=FrontPort)
  112. def extend_rearport_cable_paths(instance, created, raw, **kwargs):
  113. """
  114. When a new FrontPort is created, add it to any CablePaths which end at its corresponding RearPort.
  115. """
  116. if created and not raw:
  117. rearport = instance.rear_port
  118. for cablepath in CablePath.objects.filter(_nodes__contains=rearport):
  119. cablepath.retrace()