We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unable to update device after deleting its device redundancy group if the device DRG priority is set
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): 1.6.4
* Python version: 3.11.6
* Database platform, version: mysql 8.1.0
* Middleware(s): pynautobot, GUI, Swagger UI

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create a device redundancy group (DRG)
2. Create a device
3. Assign device to redundancy group with a priority
4. Delete device redundancy group
5. Attempt to make an edit to the device

Note that with the DRG deleted, it's no longer listed in the device edit screen but the DRG priority remains set; since having a DRG priority set without having a corresponding DRG set fails validation, no edits can be made to the device until either the priority is removed or a new DRG is configured. Attempts to edit the device result in the error see in Observed Behavior, below. Bonus: If you attempt to add such a device to a Virtual Chassis, you get this error:

![Screenshot 2023-10-26 at 5 32 26 PM](https://github.com/nautobot/nautobot/assets/120611026/c911bc78-d9ba-42e6-b893-3dbceb9836ff)


<!-- What did you expect to happen? -->
### Expected Behavior
I can absolutely see that changing this behavior may be more trouble than it's worth, however in a perfect world it feels to me as though either any devices using the deleted redundancy group should have their priority set to `None` or DRG deletion should fail until/unless no devices are using it. That said, I don't know the code base well enough to know which (or if either) of those solutions is correct. 

<!-- What happened instead? -->
### Observed Behavior
After the DRG was deleted and I was unable to make any updates to the device without first nulling out the device DRG priority. returned message is:

```json
{
  "device_redundancy_group_priority": [
    "Must assign a redundancy group when defining a redundancy group priority."
  ]
}
```

Thoughts?




--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `nautobot/dcim/signals.py`
Content:
```
1 import logging
2 
3 from cacheops import invalidate_obj
4 from django.contrib.contenttypes.models import ContentType
5 from django.db.models.signals import m2m_changed, post_save, pre_delete
6 from django.db import transaction
7 from django.dispatch import receiver
8 
9 from nautobot.core.signals import disable_for_loaddata
10 from .models import (
11     Cable,
12     CablePath,
13     Device,
14     PathEndpoint,
15     PowerPanel,
16     Rack,
17     RackGroup,
18     VirtualChassis,
19     Interface,
20 )
21 from .utils import validate_interface_tagged_vlans
22 
23 
24 def create_cablepath(node, rebuild=True):
25     """
26     Create CablePaths for all paths originating from the specified node.
27 
28     rebuild (bool) - Used to refresh paths where this node is not an endpoint.
29     """
30     cp = CablePath.from_origin(node)
31     if cp:
32         try:
33             cp.save()
34         except Exception as e:
35             print(node, node.pk)
36             raise e
37     if rebuild:
38         rebuild_paths(node)
39 
40 
41 def rebuild_paths(obj):
42     """
43     Rebuild all CablePaths which traverse the specified node
44     """
45     cable_paths = CablePath.objects.filter(path__contains=obj)
46 
47     with transaction.atomic():
48         for cp in cable_paths:
49             invalidate_obj(cp.origin)
50             cp.delete()
51             # Prevent looping back to rebuild_paths during the atomic transaction.
52             create_cablepath(cp.origin, rebuild=False)
53 
54 
55 #
56 # Site/location/rack/device assignment
57 #
58 
59 
60 @receiver(post_save, sender=RackGroup)
61 def handle_rackgroup_site_location_change(instance, created, raw=False, **kwargs):
62     """
63     Update child RackGroups, Racks, and PowerPanels if Site or Location assignment has changed.
64 
65     We intentionally recurse through each child object instead of calling update() on the QuerySet
66     to ensure the proper change records get created for each.
67 
68     Note that this is non-trivial for Location changes, since a LocationType that can contain RackGroups
69     may or may not be permitted to contain Racks or PowerPanels. If it's not permitted, rather than trying to search
70     through child locations to find the "right" one, the best we can do is simply to null out the location.
71     """
72     if raw:
73         return
74     if not created:
75         if instance.location is not None:
76             descendants = instance.location.descendants(include_self=True)
77             content_types = instance.location.location_type.content_types.all()
78             rack_groups_permitted = ContentType.objects.get_for_model(RackGroup) in content_types
79             racks_permitted = ContentType.objects.get_for_model(Rack) in content_types
80             power_panels_permitted = ContentType.objects.get_for_model(PowerPanel) in content_types
81         else:
82             descendants = None
83             rack_groups_permitted = False
84             racks_permitted = False
85             power_panels_permitted = False
86 
87         for rackgroup in instance.get_children():
88             changed = False
89             if rackgroup.site != instance.site:
90                 rackgroup.site = instance.site
91                 changed = True
92 
93             if instance.location is not None:
94                 if rackgroup.location is not None and rackgroup.location not in descendants:
95                     rackgroup.location = instance.location if rack_groups_permitted else None
96                     changed = True
97             elif rackgroup.location is not None and rackgroup.location.base_site != instance.site:
98                 rackgroup.location = None
99                 changed = True
100 
101             if changed:
102                 rackgroup.save()
103 
104         for rack in Rack.objects.filter(group=instance):
105             changed = False
106             if rack.site != instance.site:
107                 rack.site = instance.site
108                 changed = True
109 
110             if instance.location is not None:
111                 if rack.location is not None and rack.location not in descendants:
112                     rack.location = instance.location if racks_permitted else None
113                     changed = True
114             elif rack.location is not None and rack.location.base_site != instance.site:
115                 rack.location = None
116                 changed = True
117 
118             if changed:
119                 rack.save()
120 
121         for powerpanel in PowerPanel.objects.filter(rack_group=instance):
122             changed = False
123             if powerpanel.site != instance.site:
124                 powerpanel.site = instance.site
125                 changed = True
126 
127             if instance.location is not None:
128                 if powerpanel.location is not None and powerpanel.location not in descendants:
129                     powerpanel.location = instance.location if power_panels_permitted else None
130                     changed = True
131             elif powerpanel.location is not None and powerpanel.location.base_site != instance.site:
132                 powerpanel.location = None
133                 changed = True
134 
135             if changed:
136                 powerpanel.save()
137 
138 
139 @receiver(post_save, sender=Rack)
140 def handle_rack_site_location_change(instance, created, raw=False, **kwargs):
141     """
142     Update child Devices if Site or Location assignment has changed.
143 
144     Note that this is non-trivial for Location changes, since a LocationType that can contain Racks
145     may or may not be permitted to contain Devices. If it's not permitted, rather than trying to search
146     through child locations to find the "right" one, the best we can do is simply to null out the location.
147     """
148     if raw:
149         return
150     if not created:
151         if instance.location is not None:
152             devices_permitted = (
153                 ContentType.objects.get_for_model(Device) in instance.location.location_type.content_types.all()
154             )
155 
156         for device in Device.objects.filter(rack=instance):
157             changed = False
158             if device.site != instance.site:
159                 device.site = instance.site
160                 changed = True
161 
162             if instance.location is not None:
163                 if device.location is not None and device.location != instance.location:
164                     device.location = instance.location if devices_permitted else None
165                     changed = True
166             elif device.location is not None and device.location.base_site != instance.site:
167                 device.location = None
168                 changed = True
169 
170             if changed:
171                 device.save()
172 
173 
174 #
175 # Virtual chassis
176 #
177 
178 
179 @receiver(post_save, sender=VirtualChassis)
180 def assign_virtualchassis_master(instance, created, raw=False, **kwargs):
181     """
182     When a VirtualChassis is created, automatically assign its master device (if any) to the VC.
183     """
184     if raw:
185         return
186     if created and instance.master:
187         master = Device.objects.get(pk=instance.master.pk)
188         master.virtual_chassis = instance
189         if instance.master.vc_position is None:
190             master.vc_position = 1
191         master.save()
192 
193 
194 @receiver(pre_delete, sender=VirtualChassis)
195 def clear_virtualchassis_members(instance, **kwargs):
196     """
197     When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
198     """
199     devices = Device.objects.filter(virtual_chassis=instance.pk)
200     for device in devices:
201         device.vc_position = None
202         device.vc_priority = None
203         device.save()
204 
205 
206 #
207 # Cables
208 #
209 
210 
211 @receiver(post_save, sender=Cable)
212 def update_connected_endpoints(instance, created, raw=False, **kwargs):
213     """
214     When a Cable is saved, check for and update its two connected endpoints
215     """
216     logger = logging.getLogger("nautobot.dcim.cable")
217     if raw:
218         logger.debug(f"Skipping endpoint updates for imported cable {instance}")
219         return
220 
221     # Cache the Cable on its two termination points
222     if instance.termination_a.cable != instance:
223         logger.debug(f"Updating termination A for cable {instance}")
224         instance.termination_a.cable = instance
225         instance.termination_a._cable_peer = instance.termination_b
226         instance.termination_a.save()
227     if instance.termination_b.cable != instance:
228         logger.debug(f"Updating termination B for cable {instance}")
229         instance.termination_b.cable = instance
230         instance.termination_b._cable_peer = instance.termination_a
231         instance.termination_b.save()
232 
233     # Create/update cable paths
234     if created:
235         for termination in (instance.termination_a, instance.termination_b):
236             if isinstance(termination, PathEndpoint):
237                 create_cablepath(termination)
238             else:
239                 rebuild_paths(termination)
240     elif instance.status != instance._orig_status:
241         # We currently don't support modifying either termination of an existing Cable. (This
242         # may change in the future.) However, we do need to capture status changes and update
243         # any CablePaths accordingly.
244         if instance.status != Cable.STATUS_CONNECTED:
245             CablePath.objects.filter(path__contains=instance).update(is_active=False)
246         else:
247             rebuild_paths(instance)
248 
249 
250 @receiver(pre_delete, sender=Cable)
251 def nullify_connected_endpoints(instance, **kwargs):
252     """
253     When a Cable is deleted, check for and update its two connected endpoints
254     """
255     logger = logging.getLogger("nautobot.dcim.cable")
256 
257     # Disassociate the Cable from its termination points
258     if instance.termination_a is not None:
259         logger.debug(f"Nullifying termination A for cable {instance}")
260         instance.termination_a.cable = None
261         instance.termination_a._cable_peer = None
262         instance.termination_a.save()
263     if instance.termination_b is not None:
264         logger.debug(f"Nullifying termination B for cable {instance}")
265         instance.termination_b.cable = None
266         instance.termination_b._cable_peer = None
267         instance.termination_b.save()
268 
269     # Delete and retrace any dependent cable paths
270     for cablepath in CablePath.objects.filter(path__contains=instance):
271         cp = CablePath.from_origin(cablepath.origin)
272         if cp:
273             CablePath.objects.filter(pk=cablepath.pk).update(
274                 path=cp.path,
275                 destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
276                 destination_id=cp.destination.pk if cp.destination else None,
277                 is_active=cp.is_active,
278                 is_split=cp.is_split,
279             )
280         else:
281             cablepath.delete()
282 
283 
284 #
285 # Interface tagged VLAMs
286 #
287 
288 
289 @receiver(m2m_changed, sender=Interface.tagged_vlans.through)
290 @disable_for_loaddata
291 def prevent_adding_tagged_vlans_with_incorrect_mode_or_site(sender, instance, action, **kwargs):
292     if action != "pre_add":
293         return
294 
295     validate_interface_tagged_vlans(instance, kwargs["model"], kwargs["pk_set"])
296
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/nautobot/dcim/signals.py b/nautobot/dcim/signals.py
--- a/nautobot/dcim/signals.py
+++ b/nautobot/dcim/signals.py
@@ -11,6 +11,7 @@
     Cable,
     CablePath,
     Device,
+    DeviceRedundancyGroup,
     PathEndpoint,
     PowerPanel,
     Rack,
@@ -171,6 +172,22 @@
                 device.save()
 
 
+#
+# Device redundancy group
+#
+
+
+@receiver(pre_delete, sender=DeviceRedundancyGroup)
+def clear_deviceredundancygroup_members(instance, **kwargs):
+    """
+    When a DeviceRedundancyGroup is deleted, nullify the device_redundancy_group_priority field of its prior members.
+    """
+    devices = Device.objects.filter(device_redundancy_group=instance.pk)
+    for device in devices:
+        device.device_redundancy_group_priority = None
+        device.save()
+
+
 #
 # Virtual chassis
 #

{"golden_diff": "diff --git a/nautobot/dcim/signals.py b/nautobot/dcim/signals.py\n--- a/nautobot/dcim/signals.py\n+++ b/nautobot/dcim/signals.py\n@@ -11,6 +11,7 @@\n     Cable,\n     CablePath,\n     Device,\n+    DeviceRedundancyGroup,\n     PathEndpoint,\n     PowerPanel,\n     Rack,\n@@ -171,6 +172,22 @@\n                 device.save()\n \n \n+#\n+# Device redundancy group\n+#\n+\n+\n+@receiver(pre_delete, sender=DeviceRedundancyGroup)\n+def clear_deviceredundancygroup_members(instance, **kwargs):\n+    \"\"\"\n+    When a DeviceRedundancyGroup is deleted, nullify the device_redundancy_group_priority field of its prior members.\n+    \"\"\"\n+    devices = Device.objects.filter(device_redundancy_group=instance.pk)\n+    for device in devices:\n+        device.device_redundancy_group_priority = None\n+        device.save()\n+\n+\n #\n # Virtual chassis\n #\n", "issue": "Unable to update device after deleting its device redundancy group if the device DRG priority is set\n<!--\r\n    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.\r\n\r\n    This form is only for reporting reproducible bugs. If you need assistance\r\n    with Nautobot installation, or if you have a general question, please start a\r\n    discussion instead: https://github.com/nautobot/nautobot/discussions\r\n\r\n    Please describe the environment in which you are running Nautobot. Be sure\r\n    that you are running an unmodified instance of the latest stable release\r\n    before submitting a bug report, and that any plugins have been disabled.\r\n-->\r\n### Environment\r\n* Nautobot version (Docker tag too if applicable): 1.6.4\r\n* Python version: 3.11.6\r\n* Database platform, version: mysql 8.1.0\r\n* Middleware(s): pynautobot, GUI, Swagger UI\r\n\r\n<!--\r\n    Describe in detail the exact steps that someone else can take to reproduce\r\n    this bug using the current stable release of Nautobot. Begin with the\r\n    creation of any necessary database objects and call out every operation\r\n    being performed explicitly. If reporting a bug in the REST API, be sure to\r\n    reconstruct the raw HTTP request(s) being made: Don't rely on a client\r\n    library such as pynautobot.\r\n-->\r\n### Steps to Reproduce\r\n1. Create a device redundancy group (DRG)\r\n2. Create a device\r\n3. Assign device to redundancy group with a priority\r\n4. Delete device redundancy group\r\n5. Attempt to make an edit to the device\r\n\r\nNote that with the DRG deleted, it's no longer listed in the device edit screen but the DRG priority remains set; since having a DRG priority set without having a corresponding DRG set fails validation, no edits can be made to the device until either the priority is removed or a new DRG is configured. Attempts to edit the device result in the error see in Observed Behavior, below. Bonus: If you attempt to add such a device to a Virtual Chassis, you get this error:\r\n\r\n![Screenshot 2023-10-26 at 5 32 26 PM](https://github.com/nautobot/nautobot/assets/120611026/c911bc78-d9ba-42e6-b893-3dbceb9836ff)\r\n\r\n\r\n<!-- What did you expect to happen? -->\r\n### Expected Behavior\r\nI can absolutely see that changing this behavior may be more trouble than it's worth, however in a perfect world it feels to me as though either any devices using the deleted redundancy group should have their priority set to `None` or DRG deletion should fail until/unless no devices are using it. That said, I don't know the code base well enough to know which (or if either) of those solutions is correct. \r\n\r\n<!-- What happened instead? -->\r\n### Observed Behavior\r\nAfter the DRG was deleted and I was unable to make any updates to the device without first nulling out the device DRG priority. returned message is:\r\n\r\n```json\r\n{\r\n  \"device_redundancy_group_priority\": [\r\n    \"Must assign a redundancy group when defining a redundancy group priority.\"\r\n  ]\r\n}\r\n```\r\n\r\nThoughts?\r\n\r\n\r\n\n", "before_files": [{"content": "import logging\n\nfrom cacheops import invalidate_obj\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db.models.signals import m2m_changed, post_save, pre_delete\nfrom django.db import transaction\nfrom django.dispatch import receiver\n\nfrom nautobot.core.signals import disable_for_loaddata\nfrom .models import (\n    Cable,\n    CablePath,\n    Device,\n    PathEndpoint,\n    PowerPanel,\n    Rack,\n    RackGroup,\n    VirtualChassis,\n    Interface,\n)\nfrom .utils import validate_interface_tagged_vlans\n\n\ndef create_cablepath(node, rebuild=True):\n    \"\"\"\n    Create CablePaths for all paths originating from the specified node.\n\n    rebuild (bool) - Used to refresh paths where this node is not an endpoint.\n    \"\"\"\n    cp = CablePath.from_origin(node)\n    if cp:\n        try:\n            cp.save()\n        except Exception as e:\n            print(node, node.pk)\n            raise e\n    if rebuild:\n        rebuild_paths(node)\n\n\ndef rebuild_paths(obj):\n    \"\"\"\n    Rebuild all CablePaths which traverse the specified node\n    \"\"\"\n    cable_paths = CablePath.objects.filter(path__contains=obj)\n\n    with transaction.atomic():\n        for cp in cable_paths:\n            invalidate_obj(cp.origin)\n            cp.delete()\n            # Prevent looping back to rebuild_paths during the atomic transaction.\n            create_cablepath(cp.origin, rebuild=False)\n\n\n#\n# Site/location/rack/device assignment\n#\n\n\n@receiver(post_save, sender=RackGroup)\ndef handle_rackgroup_site_location_change(instance, created, raw=False, **kwargs):\n    \"\"\"\n    Update child RackGroups, Racks, and PowerPanels if Site or Location assignment has changed.\n\n    We intentionally recurse through each child object instead of calling update() on the QuerySet\n    to ensure the proper change records get created for each.\n\n    Note that this is non-trivial for Location changes, since a LocationType that can contain RackGroups\n    may or may not be permitted to contain Racks or PowerPanels. If it's not permitted, rather than trying to search\n    through child locations to find the \"right\" one, the best we can do is simply to null out the location.\n    \"\"\"\n    if raw:\n        return\n    if not created:\n        if instance.location is not None:\n            descendants = instance.location.descendants(include_self=True)\n            content_types = instance.location.location_type.content_types.all()\n            rack_groups_permitted = ContentType.objects.get_for_model(RackGroup) in content_types\n            racks_permitted = ContentType.objects.get_for_model(Rack) in content_types\n            power_panels_permitted = ContentType.objects.get_for_model(PowerPanel) in content_types\n        else:\n            descendants = None\n            rack_groups_permitted = False\n            racks_permitted = False\n            power_panels_permitted = False\n\n        for rackgroup in instance.get_children():\n            changed = False\n            if rackgroup.site != instance.site:\n                rackgroup.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if rackgroup.location is not None and rackgroup.location not in descendants:\n                    rackgroup.location = instance.location if rack_groups_permitted else None\n                    changed = True\n            elif rackgroup.location is not None and rackgroup.location.base_site != instance.site:\n                rackgroup.location = None\n                changed = True\n\n            if changed:\n                rackgroup.save()\n\n        for rack in Rack.objects.filter(group=instance):\n            changed = False\n            if rack.site != instance.site:\n                rack.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if rack.location is not None and rack.location not in descendants:\n                    rack.location = instance.location if racks_permitted else None\n                    changed = True\n            elif rack.location is not None and rack.location.base_site != instance.site:\n                rack.location = None\n                changed = True\n\n            if changed:\n                rack.save()\n\n        for powerpanel in PowerPanel.objects.filter(rack_group=instance):\n            changed = False\n            if powerpanel.site != instance.site:\n                powerpanel.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if powerpanel.location is not None and powerpanel.location not in descendants:\n                    powerpanel.location = instance.location if power_panels_permitted else None\n                    changed = True\n            elif powerpanel.location is not None and powerpanel.location.base_site != instance.site:\n                powerpanel.location = None\n                changed = True\n\n            if changed:\n                powerpanel.save()\n\n\n@receiver(post_save, sender=Rack)\ndef handle_rack_site_location_change(instance, created, raw=False, **kwargs):\n    \"\"\"\n    Update child Devices if Site or Location assignment has changed.\n\n    Note that this is non-trivial for Location changes, since a LocationType that can contain Racks\n    may or may not be permitted to contain Devices. If it's not permitted, rather than trying to search\n    through child locations to find the \"right\" one, the best we can do is simply to null out the location.\n    \"\"\"\n    if raw:\n        return\n    if not created:\n        if instance.location is not None:\n            devices_permitted = (\n                ContentType.objects.get_for_model(Device) in instance.location.location_type.content_types.all()\n            )\n\n        for device in Device.objects.filter(rack=instance):\n            changed = False\n            if device.site != instance.site:\n                device.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if device.location is not None and device.location != instance.location:\n                    device.location = instance.location if devices_permitted else None\n                    changed = True\n            elif device.location is not None and device.location.base_site != instance.site:\n                device.location = None\n                changed = True\n\n            if changed:\n                device.save()\n\n\n#\n# Virtual chassis\n#\n\n\n@receiver(post_save, sender=VirtualChassis)\ndef assign_virtualchassis_master(instance, created, raw=False, **kwargs):\n    \"\"\"\n    When a VirtualChassis is created, automatically assign its master device (if any) to the VC.\n    \"\"\"\n    if raw:\n        return\n    if created and instance.master:\n        master = Device.objects.get(pk=instance.master.pk)\n        master.virtual_chassis = instance\n        if instance.master.vc_position is None:\n            master.vc_position = 1\n        master.save()\n\n\n@receiver(pre_delete, sender=VirtualChassis)\ndef clear_virtualchassis_members(instance, **kwargs):\n    \"\"\"\n    When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.\n    \"\"\"\n    devices = Device.objects.filter(virtual_chassis=instance.pk)\n    for device in devices:\n        device.vc_position = None\n        device.vc_priority = None\n        device.save()\n\n\n#\n# Cables\n#\n\n\n@receiver(post_save, sender=Cable)\ndef update_connected_endpoints(instance, created, raw=False, **kwargs):\n    \"\"\"\n    When a Cable is saved, check for and update its two connected endpoints\n    \"\"\"\n    logger = logging.getLogger(\"nautobot.dcim.cable\")\n    if raw:\n        logger.debug(f\"Skipping endpoint updates for imported cable {instance}\")\n        return\n\n    # Cache the Cable on its two termination points\n    if instance.termination_a.cable != instance:\n        logger.debug(f\"Updating termination A for cable {instance}\")\n        instance.termination_a.cable = instance\n        instance.termination_a._cable_peer = instance.termination_b\n        instance.termination_a.save()\n    if instance.termination_b.cable != instance:\n        logger.debug(f\"Updating termination B for cable {instance}\")\n        instance.termination_b.cable = instance\n        instance.termination_b._cable_peer = instance.termination_a\n        instance.termination_b.save()\n\n    # Create/update cable paths\n    if created:\n        for termination in (instance.termination_a, instance.termination_b):\n            if isinstance(termination, PathEndpoint):\n                create_cablepath(termination)\n            else:\n                rebuild_paths(termination)\n    elif instance.status != instance._orig_status:\n        # We currently don't support modifying either termination of an existing Cable. (This\n        # may change in the future.) However, we do need to capture status changes and update\n        # any CablePaths accordingly.\n        if instance.status != Cable.STATUS_CONNECTED:\n            CablePath.objects.filter(path__contains=instance).update(is_active=False)\n        else:\n            rebuild_paths(instance)\n\n\n@receiver(pre_delete, sender=Cable)\ndef nullify_connected_endpoints(instance, **kwargs):\n    \"\"\"\n    When a Cable is deleted, check for and update its two connected endpoints\n    \"\"\"\n    logger = logging.getLogger(\"nautobot.dcim.cable\")\n\n    # Disassociate the Cable from its termination points\n    if instance.termination_a is not None:\n        logger.debug(f\"Nullifying termination A for cable {instance}\")\n        instance.termination_a.cable = None\n        instance.termination_a._cable_peer = None\n        instance.termination_a.save()\n    if instance.termination_b is not None:\n        logger.debug(f\"Nullifying termination B for cable {instance}\")\n        instance.termination_b.cable = None\n        instance.termination_b._cable_peer = None\n        instance.termination_b.save()\n\n    # Delete and retrace any dependent cable paths\n    for cablepath in CablePath.objects.filter(path__contains=instance):\n        cp = CablePath.from_origin(cablepath.origin)\n        if cp:\n            CablePath.objects.filter(pk=cablepath.pk).update(\n                path=cp.path,\n                destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,\n                destination_id=cp.destination.pk if cp.destination else None,\n                is_active=cp.is_active,\n                is_split=cp.is_split,\n            )\n        else:\n            cablepath.delete()\n\n\n#\n# Interface tagged VLAMs\n#\n\n\n@receiver(m2m_changed, sender=Interface.tagged_vlans.through)\n@disable_for_loaddata\ndef prevent_adding_tagged_vlans_with_incorrect_mode_or_site(sender, instance, action, **kwargs):\n    if action != \"pre_add\":\n        return\n\n    validate_interface_tagged_vlans(instance, kwargs[\"model\"], kwargs[\"pk_set\"])\n", "path": "nautobot/dcim/signals.py"}], "after_files": [{"content": "import logging\n\nfrom cacheops import invalidate_obj\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db.models.signals import m2m_changed, post_save, pre_delete\nfrom django.db import transaction\nfrom django.dispatch import receiver\n\nfrom nautobot.core.signals import disable_for_loaddata\nfrom .models import (\n    Cable,\n    CablePath,\n    Device,\n    DeviceRedundancyGroup,\n    PathEndpoint,\n    PowerPanel,\n    Rack,\n    RackGroup,\n    VirtualChassis,\n    Interface,\n)\nfrom .utils import validate_interface_tagged_vlans\n\n\ndef create_cablepath(node, rebuild=True):\n    \"\"\"\n    Create CablePaths for all paths originating from the specified node.\n\n    rebuild (bool) - Used to refresh paths where this node is not an endpoint.\n    \"\"\"\n    cp = CablePath.from_origin(node)\n    if cp:\n        try:\n            cp.save()\n        except Exception as e:\n            print(node, node.pk)\n            raise e\n    if rebuild:\n        rebuild_paths(node)\n\n\ndef rebuild_paths(obj):\n    \"\"\"\n    Rebuild all CablePaths which traverse the specified node\n    \"\"\"\n    cable_paths = CablePath.objects.filter(path__contains=obj)\n\n    with transaction.atomic():\n        for cp in cable_paths:\n            invalidate_obj(cp.origin)\n            cp.delete()\n            # Prevent looping back to rebuild_paths during the atomic transaction.\n            create_cablepath(cp.origin, rebuild=False)\n\n\n#\n# Site/location/rack/device assignment\n#\n\n\n@receiver(post_save, sender=RackGroup)\ndef handle_rackgroup_site_location_change(instance, created, raw=False, **kwargs):\n    \"\"\"\n    Update child RackGroups, Racks, and PowerPanels if Site or Location assignment has changed.\n\n    We intentionally recurse through each child object instead of calling update() on the QuerySet\n    to ensure the proper change records get created for each.\n\n    Note that this is non-trivial for Location changes, since a LocationType that can contain RackGroups\n    may or may not be permitted to contain Racks or PowerPanels. If it's not permitted, rather than trying to search\n    through child locations to find the \"right\" one, the best we can do is simply to null out the location.\n    \"\"\"\n    if raw:\n        return\n    if not created:\n        if instance.location is not None:\n            descendants = instance.location.descendants(include_self=True)\n            content_types = instance.location.location_type.content_types.all()\n            rack_groups_permitted = ContentType.objects.get_for_model(RackGroup) in content_types\n            racks_permitted = ContentType.objects.get_for_model(Rack) in content_types\n            power_panels_permitted = ContentType.objects.get_for_model(PowerPanel) in content_types\n        else:\n            descendants = None\n            rack_groups_permitted = False\n            racks_permitted = False\n            power_panels_permitted = False\n\n        for rackgroup in instance.get_children():\n            changed = False\n            if rackgroup.site != instance.site:\n                rackgroup.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if rackgroup.location is not None and rackgroup.location not in descendants:\n                    rackgroup.location = instance.location if rack_groups_permitted else None\n                    changed = True\n            elif rackgroup.location is not None and rackgroup.location.base_site != instance.site:\n                rackgroup.location = None\n                changed = True\n\n            if changed:\n                rackgroup.save()\n\n        for rack in Rack.objects.filter(group=instance):\n            changed = False\n            if rack.site != instance.site:\n                rack.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if rack.location is not None and rack.location not in descendants:\n                    rack.location = instance.location if racks_permitted else None\n                    changed = True\n            elif rack.location is not None and rack.location.base_site != instance.site:\n                rack.location = None\n                changed = True\n\n            if changed:\n                rack.save()\n\n        for powerpanel in PowerPanel.objects.filter(rack_group=instance):\n            changed = False\n            if powerpanel.site != instance.site:\n                powerpanel.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if powerpanel.location is not None and powerpanel.location not in descendants:\n                    powerpanel.location = instance.location if power_panels_permitted else None\n                    changed = True\n            elif powerpanel.location is not None and powerpanel.location.base_site != instance.site:\n                powerpanel.location = None\n                changed = True\n\n            if changed:\n                powerpanel.save()\n\n\n@receiver(post_save, sender=Rack)\ndef handle_rack_site_location_change(instance, created, raw=False, **kwargs):\n    \"\"\"\n    Update child Devices if Site or Location assignment has changed.\n\n    Note that this is non-trivial for Location changes, since a LocationType that can contain Racks\n    may or may not be permitted to contain Devices. If it's not permitted, rather than trying to search\n    through child locations to find the \"right\" one, the best we can do is simply to null out the location.\n    \"\"\"\n    if raw:\n        return\n    if not created:\n        if instance.location is not None:\n            devices_permitted = (\n                ContentType.objects.get_for_model(Device) in instance.location.location_type.content_types.all()\n            )\n\n        for device in Device.objects.filter(rack=instance):\n            changed = False\n            if device.site != instance.site:\n                device.site = instance.site\n                changed = True\n\n            if instance.location is not None:\n                if device.location is not None and device.location != instance.location:\n                    device.location = instance.location if devices_permitted else None\n                    changed = True\n            elif device.location is not None and device.location.base_site != instance.site:\n                device.location = None\n                changed = True\n\n            if changed:\n                device.save()\n\n\n#\n# Device redundancy group\n#\n\n\n@receiver(pre_delete, sender=DeviceRedundancyGroup)\ndef clear_deviceredundancygroup_members(instance, **kwargs):\n    \"\"\"\n    When a DeviceRedundancyGroup is deleted, nullify the device_redundancy_group_priority field of its prior members.\n    \"\"\"\n    devices = Device.objects.filter(device_redundancy_group=instance.pk)\n    for device in devices:\n        device.device_redundancy_group_priority = None\n        device.save()\n\n\n#\n# Virtual chassis\n#\n\n\n@receiver(post_save, sender=VirtualChassis)\ndef assign_virtualchassis_master(instance, created, raw=False, **kwargs):\n    \"\"\"\n    When a VirtualChassis is created, automatically assign its master device (if any) to the VC.\n    \"\"\"\n    if raw:\n        return\n    if created and instance.master:\n        master = Device.objects.get(pk=instance.master.pk)\n        master.virtual_chassis = instance\n        if instance.master.vc_position is None:\n            master.vc_position = 1\n        master.save()\n\n\n@receiver(pre_delete, sender=VirtualChassis)\ndef clear_virtualchassis_members(instance, **kwargs):\n    \"\"\"\n    When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.\n    \"\"\"\n    devices = Device.objects.filter(virtual_chassis=instance.pk)\n    for device in devices:\n        device.vc_position = None\n        device.vc_priority = None\n        device.save()\n\n\n#\n# Cables\n#\n\n\n@receiver(post_save, sender=Cable)\ndef update_connected_endpoints(instance, created, raw=False, **kwargs):\n    \"\"\"\n    When a Cable is saved, check for and update its two connected endpoints\n    \"\"\"\n    logger = logging.getLogger(\"nautobot.dcim.cable\")\n    if raw:\n        logger.debug(f\"Skipping endpoint updates for imported cable {instance}\")\n        return\n\n    # Cache the Cable on its two termination points\n    if instance.termination_a.cable != instance:\n        logger.debug(f\"Updating termination A for cable {instance}\")\n        instance.termination_a.cable = instance\n        instance.termination_a._cable_peer = instance.termination_b\n        instance.termination_a.save()\n    if instance.termination_b.cable != instance:\n        logger.debug(f\"Updating termination B for cable {instance}\")\n        instance.termination_b.cable = instance\n        instance.termination_b._cable_peer = instance.termination_a\n        instance.termination_b.save()\n\n    # Create/update cable paths\n    if created:\n        for termination in (instance.termination_a, instance.termination_b):\n            if isinstance(termination, PathEndpoint):\n                create_cablepath(termination)\n            else:\n                rebuild_paths(termination)\n    elif instance.status != instance._orig_status:\n        # We currently don't support modifying either termination of an existing Cable. (This\n        # may change in the future.) However, we do need to capture status changes and update\n        # any CablePaths accordingly.\n        if instance.status != Cable.STATUS_CONNECTED:\n            CablePath.objects.filter(path__contains=instance).update(is_active=False)\n        else:\n            rebuild_paths(instance)\n\n\n@receiver(pre_delete, sender=Cable)\ndef nullify_connected_endpoints(instance, **kwargs):\n    \"\"\"\n    When a Cable is deleted, check for and update its two connected endpoints\n    \"\"\"\n    logger = logging.getLogger(\"nautobot.dcim.cable\")\n\n    # Disassociate the Cable from its termination points\n    if instance.termination_a is not None:\n        logger.debug(f\"Nullifying termination A for cable {instance}\")\n        instance.termination_a.cable = None\n        instance.termination_a._cable_peer = None\n        instance.termination_a.save()\n    if instance.termination_b is not None:\n        logger.debug(f\"Nullifying termination B for cable {instance}\")\n        instance.termination_b.cable = None\n        instance.termination_b._cable_peer = None\n        instance.termination_b.save()\n\n    # Delete and retrace any dependent cable paths\n    for cablepath in CablePath.objects.filter(path__contains=instance):\n        cp = CablePath.from_origin(cablepath.origin)\n        if cp:\n            CablePath.objects.filter(pk=cablepath.pk).update(\n                path=cp.path,\n                destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,\n                destination_id=cp.destination.pk if cp.destination else None,\n                is_active=cp.is_active,\n                is_split=cp.is_split,\n            )\n        else:\n            cablepath.delete()\n\n\n#\n# Interface tagged VLAMs\n#\n\n\n@receiver(m2m_changed, sender=Interface.tagged_vlans.through)\n@disable_for_loaddata\ndef prevent_adding_tagged_vlans_with_incorrect_mode_or_site(sender, instance, action, **kwargs):\n    if action != \"pre_add\":\n        return\n\n    validate_interface_tagged_vlans(instance, kwargs[\"model\"], kwargs[\"pk_set\"])\n", "path": "nautobot/dcim/signals.py"}]}