Merge pull request #7622 from netbox-community/6346-interface-bridge

Closes #6346: Bridge group support
This commit is contained in:
Jeremy Stretch 2021-10-22 09:37:28 -04:00 committed by GitHub
commit dbe2f8a6f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 308 additions and 123 deletions

View File

@ -7,6 +7,8 @@
* The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination. * The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination.
### New Features
#### Contacts ([#1344](https://github.com/netbox-community/netbox/issues/1344)) #### Contacts ([#1344](https://github.com/netbox-community/netbox/issues/1344))
A set of new models for tracking contact information has been introduced within the tenancy app. Users may now create individual contact objects to be associated with various models within NetBox. Each contact has a name, title, email address, etc. Contacts can be arranged in hierarchical groups for ease of management. A set of new models for tracking contact information has been introduced within the tenancy app. Users may now create individual contact objects to be associated with various models within NetBox. Each contact has a name, title, email address, etc. Contacts can be arranged in hierarchical groups for ease of management.
@ -26,6 +28,12 @@ Both types of connection include SSID and authentication attributes. Additionall
* Channel - A predefined channel within a standardized band * Channel - A predefined channel within a standardized band
* Channel frequency & width - Customizable channel attributes (e.g. for licensed bands) * Channel frequency & width - Customizable channel attributes (e.g. for licensed bands)
#### Interface Bridging ([#6346](https://github.com/netbox-community/netbox/issues/6346))
A `bridge` field has been added to the interface model for devices and virtual machines. This can be set to reference another interface on the same parent device/VM to indicate a direct layer two bridging adjacency.
Multiple interfaces can be bridged to a single virtual interface to effect a bridge group. Alternatively, two physical interfaces can be bridged to one another, to effect an internal cross-connect.
### Enhancements ### Enhancements
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
@ -73,6 +81,9 @@ Both types of connection include SSID and authentication attributes. Additionall
* dcim.DeviceType * dcim.DeviceType
* Added `airflow` field * Added `airflow` field
* dcim.Interface * dcim.Interface
* Added `bridge` field
* Added `wwn` field * Added `wwn` field
* dcim.Location * dcim.Location
* Added `tenant` field * Added `tenant` field
* virtualization.VMInterface
* Added `bridge` field

View File

@ -605,6 +605,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True) parent = NestedInterfaceSerializer(required=False, allow_null=True)
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True) rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
@ -622,8 +623,8 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu',
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'link_peer',
'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',

View File

@ -544,7 +544,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
class InterfaceViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'parent', 'lag', '_path__destination', 'cable', '_link_peer', 'ip_addresses', 'tags' 'device', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer', 'ip_addresses', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet

View File

