diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 38cfc88666b..eaf4cbd184f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -475,6 +475,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): default=None ) type = ChoiceField(choices=InterfaceTypeChoices) + bridge = NestedInterfaceTemplateSerializer(required=False, allow_null=True) poe_mode = ChoiceField( choices=InterfacePoEModeChoices, required=False, @@ -489,7 +490,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer): class Meta: model = InterfaceTemplate 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', ] diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 74e697dde2c..614a5613f90 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1048,15 +1048,24 @@ class Meta: class InterfaceTemplateForm(ModularComponentTemplateForm): + bridge = DynamicModelChoiceField( + queryset=InterfaceTemplate.objects.all(), + required=False, + query_params={ + 'devicetype_id': '$device_type', + 'moduletype_id': '$module_type', + } + ) + 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')) ) class Meta: model = InterfaceTemplate 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', ] diff --git a/netbox/dcim/migrations/0171_devicetype_add_bridge.py b/netbox/dcim/migrations/0171_devicetype_add_bridge.py new file mode 100644 index 00000000000..3e0700a7fb9 --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index be17627fba8..e2d1cb50dc0 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -350,6 +350,14 @@ class InterfaceTemplate(ModularComponentTemplateModel): default=False, 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( max_length=50, choices=InterfacePoEModeChoices, @@ -365,6 +373,19 @@ class InterfaceTemplate(ModularComponentTemplateModel): 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): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -385,6 +406,7 @@ def to_yaml(self): 'mgmt_only': self.mgmt_only, 'label': self.label, 'description': self.description, + 'bridge': self.bridge.name if self.bridge else None, 'poe_mode': self.poe_mode, 'poe_type': self.poe_type, } diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 7ce1a23885f..09b0513e4f7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -460,6 +460,20 @@ def get_absolute_url(self): return reverse('dcim:platform', args=[self.pk]) +def update_interface_bridges(device, interface_templates, module=None): + """ + Used for device and module instantiation. Iterates all InterfaceTemplates with a bridge assigned + and applies it to the actual interfaces. + """ + for interface_template in interface_templates.exclude(bridge=None): + interface = Interface.objects.get(device=device, name=interface_template.resolve_name(module=module)) + + if interface_template.bridge: + interface.bridge = Interface.objects.get(device=device, name=interface_template.bridge.resolve_name(module=module)) + interface.full_clean() + interface.save() + + class Device(PrimaryModel, ConfigContextModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, @@ -850,6 +864,8 @@ def save(self, *args, **kwargs): self._instantiate_components(self.device_type.devicebaytemplates.all()) # Disable bulk_create to accommodate MPTT self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False) + # Interface bridges have to be set after interface instantiation + update_interface_bridges(self, self.device_type.interfacetemplates.all()) # Update Site and Rack assignment for any child Devices devices = Device.objects.filter(parent_bay__device=self) @@ -1086,6 +1102,9 @@ def save(self, *args, **kwargs): update_fields=update_fields ) + # Interface bridges have to be set after interface instantiation + update_interface_bridges(self.device, self.module_type.interfacetemplates, self) + # # Virtual chassis diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 91a37fab317..0536e894000 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -187,7 +187,7 @@ class InterfaceTemplateTable(ComponentTemplateTable): class Meta(ComponentTemplateTable.Meta): 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"