Repository: nautobot/nautobot

Issue:
Unable to re-name Racks
<!--
    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): v1.5.23
* Python version: 3.10
* Database platform, version: Postgres
* Middleware(s):

<!--
    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. Go to dcim/racks/
2. select any rack to edit
3. change the name of the rack

<!-- What did you expect to happen? -->
### Expected Behavior
Page would reload and name would be reflected. 

<!-- What happened instead? -->
### Observed Behavior
When submitting an error shows up below the `name` field:
```
Device with `name` in ['2.5 960GB SSD (PCIE Gen3) Samsung', '40GBE Active Fiber Cable - Aqua - 03M', '40GBE Active Fiber Cable - Aqua - 05M', ... '2.5 960GB SSD  Intel'] and site=A52Plus already exists.
```


From [Slack Conversation with Glenn M.](https://networktocode.slack.com/archives/C01NWPK6WHL/p1689953425513139?thread_ts=1689951178.929749&cid=C01NWPK6WHL) it is suspected that #4025 may have introduced this.


Discussion:
I'm unable to reproduce this issue on demo.nautobot.com at present - I can successfully rename a Rack that contains Devices with no error. Can you provide more specific data that needs to be present to trigger the issue?
I think I've been able to reproduce this symptom by first cloning a device from the Rack in question into another Site (same device name, different site). Would still be good to confirm whether there are other scenarios that introduce this issue.

Resolving patch:
diff --git a/nautobot/dcim/forms.py b/nautobot/dcim/forms.py
--- a/nautobot/dcim/forms.py
+++ b/nautobot/dcim/forms.py
@@ -5,7 +5,7 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.db.models import Count, Q
+from django.db.models import Q
 from django.utils.safestring import mark_safe
 from netaddr import EUI
 from netaddr.core import AddrFormatError
@@ -696,21 +696,21 @@ def clean(self):
         cleaned_data = self.cleaned_data
         site = cleaned_data.get("site")
 
-        if self.instance:
+        if self.instance and self.instance.present_in_database and site != self.instance.site:
             # If the site is changed, the rack post save signal attempts to update the rack devices,
-            # which may result in an Exception.
-            # To avoid an unhandled exception in signal, catch this error here.
-            duplicate_devices_names = (
-                Device.objects.values_list("name", flat=True)
-                .annotate(name_count=Count("name"))
-                .filter(name_count__gt=1)
-            )
-            duplicate_devices = Device.objects.filter(site=site, name__in=list(duplicate_devices_names)).values_list(
-                "name", flat=True
-            )
+            # which may result in an Exception if the updated devices conflict with existing devices at this site.
+            # To avoid an unhandled exception in the signal, check for this scenario here.
+            duplicate_devices = set()
+            for device in self.instance.devices.all():
+                qs = Device.objects.exclude(pk=device.pk).filter(site=site, tenant=device.tenant, name=device.name)
+                if qs.exists():
+                    duplicate_devices.add(qs.first().name)
             if duplicate_devices:
                 raise ValidationError(
-                    {"site": f"Device with `name` in {list(duplicate_devices)} and site={site} already exists."}
+                    {
+                        "site": f"Device(s) {sorted(duplicate_devices)} already exist in site {site} and "
+                        "would conflict with same-named devices in this rack."
+                    }
                 )
         return cleaned_data
 


Test patch:
diff --git a/nautobot/dcim/tests/test_forms.py b/nautobot/dcim/tests/test_forms.py
--- a/nautobot/dcim/tests/test_forms.py
+++ b/nautobot/dcim/tests/test_forms.py
@@ -444,5 +444,21 @@ def test_update_rack_site(self):
         form = RackForm(data=data, instance=racks[0])
         self.assertEqual(
             str(form.errors.as_data()["site"][0]),
-            str(["Device with `name` in ['device1'] and site=Site-2 already exists."]),
+            str(
+                [
+                    "Device(s) ['device1'] already exist in site Site-2 and "
+                    "would conflict with same-named devices in this rack."
+                ]
+            ),
         )
+
+        # Check for https://github.com/nautobot/nautobot/issues/4149
+        data = {
+            "name": "New name",
+            "site": racks[0].site.pk,
+            "status": racks[0].status.pk,
+            "u_height": 48,
+            "width": RackWidthChoices.WIDTH_19IN,
+        }
+        form = RackForm(data=data, instance=racks[0])
+        self.assertTrue(form.is_valid())