@ -975,6 +975,11 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Parent interface (ID)', label='Parent interface (ID)',
) )
bridge_id = django_filters.ModelMultipleChoiceFilter(
field_name='bridge',
queryset=Interface.objects.all(),
label='Bridged interface (ID)',
)
lag_id = django_filters.ModelMultipleChoiceFilter( lag_id = django_filters.ModelMultipleChoiceFilter(
field_name='lag', field_name='lag',
queryset=Interface.objects.all(), queryset=Interface.objects.all(),

View File

@ -939,8 +939,8 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm( class InterfaceBulkEditForm(
form_from_model(Interface, [ form_from_model(Interface, [
'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
]), ]),
BootstrapMixin, BootstrapMixin,
AddRemoveTagsForm, AddRemoveTagsForm,
@ -964,6 +964,10 @@ class InterfaceBulkEditForm(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False required=False
) )
bridge = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False
)
lag = DynamicModelChoiceField( lag = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -991,7 +995,7 @@ class InterfaceBulkEditForm(
class Meta: class Meta:
nullable_fields = [ nullable_fields = [
'label', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', 'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans',
] ]
@ -1000,8 +1004,9 @@ class InterfaceBulkEditForm(
if 'device' in self.initial: if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first() device = Device.objects.filter(pk=self.initial['device']).first()
# Restrict parent/LAG interface assignment by device # Restrict parent/bridge/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('device_id', device.pk) self.fields['parent'].widget.add_query_param('device_id', device.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.pk)
self.fields['lag'].widget.add_query_param('device_id', device.pk) self.fields['lag'].widget.add_query_param('device_id', device.pk)
# Limit VLAN choices by device # Limit VLAN choices by device
@ -1029,6 +1034,8 @@ class InterfaceBulkEditForm(
self.fields['parent'].choices = () self.fields['parent'].choices = ()
self.fields['parent'].widget.attrs['disabled'] = True self.fields['parent'].widget.attrs['disabled'] = True
self.fields['bridge'].choices = ()
self.fields['bridge'].widget.attrs['disabled'] = True
self.fields['lag'].choices = () self.fields['lag'].choices = ()
self.fields['lag'].widget.attrs['disabled'] = True self.fields['lag'].widget.attrs['disabled'] = True

View File

@ -570,6 +570,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
to_field_name='name', to_field_name='name',
help_text='Parent interface' help_text='Parent interface'
) )
bridge = CSVModelChoiceField(
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text='Bridged interface'
)
lag = CSVModelChoiceField( lag = CSVModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -594,39 +600,11 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = Interface model = Interface
fields = ( fields = (
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn', 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'rf_channel_width',
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device (or virtual chassis)
device = None
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
pass
if device and device.virtual_chassis:
self.fields['lag'].queryset = Interface.objects.filter(
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis),
type=InterfaceTypeChoices.TYPE_LAG
)
self.fields['parent'].queryset = Interface.objects.filter(
Q(device=device) | Q(device__virtual_chassis=device.virtual_chassis)
)
elif device:
self.fields['lag'].queryset = Interface.objects.filter(
device=device,
type=InterfaceTypeChoices.TYPE_LAG
)
self.fields['parent'].queryset = Interface.objects.filter(device=device)
else:
self.fields['lag'].queryset = Interface.objects.none()
self.fields['parent'].queryset = Interface.objects.none()
def clean_enabled(self): def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data # Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data: if 'enabled' not in self.data:

View File

@ -1093,6 +1093,11 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
required=False, required=False,
label='Parent interface' label='Parent interface'
) )
bridge = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Bridged interface'
)
lag = DynamicModelChoiceField( lag = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -1143,8 +1148,8 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags', 'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
] ]
widgets = { widgets = {
@ -1168,13 +1173,14 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
# Restrict parent/LAG interface assignment by device/VC # Restrict parent/bridge/LAG interface assignment by device/VC
self.fields['parent'].widget.add_query_param('device_id', device.pk) self.fields['parent'].widget.add_query_param('device_id', device.pk)
if device.virtual_chassis and device.virtual_chassis.master: self.fields['bridge'].widget.add_query_param('device_id', device.pk)
# Get available LAG interfaces by VirtualChassis master
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
else:
self.fields['lag'].widget.add_query_param('device_id', device.pk) self.fields['lag'].widget.add_query_param('device_id', device.pk)
if device.virtual_chassis and device.virtual_chassis.master:
self.fields['parent'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
# Limit VLAN choices by device # Limit VLAN choices by device
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk) self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)

View File

@ -446,6 +446,13 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
'device_id': '$device', 'device_id': '$device',
} }
) )
bridge = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
lag = DynamicModelChoiceField( lag = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -497,7 +504,7 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
required=False required=False
) )
field_order = ( field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address',
'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'description', 'mgmt_only', 'mark_connected', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags' 'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
) )

View File

@ -1,17 +0,0 @@
import dcim.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0133_port_colors'),
]
operations = [
migrations.AddField(
model_name='interface',
name='wwn',
field=dcim.fields.WWNField(blank=True, null=True),
),
]

View File

@ -0,0 +1,23 @@
import dcim.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0133_port_colors'),
]
operations = [
migrations.AddField(
model_name='interface',
name='wwn',
field=dcim.fields.WWNField(blank=True, null=True),
),
migrations.AddField(
model_name='interface',
name='bridge',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='dcim.interface'),
),
]

View File

