Repository: netbox-community/netbox

Issue:
Module interface creation error
### NetBox version

v3.2.0-beta2

### Python version

3.8

### Steps to Reproduce

Reproducable in [beta demo site](https://beta-demo.netbox.dev/).

1. Navigate to router [dmi01-akron-rtr01](https://beta-demo.netbox.dev/dcim/devices/1/interfaces/);
2. Create interfaces "GigabitEthernet2/1/[0-3]" of type "1000BASE-T (1GE)";
3. Add components -> Module Bay, Create "Bay 2" with position "2";
4. Create a new [Module Type](https://beta-demo.netbox.dev/dcim/module-types/add/) -> "4PORT 1GE" and add interfaces "GigabitEthernet{module}/1/[0-3];
5. Install the created module type on the router module bay. (Replicate components must be ticked)

### Expected Behavior

![image](https://user-images.githubusercontent.com/91552302/161026543-2ae64973-def4-4548-872a-79c5520d15a7.png)


### Observed Behavior

Since the interfaces with that name already existed, netbox throws an exception. I believe that this should be the expected behaviour but it should generate a toast error instead.


Discussion:

Resolving patch:
diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py
--- a/netbox/dcim/forms/models.py
+++ b/netbox/dcim/forms/models.py
@@ -675,6 +675,56 @@ def save(self, *args, **kwargs):
 
         return super().save(*args, **kwargs)
 
+    def clean(self):
+        super().clean()
+
+        replicate_components = self.cleaned_data.get("replicate_components")
+        adopt_components = self.cleaned_data.get("adopt_components")
+        device = self.cleaned_data['device']
+        module_type = self.cleaned_data['module_type']
+        module_bay = self.cleaned_data['module_bay']
+
+        # Bail out if we are not installing a new module or if we are not replicating components
+        if self.instance.pk or not replicate_components:
+            return
+
+        for templates, component_attribute in [
+                ("consoleporttemplates", "consoleports"),
+                ("consoleserverporttemplates", "consoleserverports"),
+                ("interfacetemplates", "interfaces"),
+                ("powerporttemplates", "powerports"),
+                ("poweroutlettemplates", "poweroutlets"),
+                ("rearporttemplates", "rearports"),
+                ("frontporttemplates", "frontports")
+        ]:
+            # Prefetch installed components
+            installed_components = {
+                component.name: component for component in getattr(device, component_attribute).all()
+            }
+
+            # Get the templates for the module type.
+            for template in getattr(module_type, templates).all():
+                # Installing modules with placeholders require that the bay has a position value
+                if '{module}' in template.name and not module_bay.position:
+                    raise forms.ValidationError(
+                        "Cannot install module with placeholder values in a module bay with no position defined"
+                    )
+
+                resolved_name = template.name.replace('{module}', module_bay.position)
+                existing_item = installed_components.get(resolved_name)
+
+                # It is not possible to adopt components already belonging to a module
+                if adopt_components and existing_item and existing_item.module:
+                    raise forms.ValidationError(
+                        f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs to a module"
+                    )
+
+                # If we are not adopting components we error if the component exists
+                if not adopt_components and resolved_name in installed_components:
+                    raise forms.ValidationError(
+                        f"{template.component_model.__name__} - {resolved_name} already exists"
+                    )
+
 
 class CableForm(TenancyForm, NetBoxModelForm):
 


Test patch: