Add parent field to Interface

This commit is contained in:
Jeremy Stretch 2021-03-05 13:49:41 -05:00
parent 8e1fe6339e
commit e1a86139dc
8 changed files with 114 additions and 23 deletions

View File

@ -598,6 +598,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@ -613,10 +614,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
class Meta:
model = Interface
fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer',
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'_occupied',
]
def validate(self, data):

View File

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

View File

@ -844,6 +844,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
method='filter_kind',
label='Kind of interface',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent',
queryset=Interface.objects.all(),
label='Parent interface (ID)',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
field_name='lag',
queryset=Interface.objects.all(),

View File

@ -2830,8 +2830,8 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
class Meta:
model = Interface
fields = [
'device', 'name', 'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected',
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
@ -2854,10 +2854,16 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
else:
device = self.instance.device
# Limit LAG choices to interfaces belonging to this device or a peer VC member
device_query = Q(device=device)
if device.virtual_chassis:
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
# Limit parent interface choices to interfaces belonging to this device or a peer VC member
self.fields['parent'].queryset = Interface.objects.filter(device_query).exclude(
type__in=(InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_LAG)
).exclude(pk=self.instance.pk)
# Limit LAG choices to interfaces belonging to this device or a peer VC member
self.fields['lag'].queryset = Interface.objects.filter(
device_query,
type=InterfaceTypeChoices.TYPE_LAG
@ -2878,6 +2884,12 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
required=False,
initial=True
)
parent = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Parent interface',
widget=StaticSelect2(),
)
lag = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@ -2923,20 +2935,25 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
}
)
field_order = (
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'description',
'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device or a peer VC member
device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
device_query = Q(device=device)
if device.virtual_chassis:
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
# Limit parent interface choices to interfaces belonging to this device or a peer VC member
self.fields['parent'].queryset = Interface.objects.filter(device_query).exclude(
type__in=(InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_LAG)
)
# Limit LAG choices to interfaces belonging to this device or a peer VC member
self.fields['lag'].queryset = Interface.objects.filter(device_query, type=InterfaceTypeChoices.TYPE_LAG)
# Add current site to VLANs query params
@ -2956,7 +2973,7 @@ class InterfaceBulkCreateForm(
class InterfaceBulkEditForm(
form_from_model(Interface, [
'label', 'type', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode'
'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode',
]),
BootstrapMixin,
AddRemoveTagsForm,
@ -3006,7 +3023,7 @@ class InterfaceBulkEditForm(
class Meta:
nullable_fields = [
'label', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
'label', 'parent', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
]
def __init__(self, *args, **kwargs):
@ -3024,7 +3041,7 @@ class InterfaceBulkEditForm(
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
else:
# See 4523
# See #4523
if 'pk' in self.initial:
site = None
interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
@ -3064,6 +3081,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
queryset=Device.objects.all(),
to_field_name='name'
)
parent = CSVModelChoiceField(
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text='Parent interface'
)
lag = CSVModelChoiceField(
queryset=Interface.objects.all(),
required=False,

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0128_device_location_populate'),
]
operations = [
migrations.AddField(
model_name='interface',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
),
]

View File

@ -523,6 +523,14 @@ class Interface(ComponentModel, BaseInterface, CableTermination, 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,
@ -563,8 +571,8 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
tags = TaggableManager(through=TaggedItem)
csv_headers = [
'device', 'name', 'label', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu', 'mgmt_only',
'description', 'mode',
'device', 'name', 'label', 'parent', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'mtu',
'mgmt_only', 'description', 'mode',
]
class Meta:
@ -579,6 +587,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
self.device.identifier if self.device else None,
self.name,
self.label,
self.parent.name if self.parent else None,
self.lag.name if self.lag else None,
self.get_type_display(),
self.enabled,
@ -602,6 +611,27 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
"Disconnect the interface or choose a suitable type."
})
# 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:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
f"({self.parent.device})."
})
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
f"is not part of virtual chassis {self.device.virtual_chassis}."
})
# 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."})
# A virtual interface cannot be a parent interface
if self.parent is not None and self.parent.type == InterfaceTypeChoices.TYPE_VIRTUAL:
raise ValidationError({'parent': "Virtual interfaces may not be parents of other interfaces."})
# An interface's LAG must belong to the same device or virtual chassis
if self.lag and self.lag.device != self.device:
if self.device.virtual_chassis is None:

View File

@ -436,6 +436,10 @@ class DeviceInterfaceTable(InterfaceTable):
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
attrs={'td': {'class': 'text-nowrap'}}
)
parent = tables.Column(
linkify=True,
verbose_name='Parent'
)
lag = tables.Column(
linkify=True,
verbose_name='LAG'
@ -449,13 +453,13 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta):
model = Interface
fields = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
'tagged_vlans', 'actions',
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'mark_connected', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses',
'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
'connection', 'actions',
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
'cable', 'connection', 'actions',
)
row_attrs = {
'class': lambda record: record.cable.get_status_class() if record.cable else '',

View File

@ -38,10 +38,20 @@
{% endif %}
</td>
</tr>
<tr>
<td>Parent</td>
<td>
{% if object.parent %}
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>LAG</td>
<td>
{% if object.lag%}
{% if object.lag %}
<a href="{{ object.lag.get_absolute_url }}">{{ object.lag }}</a>
{% else %}
<span class="text-muted">None</span>