Просмотр исходного кода

#7844: Allow installing modules via UI without replicating components

jeremystretch 4 лет назад
Родитель
Сommit
a2981870ce

+ 23 - 3
netbox/dcim/forms/models.py

@@ -655,7 +655,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
 class ModuleForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
-        required=False,
         initial_params={
             'modulebays': '$module_bay'
         }
@@ -670,7 +669,7 @@ class ModuleForm(NetBoxModelForm):
         queryset=Manufacturer.objects.all(),
         required=False,
         initial_params={
-            'device_types': '$device_type'
+            'module_types': '$module_type'
         }
     )
     module_type = DynamicModelChoiceField(
@@ -684,13 +683,34 @@ class ModuleForm(NetBoxModelForm):
         queryset=Tag.objects.all(),
         required=False
     )
+    replicate_components = forms.BooleanField(
+        required=False,
+        initial=True,
+        help_text="Automatically populate components associated with this module type"
+    )
 
     class Meta:
         model = Module
         fields = [
-            'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags', 'comments',
+            'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags',
+            'replicate_components', 'comments',
         ]
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if self.instance.pk:
+            self.fields['replicate_components'].initial = False
+            self.fields['replicate_components'].disabled = True
+
+    def save(self, *args, **kwargs):
+
+        # If replicate_components is False, disable automatic component replication on the instance
+        if self.instance.pk or not self.cleaned_data['replicate_components']:
+            self.instance._disable_replication = True
+
+        return super().save(*args, **kwargs)
+
 
 class CableForm(TenancyForm, NetBoxModelForm):
     tags = DynamicModelMultipleChoiceField(

+ 13 - 0
netbox/dcim/forms/object_create.py

@@ -8,6 +8,7 @@ from utilities.forms import (
 )
 
 __all__ = (
+    'ComponentTemplateCreateForm',
     'DeviceComponentCreateForm',
     'DeviceTypeComponentCreateForm',
     'FrontPortCreateForm',
@@ -51,6 +52,18 @@ class DeviceTypeComponentCreateForm(ComponentCreateForm):
     field_order = ('device_type', 'name_pattern', 'label_pattern')
 
 
+class ComponentTemplateCreateForm(ComponentCreateForm):
+    device_type = DynamicModelChoiceField(
+        queryset=DeviceType.objects.all(),
+        required=False
+    )
+    module_type = DynamicModelChoiceField(
+        queryset=ModuleType.objects.all(),
+        required=False
+    )
+    field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
+
+
 class DeviceComponentCreateForm(ComponentCreateForm):
     device = DynamicModelChoiceField(
         queryset=Device.objects.all()

+ 4 - 3
netbox/dcim/models/devices.py

@@ -1054,12 +1054,13 @@ class Module(NetBoxModel, ConfigContextModel):
         return reverse('dcim:module', args=[self.pk])
 
     def save(self, *args, **kwargs):
-        is_new = not bool(self.pk)
+        is_new = self.pk is None
 
         super().save(*args, **kwargs)
 
-        # If this is a new Module, instantiate all its related components per the ModuleType definition
-        if is_new:
+        # If this is a new Module and component replication has not been disabled, instantiate all its
+        # related components per the ModuleType definition
+        if is_new and not getattr(self, '_disable_replication', False):
             ConsolePort.objects.bulk_create(
                 [x.instantiate(device=self.device, module=self) for x in self.module_type.consoleporttemplates.all()]
             )

+ 4 - 4
netbox/dcim/tables/template_code.py

@@ -326,11 +326,11 @@ DEVICEBAY_BUTTONS = """
 {% if perms.dcim.change_devicebay %}
     {% if record.installed_device %}
         <a href="{% url 'dcim:devicebay_depopulate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-minus-thick" aria-hidden="true" title="Remove device"></i>
+            <i class="mdi mdi-server-minus" aria-hidden="true" title="Remove device"></i>
         </a>
     {% else %}
         <a href="{% url 'dcim:devicebay_populate' pk=record.pk %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-success btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true" title="Install device"></i>
+            <i class="mdi mdi-server-plus" aria-hidden="true" title="Install device"></i>
         </a>
     {% endif %}
 {% endif %}
@@ -340,11 +340,11 @@ MODULEBAY_BUTTONS = """
 {% if perms.dcim.add_module %}
     {% if record.installed_module %}
         <a href="{% url 'dcim:module_delete' pk=record.installed_module.pk %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-minus-thick" aria-hidden="true" title="Remove module"></i>
+            <i class="mdi mdi-server-minus" aria-hidden="true" title="Remove module"></i>
         </a>
     {% else %}
         <a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true" title="Install module"></i>
+            <i class="mdi mdi-server-plus" aria-hidden="true" title="Install module"></i>
         </a>
     {% endif %}
 {% endif %}

+ 37 - 1
netbox/dcim/tests/test_views.py

@@ -13,7 +13,7 @@ from dcim.constants import *
 from dcim.models import *
 from ipam.models import ASN, RIR, VLAN, VRF
 from tenancy.models import Tenant
-from utilities.testing import ViewTestCases, create_tags, create_test_device
+from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
 from wireless.models import WirelessLAN
 
 
@@ -1770,6 +1770,7 @@ class ModuleTestCase(
     # bulk creation (need to specify module bays)
     ViewTestCases.GetObjectViewTestCase,
     ViewTestCases.GetObjectChangelogViewTestCase,
+    ViewTestCases.CreateObjectViewTestCase,
     ViewTestCases.EditObjectViewTestCase,
     ViewTestCases.DeleteObjectViewTestCase,
     ViewTestCases.ListObjectsViewTestCase,
@@ -1799,9 +1800,11 @@ class ModuleTestCase(
             ModuleBay(device=devices[0], name='Module Bay 1'),
             ModuleBay(device=devices[0], name='Module Bay 2'),
             ModuleBay(device=devices[0], name='Module Bay 3'),
+            ModuleBay(device=devices[0], name='Module Bay 4'),
             ModuleBay(device=devices[1], name='Module Bay 1'),
             ModuleBay(device=devices[1], name='Module Bay 2'),
             ModuleBay(device=devices[1], name='Module Bay 3'),
+            ModuleBay(device=devices[1], name='Module Bay 4'),
         )
         ModuleBay.objects.bulk_create(module_bays)
 
@@ -1833,6 +1836,39 @@ class ModuleTestCase(
             "Device 2,Module Bay 3,Module Type 3,C,C",
         )
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_module_component_replication(self):
+        self.add_permissions('dcim.add_module')
+
+        # Add 5 InterfaceTemplates to a ModuleType
+        module_type = ModuleType.objects.first()
+        interface_templates = [
+            InterfaceTemplate(module_type=module_type, name=f'Interface {i}') for i in range(1, 6)
+        ]
+        InterfaceTemplate.objects.bulk_create(interface_templates)
+
+        form_data = self.form_data.copy()
+        device = Device.objects.get(pk=form_data['device'])
+
+        # Create a module *without* replicating components
+        form_data['replicate_components'] = False
+        request = {
+            'path': self._get_url('add'),
+            'data': post_data(form_data),
+        }
+        self.assertHttpStatus(self.client.post(**request), 302)
+        self.assertEqual(Interface.objects.filter(device=device).count(), 0)
+
+        # Create a second module (in the next bay) with replicated components
+        form_data['module_bay'] += 1
+        form_data['replicate_components'] = True
+        request = {
+            'path': self._get_url('add'),
+            'data': post_data(form_data),
+        }
+        self.assertHttpStatus(self.client.post(**request), 302)
+        self.assertEqual(Interface.objects.filter(device=device).count(), 5)
+
 
 class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = ConsolePort

+ 7 - 7
netbox/dcim/views.py

@@ -1063,7 +1063,7 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
 
 class ConsolePortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsolePortTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.ConsolePortTemplateForm
 
 
@@ -1097,7 +1097,7 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = ConsoleServerPortTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.ConsoleServerPortTemplateForm
 
 
@@ -1131,7 +1131,7 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerPortTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerPortTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.PowerPortTemplateForm
 
 
@@ -1165,7 +1165,7 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class PowerOutletTemplateCreateView(generic.ComponentCreateView):
     queryset = PowerOutletTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.PowerOutletTemplateForm
 
 
@@ -1199,7 +1199,7 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class InterfaceTemplateCreateView(generic.ComponentCreateView):
     queryset = InterfaceTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.InterfaceTemplateForm
 
 
@@ -1275,7 +1275,7 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class RearPortTemplateCreateView(generic.ComponentCreateView):
     queryset = RearPortTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.RearPortTemplateForm
 
 
@@ -1377,7 +1377,7 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
 
 class InventoryItemTemplateCreateView(generic.ComponentCreateView):
     queryset = InventoryItemTemplate.objects.all()
-    form = forms.DeviceTypeComponentCreateForm
+    form = forms.ComponentTemplateCreateForm
     model_form = forms.InventoryItemTemplateForm
     template_name = 'dcim/inventoryitem_create.html'