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

Add bridge to InterfaceTemplate

kkthxbye-code 3 лет назад
Родитель
Сommit
a74ae46f86

+ 2 - 1
netbox/dcim/api/serializers.py

@@ -475,6 +475,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
         default=None
         default=None
     )
     )
     type = ChoiceField(choices=InterfaceTypeChoices)
     type = ChoiceField(choices=InterfaceTypeChoices)
+    bridge = NestedInterfaceTemplateSerializer(required=False, allow_null=True)
     poe_mode = ChoiceField(
     poe_mode = ChoiceField(
         choices=InterfacePoEModeChoices,
         choices=InterfacePoEModeChoices,
         required=False,
         required=False,
@@ -489,7 +490,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
         fields = [
         fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description',
+            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'bridge', 'enabled', 'mgmt_only', 'description',
             'poe_mode', 'poe_type', 'created', 'last_updated',
             'poe_mode', 'poe_type', 'created', 'last_updated',
         ]
         ]
 
 

+ 11 - 2
netbox/dcim/forms/model_forms.py

@@ -1020,15 +1020,24 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
 
 
 
 
 class InterfaceTemplateForm(ModularComponentTemplateForm):
 class InterfaceTemplateForm(ModularComponentTemplateForm):
+    bridge = DynamicModelChoiceField(
+        queryset=InterfaceTemplate.objects.all(),
+        required=False,
+        query_params={
+            'devicetype_id': '$device_type',
+            'moduletype_id': '$module_type',
+        }
+    )
+
     fieldsets = (
     fieldsets = (
-        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')),
+        (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
         ('PoE', ('poe_mode', 'poe_type'))
         ('PoE', ('poe_mode', 'poe_type'))
     )
     )
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type',
+            'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge',
         ]
         ]
 
 
 
 

+ 19 - 0
netbox/dcim/migrations/0171_devicetype_add_bridge.py

@@ -0,0 +1,19 @@
+# Generated by Django 4.1.6 on 2023-03-01 13:42
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0170_configtemplate'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='interfacetemplate',
+            name='bridge',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interfacetemplate'),
+        ),
+    ]

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

@@ -350,6 +350,14 @@ class InterfaceTemplate(ModularComponentTemplateModel):
         default=False,
         default=False,
         verbose_name='Management only'
         verbose_name='Management only'
     )
     )
+    bridge = models.ForeignKey(
+        to='self',
+        on_delete=models.SET_NULL,
+        related_name='bridge_interfaces',
+        null=True,
+        blank=True,
+        verbose_name='Bridge interface'
+    )
     poe_mode = models.CharField(
     poe_mode = models.CharField(
         max_length=50,
         max_length=50,
         choices=InterfacePoEModeChoices,
         choices=InterfacePoEModeChoices,
@@ -365,6 +373,19 @@ class InterfaceTemplate(ModularComponentTemplateModel):
 
 
     component_model = Interface
     component_model = Interface
 
 
+    def clean(self):
+        super().clean()
+
+        if self.bridge:
+            if self.device_type and self.device_type != self.bridge.device_type:
+                raise ValidationError({
+                    'bridge': f"Bridge interface ({self.bridge}) must belong to the same device type"
+                })
+            if self.module_type and self.module_type != self.bridge.module_type:
+                raise ValidationError({
+                    'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type"
+                })
+
     def instantiate(self, **kwargs):
     def instantiate(self, **kwargs):
         return self.component_model(
         return self.component_model(
             name=self.resolve_name(kwargs.get('module')),
             name=self.resolve_name(kwargs.get('module')),
@@ -385,6 +406,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
             'mgmt_only': self.mgmt_only,
             'mgmt_only': self.mgmt_only,
             'label': self.label,
             'label': self.label,
             'description': self.description,
             'description': self.description,
+            'bridge': self.bridge.name if self.bridge else None,
             'poe_mode': self.poe_mode,
             'poe_mode': self.poe_mode,
             'poe_type': self.poe_type,
             'poe_type': self.poe_type,
         }
         }

+ 23 - 0
netbox/dcim/models/devices.py

@@ -802,6 +802,15 @@ class Device(PrimaryModel, ConfigContextModel):
                 'vc_position': "A device assigned to a virtual chassis must have its position defined."
                 'vc_position': "A device assigned to a virtual chassis must have its position defined."
             })
             })
 
 
+    def _update_interface_bridges(self, interface_templates, module=None):
+        for interface_template in interface_templates.exclude(bridge=None):
+            interface = Interface.objects.get(device=self, name=interface_template.resolve_name(module=module))
+
+            if interface_template.bridge:
+                interface.bridge = Interface.objects.get(device=self, name=interface_template.bridge.resolve_name(module=module))
+                interface.full_clean()
+                interface.save()
+
     def _instantiate_components(self, queryset, bulk_create=True):
     def _instantiate_components(self, queryset, bulk_create=True):
         """
         """
         Instantiate components for the device from the specified component templates.
         Instantiate components for the device from the specified component templates.
@@ -854,6 +863,8 @@ class Device(PrimaryModel, ConfigContextModel):
             self._instantiate_components(self.device_type.devicebaytemplates.all())
             self._instantiate_components(self.device_type.devicebaytemplates.all())
             # Disable bulk_create to accommodate MPTT
             # Disable bulk_create to accommodate MPTT
             self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
             self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
+            # Interface bridges have to be set after interface instantiation
+            self._update_interface_bridges(self.device_type.interfacetemplates.all())
 
 
         # Update Site and Rack assignment for any child Devices
         # Update Site and Rack assignment for any child Devices
         devices = Device.objects.filter(parent_bay__device=self)
         devices = Device.objects.filter(parent_bay__device=self)
@@ -1015,6 +1026,15 @@ class Module(PrimaryModel, ConfigContextModel):
                 f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
                 f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
             )
             )
 
 
+    def _update_interface_bridges(self, interface_templates, module=None):
+        for interface_template in interface_templates.exclude(bridge=None):
+            interface = Interface.objects.get(device=self.device, name=interface_template.resolve_name(module=module))
+
+            if interface_template.bridge:
+                interface.bridge = Interface.objects.get(device=self.device, name=interface_template.bridge.resolve_name(module=module))
+                interface.full_clean()
+                interface.save()
+
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         is_new = self.pk is None
         is_new = self.pk is None
 
 
@@ -1090,6 +1110,9 @@ class Module(PrimaryModel, ConfigContextModel):
                     update_fields=update_fields
                     update_fields=update_fields
                 )
                 )
 
 
+        # Interface bridges have to be set after interface instantiation
+        self._update_interface_bridges(self.module_type.interfacetemplates, self)
+
 
 
 #
 #
 # Virtual chassis
 # Virtual chassis

+ 1 - 1
netbox/dcim/tables/devicetypes.py

@@ -187,7 +187,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
 
 
     class Meta(ComponentTemplateTable.Meta):
     class Meta(ComponentTemplateTable.Meta):
         model = models.InterfaceTemplate
         model = models.InterfaceTemplate
-        fields = ('pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'poe_mode', 'poe_type', 'actions')
+        fields = ('pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type', 'actions')
         empty_text = "None"
         empty_text = "None"