@ -6,7 +6,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tenancy', '0002_tenant_ordering'), ('tenancy', '0002_tenant_ordering'),
('dcim', '0134_interface_wwn'), ('dcim', '0134_interface_wwn_bridge'),
] ]
operations = [ operations = [

View File

@ -462,6 +462,22 @@ class BaseInterface(models.Model):
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
blank=True blank=True
) )
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
)
bridge = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='bridge_interfaces',
null=True,
blank=True,
verbose_name='Bridge interface'
)
class Meta: class Meta:
abstract = True abstract = True
@ -495,14 +511,6 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
max_length=100, max_length=100,
blank=True blank=True
) )
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
)
lag = models.ForeignKey( lag = models.ForeignKey(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -586,7 +594,7 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
related_query_name='interface' related_query_name='interface'
) )
clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only'] clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
class Meta: class Meta:
ordering = ('device', CollateAsChar('_name')) ordering = ('device', CollateAsChar('_name'))
@ -610,6 +618,16 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected." 'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
}) })
# Parent validation
# An interface cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
# A physical interface cannot have a parent interface
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
# An interface's parent must belong to the same device or virtual chassis # An interface's parent must belong to the same device or virtual chassis
if self.parent and self.parent.device != self.device: if self.parent and self.parent.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
@ -623,13 +641,34 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
f"is not part of virtual chassis {self.device.virtual_chassis}." f"is not part of virtual chassis {self.device.virtual_chassis}."
}) })
# An interface cannot be its own parent # Bridge validation
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
# A physical interface cannot have a parent interface # An interface cannot be bridged to itself
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None: if self.pk and self.bridge_id == self.pk:
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."}) raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
# A bridged interface belong to the same device or virtual chassis
if self.bridge and self.bridge.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different device "
f"({self.bridge.device})."
})
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to {self.bridge.device}, which "
f"is not part of virtual chassis {self.device.virtual_chassis}."
})
# LAG validation
# A virtual interface cannot have a parent LAG
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
# A LAG interface cannot be its own parent
if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
# An interface's LAG must belong to the same device or virtual chassis # An interface's LAG must belong to the same device or virtual chassis
if self.lag and self.lag.device != self.device: if self.lag and self.lag.device != self.device:
@ -643,13 +682,7 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
f"of virtual chassis {self.device.virtual_chassis}." f"of virtual chassis {self.device.virtual_chassis}."
}) })
# A virtual interface cannot have a parent LAG # Wireless validation
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
# A LAG interface cannot be its own parent
if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
# RF role & channel may only be set for wireless interfaces # RF role & channel may only be set for wireless interfaces
if self.rf_role and not self.is_wireless: if self.rf_role and not self.is_wireless:
@ -679,11 +712,13 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
elif self.rf_channel: elif self.rf_channel:
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width') self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
# VLAN validation
# Validate untagged VLAN # Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
raise ValidationError({ raise ValidationError({
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " 'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
"device, or it must be global".format(self.untagged_vlan) f"interface's parent device, or it must be global."
}) })
@property @property

View File

@ -521,8 +521,10 @@ class DeviceInterfaceTable(InterfaceTable):
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
parent = tables.Column( parent = tables.Column(
linkify=True, linkify=True
verbose_name='Parent' )
bridge = tables.Column(
linkify=True
) )
lag = tables.Column( lag = tables.Column(
linkify=True, linkify=True,
@ -537,10 +539,10 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode',
'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable',
'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses',
'tagged_vlans', 'actions', 'untagged_vlan', 'tagged_vlans', 'actions',
) )
order_by = ('name',) order_by = ('name',)
default_columns = ( default_columns = (

View File

@ -1206,6 +1206,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'name': 'Interface 5', 'name': 'Interface 5',
'type': '1000base-t', 'type': '1000base-t',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'bridge': interfaces[0].pk,
'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk, 'untagged_vlan': vlans[2].pk,
}, },
@ -1214,7 +1215,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'name': 'Interface 6', 'name': 'Interface 6',
'type': 'virtual', 'type': 'virtual',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'parent': interfaces[0].pk, 'parent': interfaces[1].pk,
'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk, 'untagged_vlan': vlans[2].pk,
}, },

View File

