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

#19731: Support module_bay_types in device/module type YAML import and export

Follow-up QA for the ModuleBayType feature added in #22648.

ModuleBayTemplate.to_yaml() omitted module_bay_types, and
ModuleBayTemplateImportForm (used by the DeviceType/ModuleType YAML
"Import Components" flow) didn't expose the field either, so bay-type
constraints could never be defined as part of a device type's YAML
definition -- only assigned by hand, one bay at a time, after import.

Add module_bay_types (by name) to the import form, scoped to the parent
device/module type's manufacturer (or global types) via clean_device_type/
clean_module_type, mirroring the existing scoping pattern used elsewhere in
this form for power_port/cooling_intake. Add it to to_yaml()'s output
symmetrically.
Brian Tiemann 1 неделя назад
Родитель
Сommit
157a30ecd7

+ 26 - 1
netbox/dcim/forms/object_import.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
@@ -213,13 +214,37 @@ class PortTemplateMappingImportForm(forms.ModelForm):
 
 
 class ModuleBayTemplateImportForm(forms.ModelForm):
+    module_bay_types = forms.ModelMultipleChoiceField(
+        label=_('Module bay types'),
+        queryset=ModuleBayType.objects.all(),
+        to_field_name='name',
+        required=False,
+    )
 
     class Meta:
         model = ModuleBayTemplate
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'position', 'description',
+            'device_type', 'module_type', 'name', 'label', 'position', 'description', 'module_bay_types',
         ]
 
+    def clean_device_type(self):
+        if device_type := self.cleaned_data['device_type']:
+            module_bay_types = self.fields['module_bay_types']
+            module_bay_types.queryset = module_bay_types.queryset.filter(
+                Q(manufacturer__isnull=True) | Q(manufacturer=device_type.manufacturer)
+            )
+
+        return device_type
+
+    def clean_module_type(self):
+        if module_type := self.cleaned_data['module_type']:
+            module_bay_types = self.fields['module_bay_types']
+            module_bay_types.queryset = module_bay_types.queryset.filter(
+                Q(manufacturer__isnull=True) | Q(manufacturer=module_type.manufacturer)
+            )
+
+        return module_type
+
 
 class DeviceBayTemplateImportForm(forms.ModelForm):
 

+ 1 - 0
netbox/dcim/models/device_component_templates.py

@@ -988,6 +988,7 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
             'position': self.position,
             'enabled': self.enabled,
             'description': self.description,
+            'module_bay_types': [t.name for t in self.module_bay_types.all()],
         }
 
 

+ 13 - 0
netbox/dcim/tests/test_models.py

@@ -182,6 +182,19 @@ class ModuleTypeTestCase(TestCase):
         module_type.refresh_from_db()
         self.assertEqual(module_type.interface_template_count, 1)
 
+    def test_module_bay_template_to_yaml_includes_module_bay_types(self):
+        """
+        ModuleBayTemplate.to_yaml() should export its assigned module bay types by name.
+        """
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
+        bay_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28')
+        module_bay_template = ModuleBayTemplate.objects.create(module_type=module_type, name='Module Bay 1')
+        module_bay_template.module_bay_types.set([bay_type])
+
+        data = module_bay_template.to_yaml()
+        self.assertEqual(data['module_bay_types'], ['SFP28'])
+
     def test_attributes(self):
         """
         ModuleType.attributes should normalize iterable values into strings for presentation.

+ 8 - 0
netbox/dcim/tests/test_views.py

@@ -998,6 +998,8 @@ port-mappings:
     rear_port: Rear Port 3
 module-bays:
   - name: Module Bay 1
+    module_bay_types:
+      - SFP28
   - name: Module Bay 2
   - name: Module Bay 3
 device-bays:
@@ -1018,6 +1020,7 @@ inventory-items:
         manufacturer.save()
         platform = Platform(name='Platform', slug='test-platform', manufacturer=manufacturer)
         platform.save()
+        ModuleBayType.objects.create(name='SFP28', slug='sfp28')
 
         # Add all required permissions to the test user
         self.add_permissions(
@@ -1126,6 +1129,7 @@ inventory-items:
         self.assertEqual(device_type.modulebaytemplates.count(), 3)
         mb1 = ModuleBayTemplate.objects.first()
         self.assertEqual(mb1.name, 'Module Bay 1')
+        self.assertEqual(list(mb1.module_bay_types.values_list('name', flat=True)), ['SFP28'])
 
         self.assertEqual(device_type.devicebaytemplates.count(), 3)
         db1 = DeviceBayTemplate.objects.first()
@@ -1648,6 +1652,8 @@ port-mappings:
 module-bays:
   - name: Module Bay 1
     position: 1
+    module_bay_types:
+      - SFP28
   - name: Module Bay 2
     position: 2
   - name: Module Bay 3
@@ -1657,6 +1663,7 @@ module-bays:
         # Create the manufacturer
         manufacturer = Manufacturer(name='Generic', slug='generic')
         manufacturer.save()
+        ModuleBayType.objects.create(name='SFP28', slug='sfp28')
 
         # Add all required permissions to the test user
         self.add_permissions(
@@ -1752,6 +1759,7 @@ module-bays:
         mb1 = ModuleBayTemplate.objects.first()
         self.assertEqual(mb1.name, 'Module Bay 1')
         self.assertEqual(mb1.position, '1')
+        self.assertEqual(list(mb1.module_bay_types.values_list('name', flat=True)), ['SFP28'])
 
     @override_settings(STREAMING_EXPORTS=True)
     def test_export_objects(self):