Repository: netbox-community/netbox
Pull request: Fixes: #14044 - Allow regex renaming of unnamed devices
### Fixes: #14044 - Allow regex renaming of unnamed devices

* Allow regex renaming of unnamed devices (already allowed actually)
* Catch errors relating to unnamed devices or integrity errors as a result of the rename process

Note: I removed the ^$ requirement as, in my view, as long as there is a valid regex and the regex results in a unique set, we don't need to enforce the ^$ requirement.  For example: `^` or `.*` or `.` would all be equally as valid.

The main changes here catch both IntegrityError's from the DB and a validation error to ensure regex is used.

Problem statement:
NoneType AttributeError when Bulk Renaming Nameless Devices
### NetBox version

3.6.3

### Python version

3.8

### Steps to Reproduce

1. Create devices without a name
2. Select and Bulk Rename those devices
3. **Disable** Regex
4. Attempt a rename operation

Of note, I am also unable to rename nameless devices with Regex on, but no error is observed.

### Expected Behavior

The renaming of bulk devices operation should be successful.

### Observed Behavior

```python
<class 'AttributeError'>

'NoneType' object has no attribute 'replace'

Python version: 3.8.10
NetBox version: 3.6.3
Plugins: 
  netbox_demo: 0.3.1
```

Hints from issue discussion:
The error is caused by this line, which assumes the device name will be a string (as opposed to None):

https://github.com/netbox-community/netbox/blob/51ef4fb920acd689755af6ea9f7765b341ebfcec/netbox/netbox/views/generic/bulk_views.py#L699
> Select and Bulk Rename those devices

I'm not clear on what behavior you expect. The bulk rename function requires a "find" value, which cannot match a null name.

@arthanson's PR #14065 will effectively ignore unnamed devices, avoiding the exception. The only way I see to enable renaming unnamed devices would be to blindly apply the "replace" value, which seems very dangerous.
> I'm not clear on what behavior you expect.

The behavior I'm expecting is to be able to bulk rename unnamed devices.

Perhaps an extension to this form is suitable, e.g. _a checkbox to force-match against unnamed devices_
> The behavior I'm expecting is to be able to bulk rename unnamed devices.

Renaming them all to the same name?
> Renaming them all to the same name?

In this instance yes - the use case was a stack of devices were created as placeholders to fill out a rack diagram. Think things like Cable Organizer Trays or Patch Panels, they don't need unique names.
> The only way I see to enable renaming unnamed devices would be to blindly apply the "replace" value, which seems very dangerous.

I don't think our data model will allow this, as name would not be unique.
I believe this was discussed during a maintainers meeting, the consensus is this:

* We can permit devices to be renamed with RegEx, provided the RegEx forms a unique name or otherwise passes the uniqueness check
* The RegEx must specify only the start `^` and end `$` regex and nothing else (regex would be `^$`)

Commit message: Fixes: #14044 - Allow regex renaming of unnamed devices

* Allow regex renaming of unnamed devices (already allowed actually)
* Catch errors relating to unnamed devices or integrity errors as a result of the rename process

Patch to review:
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index b18ecdd5b64..a89bafc1a19 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -2122,6 +2122,17 @@ class DeviceBulkRenameView(generic.BulkRenameView):
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
 
+    def _rename_objects(self, form, selected_objects):
+        # Check devices for any unnamed devices and enforce requirements on the renaming of devices
+        for obj in selected_objects:
+            if not form.cleaned_data['use_regex'] and not obj.name:
+                from django.core.exceptions import ValidationError
+                raise ValidationError({
+                    'use_regex': 'You must use regex to rename a unnamed device and must pass device uniqueness checks'
+                })
+
+        super()._rename_objects(form, selected_objects)
+
 
 @register_model_view(Device, 'contacts')
 class DeviceContactsView(ObjectContactsView):
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index bdc9a7152a5..8270507b5fb 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -776,6 +776,14 @@ def post(self, request):
                             )
                             return redirect(self.get_return_url(request))
 
+                except IntegrityError as e:
+                    messages.error(self.request, ", ".join(e.args))
+                    clear_events.send(sender=self)
+
+                except ValidationError as e:
+                    messages.error(self.request, ", ".join(e.messages))
+                    clear_events.send(sender=self)
+
                 except (AbortRequest, PermissionsViolation) as e:
                     logger.debug(e.message)
                     form.add_error(None, e.message)


Code review comments:
diff_hunk: @@ -2122,6 +2122,17 @@ class DeviceBulkRenameView(generic.BulkRenameView):
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
 
+    def _rename_objects(self, form, selected_objects):
path: netbox/dcim/views.py
text: @user1:
Private view methods should not be overridden. Additionally, it's not clear why this logic would apply to devices but not other objects being renamed in bulk.