@ -2125,6 +2125,19 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'parent_id': [parent_interface.pk]} params = {'parent_id': [parent_interface.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_bridge(self):
# Create bridged interfaces
bridge_interface = Interface.objects.first()
bridged_interfaces = (
Interface(device=bridge_interface.device, name='Bridged 1', bridge=bridge_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=bridge_interface.device, name='Bridged 2', bridge=bridge_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=bridge_interface.device, name='Bridged 3', bridge=bridge_interface, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(bridged_interfaces)
params = {'bridge_id': [bridge_interface.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_lag(self): def test_lag(self):
# Create LAG members # Create LAG members
device = Device.objects.first() device = Device.objects.first()

View File

@ -1581,6 +1581,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
Interface(device=device, name='Interface 2'), Interface(device=device, name='Interface 2'),
Interface(device=device, name='Interface 3'), Interface(device=device, name='Interface 3'),
Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG), Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG),
Interface(device=device, name='_BRIDGE', type=InterfaceTypeChoices.TYPE_VIRTUAL), # Must be ordered last
) )
Interface.objects.bulk_create(interfaces) Interface.objects.bulk_create(interfaces)
@ -1596,10 +1597,10 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.form_data = { cls.form_data = {
'device': device.pk, 'device': device.pk,
'virtual_machine': None,
'name': 'Interface X', 'name': 'Interface X',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False, 'enabled': False,
'bridge': interfaces[4].pk,
'lag': interfaces[3].pk, 'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'), 'mac_address': EUI('01:02:03:04:05:06'),
'wwn': EUI('01:02:03:04:05:06:07:08', version=64), 'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
@ -1617,6 +1618,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'name_pattern': 'Interface [4-6]', 'name_pattern': 'Interface [4-6]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False, 'enabled': False,
'bridge': interfaces[4].pk,
'lag': interfaces[3].pk, 'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'), 'mac_address': EUI('01:02:03:04:05:06'),
'wwn': EUI('01:02:03:04:05:06:07:08', version=64), 'wwn': EUI('01:02:03:04:05:06:07:08', version=64),

View File

@ -69,6 +69,16 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">Bridge</th>
<td>
{% if object.bridge %}
<a href="{{ object.bridge.get_absolute_url }}">{{ object.bridge }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">LAG</th> <th scope="row">LAG</th>
<td> <td>

View File

@ -18,6 +18,7 @@
{% render_field form.label %} {% render_field form.label %}
{% render_field form.type %} {% render_field form.type %}
{% render_field form.parent %} {% render_field form.parent %}
{% render_field form.bridge %}
{% render_field form.lag %} {% render_field form.lag %}
{% render_field form.mac_address %} {% render_field form.mac_address %}
{% render_field form.wwn %} {% render_field form.wwn %}

View File

@ -47,6 +47,16 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">Bridge</th>
<td>
{% if object.bridge %}
<a href="{{ object.bridge.get_absolute_url }}">{{ object.bridge }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>
<td>{{ object.description|placeholder }} </td> <td>{{ object.description|placeholder }} </td>

View File

@ -17,6 +17,7 @@
{% render_field form.name %} {% render_field form.name %}
{% render_field form.enabled %} {% render_field form.enabled %}
{% render_field form.parent %} {% render_field form.parent %}
{% render_field form.bridge %}
{% render_field form.mac_address %} {% render_field form.mac_address %}
{% render_field form.mtu %} {% render_field form.mtu %}
{% render_field form.description %} {% render_field form.description %}

View File

@ -107,6 +107,7 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
virtual_machine = NestedVirtualMachineSerializer() virtual_machine = NestedVirtualMachineSerializer()
parent = NestedVMInterfaceSerializer(required=False, allow_null=True) parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
@ -120,8 +121,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
class Meta: class Meta:
model = VMInterface model = VMInterface
fields = [ fields = [
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address',
'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
'count_ipaddresses', 'count_ipaddresses',
] ]

View File

@ -264,6 +264,11 @@ class VMInterfaceFilterSet(PrimaryModelFilterSet):
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
label='Parent interface (ID)', label='Parent interface (ID)',
) )
bridge_id = django_filters.ModelMultipleChoiceFilter(
field_name='bridge',
queryset=VMInterface.objects.all(),
label='Bridged interface (ID)',
)
mac_address = MultiValueMACAddressFilter( mac_address = MultiValueMACAddressFilter(
label='MAC address', label='MAC address',
) )

View File

@ -165,6 +165,10 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False required=False
) )
bridge = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False
)
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
@ -195,7 +199,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
class Meta: class Meta:
nullable_fields = [ nullable_fields = [
'parent', 'mtu', 'description', 'parent', 'bridge', 'mtu', 'description',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -203,8 +207,9 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
if 'virtual_machine' in self.initial: if 'virtual_machine' in self.initial:
vm_id = self.initial.get('virtual_machine') vm_id = self.initial.get('virtual_machine')
# Restrict parent interface assignment by VM # Restrict parent/bridge interface assignment by VM
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
# Limit VLAN choices by virtual machine # Limit VLAN choices by virtual machine
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
@ -231,6 +236,11 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
self.fields['parent'].choices = ()
self.fields['parent'].widget.attrs['disabled'] = True
self.fields['bridge'].choices = ()
self.fields['bridge'].widget.attrs['disabled'] = True
class VMInterfaceBulkRenameForm(BulkRenameForm): class VMInterfaceBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(

View File

@ -104,6 +104,18 @@ class VMInterfaceCSVForm(CustomFieldModelCSVForm):
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
to_field_name='name' to_field_name='name'
) )
parent = CSVModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
to_field_name='name',
help_text='Parent interface'
)
bridge = CSVModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
to_field_name='name',
help_text='Bridged interface'
)
mode = CSVChoiceField( mode = CSVChoiceField(
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
required=False, required=False,
@ -113,7 +125,7 @@ class VMInterfaceCSVForm(CustomFieldModelCSVForm):
class Meta: class Meta:
model = VMInterface model = VMInterface
fields = ( fields = (
'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
) )
def clean_enabled(self): def clean_enabled(self):

View File

@ -277,6 +277,11 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
required=False, required=False,
label='Parent interface' label='Parent interface'
) )
bridge = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
label='Bridged interface'
)
vlan_group = DynamicModelChoiceField( vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
@ -306,8 +311,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
class Meta: class Meta:
model = VMInterface model = VMInterface
fields = [ fields = [
'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags', 'untagged_vlan', 'tagged_vlans',
] ]
widgets = { widgets = {
'virtual_machine': forms.HiddenInput(), 'virtual_machine': forms.HiddenInput(),
@ -326,6 +331,7 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
# Restrict parent interface assignment by VM # Restrict parent interface assignment by VM
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
# Limit VLAN choices by virtual machine # Limit VLAN choices by virtual machine
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)

View File

@ -35,6 +35,13 @@ class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonFo
'virtual_machine_id': '$virtual_machine', 'virtual_machine_id': '$virtual_machine',
} }
) )
bridge = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
query_params={
'virtual_machine_id': '$virtual_machine',
}
)
mac_address = forms.CharField( mac_address = forms.CharField(
required=False, required=False,
label='MAC Address' label='MAC Address'
@ -61,7 +68,7 @@ class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonFo
required=False required=False
) )
field_order = ( field_order = (
'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode', 'virtual_machine', 'name_pattern', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', 'description', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags' 'untagged_vlan', 'tagged_vlans', 'tags'
) )

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-10-21 20:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0025_extend_tag_support'),
]
operations = [
migrations.AddField(
model_name='vminterface',
name='bridge',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bridge_interfaces', to='virtualization.vminterface'),
),
]

