mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -06:00
Merge pull request #7622 from netbox-community/6346-interface-bridge
Closes #6346: Bridge group support
This commit is contained in:
commit
dbe2f8a6f1
@ -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.
|
||||
|
||||
### New Features
|
||||
|
||||
#### 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.
|
||||
@ -26,6 +28,12 @@ Both types of connection include SSID and authentication attributes. Additionall
|
||||
* Channel - A predefined channel within a standardized band
|
||||
* 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
|
||||
|
||||
* [#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
|
||||
* Added `airflow` field
|
||||
* dcim.Interface
|
||||
* Added `bridge` field
|
||||
* Added `wwn` field
|
||||
* dcim.Location
|
||||
* Added `tenant` field
|
||||
* virtualization.VMInterface
|
||||
* Added `bridge` field
|
||||
|
@ -605,6 +605,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_null=True)
|
||||
@ -622,8 +623,8 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
|
||||
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu',
|
||||
'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',
|
||||
'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
|
||||
|
@ -544,7 +544,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
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
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
|
@ -975,6 +975,11 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
|
||||
queryset=Interface.objects.all(),
|
||||
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(
|
||||
field_name='lag',
|
||||
queryset=Interface.objects.all(),
|
||||
|
@ -939,8 +939,8 @@ class PowerOutletBulkEditForm(
|
||||
|
||||
class InterfaceBulkEditForm(
|
||||
form_from_model(Interface, [
|
||||
'label', 'type', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description',
|
||||
'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected',
|
||||
'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
|
||||
]),
|
||||
BootstrapMixin,
|
||||
AddRemoveTagsForm,
|
||||
@ -964,6 +964,10 @@ class InterfaceBulkEditForm(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -991,7 +995,7 @@ class InterfaceBulkEditForm(
|
||||
|
||||
class Meta:
|
||||
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',
|
||||
]
|
||||
|
||||
@ -1000,8 +1004,9 @@ class InterfaceBulkEditForm(
|
||||
if 'device' in self.initial:
|
||||
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['bridge'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
@ -1029,6 +1034,8 @@ class InterfaceBulkEditForm(
|
||||
|
||||
self.fields['parent'].choices = ()
|
||||
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'].widget.attrs['disabled'] = True
|
||||
|
||||
|
@ -570,6 +570,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
to_field_name='name',
|
||||
help_text='Parent interface'
|
||||
)
|
||||
bridge = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Bridged interface'
|
||||
)
|
||||
lag = CSVModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -594,39 +600,11 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = (
|
||||
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn',
|
||||
'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
|
||||
'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'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):
|
||||
# Make sure enabled is True when it's not included in the uploaded data
|
||||
if 'enabled' not in self.data:
|
||||
|
@ -1093,6 +1093,11 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
required=False,
|
||||
label='Parent interface'
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface'
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -1143,8 +1148,8 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
|
||||
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
|
||||
'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
|
||||
]
|
||||
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
|
||||
|
||||
# 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['bridge'].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:
|
||||
# Get available LAG interfaces by VirtualChassis 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)
|
||||
else:
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
@ -446,6 +446,13 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
)
|
||||
lag = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -497,7 +504,7 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
|
||||
required=False
|
||||
)
|
||||
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',
|
||||
'rf_channel_width', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
|
||||
)
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
23
netbox/dcim/migrations/0134_interface_wwn_bridge.py
Normal file
23
netbox/dcim/migrations/0134_interface_wwn_bridge.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -6,7 +6,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0002_tenant_ordering'),
|
||||
('dcim', '0134_interface_wwn'),
|
||||
('dcim', '0134_interface_wwn_bridge'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -462,6 +462,22 @@ class BaseInterface(models.Model):
|
||||
choices=InterfaceModeChoices,
|
||||
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:
|
||||
abstract = True
|
||||
@ -495,14 +511,6 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||
max_length=100,
|
||||
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(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -586,7 +594,7 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||
related_query_name='interface'
|
||||
)
|
||||
|
||||
clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only']
|
||||
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
|
||||
|
||||
class Meta:
|
||||
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."
|
||||
})
|
||||
|
||||
# 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
|
||||
if self.parent and self.parent.device != self.device:
|
||||
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}."
|
||||
})
|
||||
|
||||
# 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."})
|
||||
# Bridge validation
|
||||
|
||||
# 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 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 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
|
||||
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}."
|
||||
})
|
||||
|
||||
# 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."})
|
||||
# Wireless validation
|
||||
|
||||
# RF role & channel may only be set for wireless interfaces
|
||||
if self.rf_role and not self.is_wireless:
|
||||
@ -679,11 +712,13 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||
elif self.rf_channel:
|
||||
self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')
|
||||
|
||||
# VLAN validation
|
||||
|
||||
# Validate untagged VLAN
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
|
||||
raise ValidationError({
|
||||
'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent "
|
||||
"device, or it must be global".format(self.untagged_vlan)
|
||||
'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
|
||||
f"interface's parent device, or it must be global."
|
||||
})
|
||||
|
||||
@property
|
||||
|
@ -521,8 +521,10 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
attrs={'td': {'class': 'text-nowrap'}}
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Parent'
|
||||
linkify=True
|
||||
)
|
||||
bridge = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
lag = tables.Column(
|
||||
linkify=True,
|
||||
@ -537,10 +539,10 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn',
|
||||
'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', 'cable_color',
|
||||
'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
|
||||
'tagged_vlans', 'actions',
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode',
|
||||
'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
|
@ -1206,6 +1206,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'name': 'Interface 5',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'bridge': interfaces[0].pk,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
@ -1214,7 +1215,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
'name': 'Interface 6',
|
||||
'type': 'virtual',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'parent': interfaces[0].pk,
|
||||
'parent': interfaces[1].pk,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
|
@ -2125,6 +2125,19 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'parent_id': [parent_interface.pk]}
|
||||
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):
|
||||
# Create LAG members
|
||||
device = Device.objects.first()
|
||||
|
@ -1581,6 +1581,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
Interface(device=device, name='Interface 2'),
|
||||
Interface(device=device, name='Interface 3'),
|
||||
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)
|
||||
|
||||
@ -1596,10 +1597,10 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
|
||||
cls.form_data = {
|
||||
'device': device.pk,
|
||||
'virtual_machine': None,
|
||||
'name': 'Interface X',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'bridge': interfaces[4].pk,
|
||||
'lag': interfaces[3].pk,
|
||||
'mac_address': EUI('01:02:03:04:05:06'),
|
||||
'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]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'bridge': interfaces[4].pk,
|
||||
'lag': interfaces[3].pk,
|
||||
'mac_address': EUI('01:02:03:04:05:06'),
|
||||
'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
|
||||
|
@ -69,6 +69,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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>
|
||||
<th scope="row">LAG</th>
|
||||
<td>
|
||||
|
@ -18,6 +18,7 @@
|
||||
{% render_field form.label %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.parent %}
|
||||
{% render_field form.bridge %}
|
||||
{% render_field form.lag %}
|
||||
{% render_field form.mac_address %}
|
||||
{% render_field form.wwn %}
|
||||
|
@ -47,6 +47,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }} </td>
|
||||
|
@ -17,6 +17,7 @@
|
||||
{% render_field form.name %}
|
||||
{% render_field form.enabled %}
|
||||
{% render_field form.parent %}
|
||||
{% render_field form.bridge %}
|
||||
{% render_field form.mac_address %}
|
||||
{% render_field form.mtu %}
|
||||
{% render_field form.description %}
|
||||
|
@ -107,6 +107,7 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
@ -120,8 +121,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'mtu', 'mac_address', 'description',
|
||||
'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address',
|
||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'count_ipaddresses',
|
||||
]
|
||||
|
||||
|
@ -264,6 +264,11 @@ class VMInterfaceFilterSet(PrimaryModelFilterSet):
|
||||
queryset=VMInterface.objects.all(),
|
||||
label='Parent interface (ID)',
|
||||
)
|
||||
bridge_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='bridge',
|
||||
queryset=VMInterface.objects.all(),
|
||||
label='Bridged interface (ID)',
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
label='MAC address',
|
||||
)
|
||||
|
@ -165,6 +165,10 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
@ -195,7 +199,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'parent', 'mtu', 'description',
|
||||
'parent', 'bridge', 'mtu', 'description',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -203,8 +207,9 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
|
||||
if 'virtual_machine' in self.initial:
|
||||
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['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
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['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):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
|
@ -104,6 +104,18 @@ class VMInterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
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(
|
||||
choices=InterfaceModeChoices,
|
||||
required=False,
|
||||
@ -113,7 +125,7 @@ class VMInterfaceCSVForm(CustomFieldModelCSVForm):
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
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):
|
||||
|
@ -277,6 +277,11 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
||||
required=False,
|
||||
label='Parent interface'
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
label='Bridged interface'
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
@ -306,8 +311,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
fields = [
|
||||
'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags',
|
||||
'untagged_vlan', 'tagged_vlans',
|
||||
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
|
||||
'tags', 'untagged_vlan', 'tagged_vlans',
|
||||
]
|
||||
widgets = {
|
||||
'virtual_machine': forms.HiddenInput(),
|
||||
@ -326,6 +331,7 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
||||
|
||||
# Restrict parent interface assignment by VM
|
||||
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
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
|
@ -35,6 +35,13 @@ class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonFo
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
bridge = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC Address'
|
||||
@ -61,7 +68,7 @@ class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonFo
|
||||
required=False
|
||||
)
|
||||
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'
|
||||
)
|
||||
|
||||
|
19
netbox/virtualization/migrations/0026_vminterface_bridge.py
Normal file
19
netbox/virtualization/migrations/0026_vminterface_bridge.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -378,14 +378,6 @@ class VMInterface(PrimaryModel, BaseInterface):
|
||||
max_length=200,
|
||||
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(
|
||||
to='ipam.VLAN',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -423,6 +415,12 @@ class VMInterface(PrimaryModel, BaseInterface):
|
||||
def clean(self):
|
||||
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
|
||||
if self.parent and self.parent.virtual_machine != self.virtual_machine:
|
||||
raise ValidationError({
|
||||
@ -430,15 +428,26 @@ class VMInterface(PrimaryModel, BaseInterface):
|
||||
f"({self.parent.virtual_machine})."
|
||||
})
|
||||
|
||||
# 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."})
|
||||
# Bridge validation
|
||||
|
||||
# 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
|
||||
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
|
||||
raise ValidationError({
|
||||
'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):
|
||||
|
@ -166,9 +166,6 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
parent = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='virtualization:vminterface_list'
|
||||
)
|
||||
@ -176,13 +173,19 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
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',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'parent', 'description')
|
||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||
|
||||
|
||||
class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
||||
parent = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
bridge = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=VMInterface,
|
||||
buttons=('edit', 'delete'),
|
||||
@ -192,8 +195,8 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
'pk', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions',
|
||||
|
@ -246,14 +246,15 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
'virtual_machine': virtualmachine.pk,
|
||||
'name': 'Interface 5',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'bridge': interfaces[0].pk,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
{
|
||||
'virtual_machine': virtualmachine.pk,
|
||||
'name': 'Interface 6',
|
||||
'parent': interfaces[0].pk,
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'parent': interfaces[1].pk,
|
||||
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
|
||||
'untagged_vlan': vlans[2].pk,
|
||||
},
|
||||
|
@ -452,6 +452,19 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'parent_id': [parent_interface.pk]}
|
||||
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):
|
||||
params = {'mtu': [100, 200]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
@ -248,10 +248,11 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
)
|
||||
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 2'),
|
||||
VMInterface(virtual_machine=virtualmachines[0], name='Interface 3'),
|
||||
VMInterface(virtual_machine=virtualmachines[1], name='BRIDGE'),
|
||||
])
|
||||
|
||||
vlans = (
|
||||
@ -268,6 +269,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'virtual_machine': virtualmachines[1].pk,
|
||||
'name': 'Interface X',
|
||||
'enabled': False,
|
||||
'bridge': interfaces[3].pk,
|
||||
'mac_address': EUI('01-02-03-04-05-06'),
|
||||
'mtu': 65000,
|
||||
'description': 'New description',
|
||||
@ -281,6 +283,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
'virtual_machine': virtualmachines[1].pk,
|
||||
'name_pattern': 'Interface [4-6]',
|
||||
'enabled': False,
|
||||
'bridge': interfaces[3].pk,
|
||||
'mac_address': EUI('01-02-03-04-05-06'),
|
||||
'mtu': 2000,
|
||||
'description': 'New description',
|
||||
|
Loading…
Reference in New Issue
Block a user