Skip to content

Commit 1c66733

Browse files
Merge pull request #5930 from netbox-community/1519-interface-parent
Closes #1519: Enable parent assignment for interfaces
2 parents 5406e8e + 2ef85ea commit 1c66733

23 files changed

+223
-88
lines changed

docs/release-notes/version-2.11.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
### New Features
88

9+
#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
10+
11+
Virtual interfaces can now be assigned to a "parent" physical interface, by setting the `parent` field on the Interface model. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 to the physical interface Gi0/0.
12+
913
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
1014

1115
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where you don't necessarily know or care what is connected to the far end of a cable, but still need to designate the near end termination.
@@ -58,6 +62,8 @@ The ObjectChange model (which is used to record the creation, modification, and
5862
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
5963
* dcim.Device
6064
* Added the `location` field
65+
* dcim.Interface
66+
* Added the `parent` field
6167
* dcim.PowerPanel
6268
* Renamed `rack_group` field to `location`
6369
* dcim.Rack

netbox/circuits/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def to_objectchange(self, action):
295295
return super().to_objectchange(action, related_object=circuit)
296296

297297
@property
298-
def parent(self):
298+
def parent_object(self):
299299
return self.circuit
300300

301301
def get_peer_termination(self):

netbox/dcim/api/serializers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
598598
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
599599
device = NestedDeviceSerializer()
600600
type = ChoiceField(choices=InterfaceTypeChoices)
601+
parent = NestedInterfaceSerializer(required=False, allow_null=True)
601602
lag = NestedInterfaceSerializer(required=False, allow_null=True)
602603
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
603604
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@@ -613,10 +614,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
613614
class Meta:
614615
model = Interface
615616
fields = [
616-
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
617-
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_peer',
618-
'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
619-
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied',
617+
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
618+
'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
619+
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
620+
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
621+
'_occupied',
620622
]
621623

622624
def validate(self, data):

netbox/dcim/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
522522

523523
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
524524
queryset = Interface.objects.prefetch_related(
525-
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
525+
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
526526
)
527527
serializer_class = serializers.InterfaceSerializer
528528
filterset_class = filters.InterfaceFilterSet

netbox/dcim/filters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
844844
method='filter_kind',
845845
label='Kind of interface',
846846
)
847+
parent_id = django_filters.ModelMultipleChoiceFilter(
848+
field_name='parent',
849+
queryset=Interface.objects.all(),
850+
label='Parent interface (ID)',
851+
)
847852
lag_id = django_filters.ModelMultipleChoiceFilter(
848853
field_name='lag',
849854
queryset=Interface.objects.all(),

netbox/dcim/forms.py

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,6 +2802,24 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
28022802

28032803

28042804
class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
2805+
parent = DynamicModelChoiceField(
2806+
queryset=Interface.objects.all(),
2807+
required=False,
2808+
label='Parent interface',
2809+
display_field='display_name',
2810+
query_params={
2811+
'kind': 'physical',
2812+
}
2813+
)
2814+
lag = DynamicModelChoiceField(
2815+
queryset=Interface.objects.all(),
2816+
required=False,
2817+
label='LAG interface',
2818+
display_field='display_name',
2819+
query_params={
2820+
'type': 'lag',
2821+
}
2822+
)
28052823
untagged_vlan = DynamicModelChoiceField(
28062824
queryset=VLAN.objects.all(),
28072825
required=False,
@@ -2830,13 +2848,12 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
28302848
class Meta:
28312849
model = Interface
28322850
fields = [
2833-
'device', 'name', 'label', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected',
2834-
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
2851+
'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only',
2852+
'mark_connected', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
28352853
]
28362854
widgets = {
28372855
'device': forms.HiddenInput(),
28382856
'type': StaticSelect2(),
2839-
'lag': StaticSelect2(),
28402857
'mode': StaticSelect2(),
28412858
}
28422859
labels = {
@@ -2849,19 +2866,11 @@ class Meta:
28492866
def __init__(self, *args, **kwargs):
28502867
super().__init__(*args, **kwargs)
28512868

2852-
if self.is_bound:
2853-
device = Device.objects.get(pk=self.data['device'])
2854-
else:
2855-
device = self.instance.device
2869+
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
28562870

2857-
# Limit LAG choices to interfaces belonging to this device or a peer VC member
2858-
device_query = Q(device=device)
2859-
if device.virtual_chassis:
2860-
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
2861-
self.fields['lag'].queryset = Interface.objects.filter(
2862-
device_query,
2863-
type=InterfaceTypeChoices.TYPE_LAG
2864-
).exclude(pk=self.instance.pk)
2871+
# Restrict parent/LAG interface assignment by device
2872+
self.fields['parent'].widget.add_query_param('device_id', device.pk)
2873+
self.fields['lag'].widget.add_query_param('device_id', device.pk)
28652874

28662875
# Add current site to VLANs query params
28672876
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
@@ -2878,11 +2887,23 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
28782887
required=False,
28792888
initial=True
28802889
)
2881-
lag = forms.ModelChoiceField(
2890+
parent = DynamicModelChoiceField(
28822891
queryset=Interface.objects.all(),
28832892
required=False,
2884-
label='Parent LAG',
2885-
widget=StaticSelect2(),
2893+
display_field='display_name',
2894+
query_params={
2895+
'device_id': '$device',
2896+
'kind': 'physical',
2897+
}
2898+
)
2899+
lag = DynamicModelChoiceField(
2900+
queryset=Interface.objects.all(),
2901+
required=False,
2902+
display_field='display_name',
2903+
query_params={
2904+
'device_id': '$device',
2905+
'type': 'lag',
2906+
}
28862907
)
28872908
mtu = forms.IntegerField(
28882909
required=False,
@@ -2923,23 +2944,17 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
29232944
}
29242945
)
29252946
field_order = (
2926-
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'description',
2927-
'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
2947+
'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
2948+
'description', 'mgmt_only', 'mark_connected', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags'
29282949
)
29292950

29302951
def __init__(self, *args, **kwargs):
29312952
super().__init__(*args, **kwargs)
29322953

2933-
# Limit LAG choices to interfaces belonging to this device or a peer VC member
2954+
# Add current site to VLANs query params
29342955
device = Device.objects.get(
29352956
pk=self.initial.get('device') or self.data.get('device')
29362957
)
2937-
device_query = Q(device=device)
2938-
if device.virtual_chassis:
2939-
device_query |= Q(device__virtual_chassis=device.virtual_chassis)
2940-
self.fields['lag'].queryset = Interface.objects.filter(device_query, type=InterfaceTypeChoices.TYPE_LAG)
2941-
2942-
# Add current site to VLANs query params
29432958
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
29442959
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
29452960

@@ -2956,7 +2971,7 @@ class InterfaceBulkCreateForm(
29562971

29572972
class InterfaceBulkEditForm(
29582973
form_from_model(Interface, [
2959-
'label', 'type', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode'
2974+
'label', 'type', 'parent', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode',
29602975
]),
29612976
BootstrapMixin,
29622977
AddRemoveTagsForm,
@@ -2976,6 +2991,22 @@ class InterfaceBulkEditForm(
29762991
required=False,
29772992
widget=BulkEditNullBooleanSelect
29782993
)
2994+
parent = DynamicModelChoiceField(
2995+
queryset=Interface.objects.all(),
2996+
required=False,
2997+
display_field='display_name',
2998+
query_params={
2999+
'kind': 'physical',
3000+
}
3001+
)
3002+
lag = DynamicModelChoiceField(
3003+
queryset=Interface.objects.all(),
3004+
required=False,
3005+
display_field='display_name',
3006+
query_params={
3007+
'type': 'lag',
3008+
}
3009+
)
29793010
mgmt_only = forms.NullBooleanField(
29803011
required=False,
29813012
widget=BulkEditNullBooleanSelect,
@@ -3006,25 +3037,24 @@ class InterfaceBulkEditForm(
30063037

30073038
class Meta:
30083039
nullable_fields = [
3009-
'label', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
3040+
'label', 'parent', 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
30103041
]
30113042

30123043
def __init__(self, *args, **kwargs):
30133044
super().__init__(*args, **kwargs)
3014-
3015-
# Limit LAG choices to interfaces which belong to the parent device (or VC master)
30163045
if 'device' in self.initial:
30173046
device = Device.objects.filter(pk=self.initial['device']).first()
3018-
self.fields['lag'].queryset = Interface.objects.filter(
3019-
device__in=[device, device.get_vc_master()],
3020-
type=InterfaceTypeChoices.TYPE_LAG
3021-
)
3047+
3048+
# Restrict parent/LAG interface assignment by device
3049+
self.fields['parent'].widget.add_query_param('device_id', device.pk)
3050+
self.fields['lag'].widget.add_query_param('device_id', device.pk)
30223051

30233052
# Add current site to VLANs query params
30243053
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
30253054
self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk)
3055+
30263056
else:
3027-
# See 4523
3057+
# See #4523
30283058
if 'pk' in self.initial:
30293059
site = None
30303060
interfaces = Interface.objects.filter(pk__in=self.initial['pk']).prefetch_related('device__site')
@@ -3042,6 +3072,8 @@ def __init__(self, *args, **kwargs):
30423072
self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
30433073
self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
30443074

3075+
self.fields['parent'].choices = ()
3076+
self.fields['parent'].widget.attrs['disabled'] = True
30453077
self.fields['lag'].choices = ()
30463078
self.fields['lag'].widget.attrs['disabled'] = True
30473079

@@ -3064,6 +3096,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
30643096
queryset=Device.objects.all(),
30653097
to_field_name='name'
30663098
)
3099+
parent = CSVModelChoiceField(
3100+
queryset=Interface.objects.all(),
3101+
required=False,
3102+
to_field_name='name',
3103+
help_text='Parent interface'
3104+
)
30673105
lag = CSVModelChoiceField(
30683106
queryset=Interface.objects.all(),
30693107
required=False,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.db import migrations, models
2+
import django.db.models.deletion
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
('dcim', '0128_device_location_populate'),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name='interface',
14+
name='parent',
15+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='dcim.interface'),
16+
),
17+
]

netbox/dcim/models/device_component_templates.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
from dcim.choices import *
66
from dcim.constants import *
7-
from extras.models import ObjectChange
87
from extras.utils import extras_features
98
from netbox.models import BigIDModel, ChangeLoggingMixin
109
from utilities.fields import NaturalOrderingField
1110
from utilities.querysets import RestrictedQuerySet
1211
from utilities.ordering import naturalize_interface
13-
from utilities.utils import serialize_object
1412
from .device_components import (
1513
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
1614
)

0 commit comments

Comments
 (0)