View File

@ -378,14 +378,6 @@ class VMInterface(PrimaryModel, BaseInterface):
max_length=200, max_length=200,
blank=True blank=True
) )
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
)
untagged_vlan = models.ForeignKey( untagged_vlan = models.ForeignKey(
to='ipam.VLAN', to='ipam.VLAN',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -423,6 +415,12 @@ class VMInterface(PrimaryModel, BaseInterface):
def clean(self): def clean(self):
super().clean() super().clean()
# Parent validation
# An interface cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
# An interface's parent must belong to the same virtual machine # An interface's parent must belong to the same virtual machine
if self.parent and self.parent.virtual_machine != self.virtual_machine: if self.parent and self.parent.virtual_machine != self.virtual_machine:
raise ValidationError({ raise ValidationError({
@ -430,15 +428,26 @@ class VMInterface(PrimaryModel, BaseInterface):
f"({self.parent.virtual_machine})." f"({self.parent.virtual_machine})."
}) })
# An interface cannot be its own parent # Bridge validation
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."}) # An interface cannot be bridged to itself
if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
# A bridged interface belong to the same virtual machine
if self.bridge and self.bridge.virtual_machine != self.virtual_machine:
raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different virtual machine "
f"({self.bridge.virtual_machine})."
})
# VLAN validation
# Validate untagged VLAN # Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]: if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
raise ValidationError({ raise ValidationError({
'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the " 'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
f"interface's parent virtual machine, or it must be global" f"interface's parent virtual machine, or it must be global."
}) })
def to_objectchange(self, action): def to_objectchange(self, action):

