From a74ae46f869ee757c893c1eeb525178b457db0ee Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 1 Mar 2023 16:05:41 +0100 Subject: [PATCH] Add bridge to InterfaceTemplate --- netbox/dcim/api/serializers.py | 3 ++- netbox/dcim/forms/model_forms.py | 13 +++++++++-- .../migrations/0171_devicetype_add_bridge.py | 19 +++++++++++++++ .../dcim/models/device_component_templates.py | 22 ++++++++++++++++++ netbox/dcim/models/devices.py | 23 +++++++++++++++++++ netbox/dcim/tables/devicetypes.py | 2 +- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 netbox/dcim/migrations/0171_devicetype_add_bridge.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 38cfc8866..eaf4cbd18 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 1c72a12dd..f1f392c99 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1020,15 +1020,24 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm): 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 000000000..3e0700a7f --- /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 be17627fb..e2d1cb50d 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 @@ class InterfaceTemplate(ModularComponentTemplateModel): '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 f4e0bad5d..6a74065f1 100644 --- a/netbox/dcim/models/devices.py +++ b/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." }) + 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): """ 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()) # 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 + self._update_interface_bridges(self.device_type.interfacetemplates.all()) # Update Site and Rack assignment for any child Devices 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})." ) + 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): is_new = self.pk is None @@ -1090,6 +1110,9 @@ class Module(PrimaryModel, ConfigContextModel): update_fields=update_fields ) + # Interface bridges have to be set after interface instantiation + self._update_interface_bridges(self.module_type.interfacetemplates, self) + # # Virtual chassis diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 91a37fab3..0536e8940 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"