mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Add parent field to Interface
This commit is contained in:
parent
8e1fe6339e
commit
e1a86139dc
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
17
netbox/dcim/migrations/0129_interface_parent.py
Normal file
17
netbox/dcim/migrations/0129_interface_parent.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -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 '',
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user