View File

@ -166,9 +166,6 @@ class VMInterfaceTable(BaseInterfaceTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
parent = tables.Column(
linkify=True
)
tags = TagColumn( tags = TagColumn(
url_name='virtualization:vminterface_list' url_name='virtualization:vminterface_list'
) )
@ -176,13 +173,19 @@ class VMInterfaceTable(BaseInterfaceTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'name', 'virtual_machine', 'enabled', 'parent', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'pk', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
) )
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'parent', 'description') default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
class VirtualMachineVMInterfaceTable(VMInterfaceTable): class VirtualMachineVMInterfaceTable(VMInterfaceTable):
parent = tables.Column(
linkify=True
)
bridge = tables.Column(
linkify=True
)
actions = ButtonsColumn( actions = ButtonsColumn(
model=VMInterface, model=VMInterface,
buttons=('edit', 'delete'), buttons=('edit', 'delete'),
@ -192,8 +195,8 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'ip_addresses', 'pk', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'untagged_vlan', 'tagged_vlans', 'actions', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions', 'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions',

View File

@ -246,14 +246,15 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'virtual_machine': virtualmachine.pk, 'virtual_machine': virtualmachine.pk,
'name': 'Interface 5', 'name': 'Interface 5',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'bridge': interfaces[0].pk,
'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk, 'untagged_vlan': vlans[2].pk,
}, },
{ {
'virtual_machine': virtualmachine.pk, 'virtual_machine': virtualmachine.pk,
'name': 'Interface 6', 'name': 'Interface 6',
'parent': interfaces[0].pk,
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'parent': interfaces[1].pk,
'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk, 'untagged_vlan': vlans[2].pk,
}, },

View File

@ -452,6 +452,19 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'parent_id': [parent_interface.pk]} params = {'parent_id': [parent_interface.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_bridge(self):
# Create bridged interfaces
bridge_interface = VMInterface.objects.first()
bridged_interfaces = (
VMInterface(virtual_machine=bridge_interface.virtual_machine, name='Bridged 1', bridge=bridge_interface),
VMInterface(virtual_machine=bridge_interface.virtual_machine, name='Bridged 2', bridge=bridge_interface),
VMInterface(virtual_machine=bridge_interface.virtual_machine, name='Bridged 3', bridge=bridge_interface),
)
VMInterface.objects.bulk_create(bridged_interfaces)
params = {'bridge_id': [bridge_interface.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_mtu(self): def test_mtu(self):
params = {'mtu': [100, 200]} params = {'mtu': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -248,10 +248,11 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
) )
VirtualMachine.objects.bulk_create(virtualmachines) VirtualMachine.objects.bulk_create(virtualmachines)
VMInterface.objects.bulk_create([ interfaces = VMInterface.objects.bulk_create([
VMInterface(virtual_machine=virtualmachines[0], name='Interface 1'), VMInterface(virtual_machine=virtualmachines[0], name='Interface 1'),
VMInterface(virtual_machine=virtualmachines[0], name='Interface 2'), VMInterface(virtual_machine=virtualmachines[0], name='Interface 2'),
VMInterface(virtual_machine=virtualmachines[0], name='Interface 3'), VMInterface(virtual_machine=virtualmachines[0], name='Interface 3'),
VMInterface(virtual_machine=virtualmachines[1], name='BRIDGE'),
]) ])
vlans = ( vlans = (
@ -268,6 +269,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'virtual_machine': virtualmachines[1].pk, 'virtual_machine': virtualmachines[1].pk,
'name': 'Interface X', 'name': 'Interface X',
'enabled': False, 'enabled': False,
'bridge': interfaces[3].pk,
'mac_address': EUI('01-02-03-04-05-06'), 'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 65000, 'mtu': 65000,
'description': 'New description', 'description': 'New description',
@ -281,6 +283,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'virtual_machine': virtualmachines[1].pk, 'virtual_machine': virtualmachines[1].pk,
'name_pattern': 'Interface [4-6]', 'name_pattern': 'Interface [4-6]',
'enabled': False, 'enabled': False,
'bridge': interfaces[3].pk,
'mac_address': EUI('01-02-03-04-05-06'), 'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 2000, 'mtu': 2000,
'description': 'New description', 'description': 'New description',