Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #1519: Enable parent assignment for interfaces #5930

Merged
merged 6 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/release-notes/version-2.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### New Features

#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))

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.

#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))

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.
Expand Down Expand Up @@ -58,6 +62,8 @@ The ObjectChange model (which is used to record the creation, modification, and
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
* dcim.Device
* Added the `location` field
* dcim.Interface
* Added the `parent` field
* dcim.PowerPanel
* Renamed `rack_group` field to `location`
* dcim.Rack
Expand Down
2 changes: 1 addition & 1 deletion netbox/circuits/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def to_objectchange(self, action):
return super().to_objectchange(action, related_object=circuit)

@property
def parent(self):
def parent_object(self):
return self.circuit

def get_peer_termination(self):
Expand Down
10 changes: 6 additions & 4 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
110 changes: 74 additions & 36 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2802,6 +2802,24 @@ class InterfaceFilterForm(DeviceComponentFilterForm):


class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Parent interface',
display_field='display_name',
query_params={
'kind': 'physical',
}
)
lag = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='LAG interface',
display_field='display_name',
query_params={
'type': 'lag',
}
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
Expand Down Expand Up @@ -2830,13 +2848,12 @@ 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(),
'type': StaticSelect2(),
'lag': StaticSelect2(),
'mode': StaticSelect2(),
}
labels = {
Expand All @@ -2849,19 +2866,11 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if self.is_bound:
device = Device.objects.get(pk=self.data['device'])
else:
device = self.instance.device
device = Device.objects.get(pk=self.data['device']) if self.is_bound else 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)
self.fields['lag'].queryset = Interface.objects.filter(
device_query,
type=InterfaceTypeChoices.TYPE_LAG
).exclude(pk=self.instance.pk)
# Restrict parent/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('device_id', device.pk)
self.fields['lag'].widget.add_query_param('device_id', device.pk)

# Add current site to VLANs query params
self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk)
Expand All @@ -2878,11 +2887,23 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm):
required=False,
initial=True
)
lag = forms.ModelChoiceField(
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Parent LAG',
widget=StaticSelect2(),
display_field='display_name',
query_params={
'device_id': '$device',
'kind': 'physical',
}
)
lag = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
display_field='display_name',
query_params={
'device_id': '$device',
'type': 'lag',
}
)
mtu = forms.IntegerField(
required=False,
Expand Down Expand Up @@ -2923,23 +2944,17 @@ 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
# Add current site to VLANs query params
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)
self.fields['lag'].queryset = Interface.objects.filter(device_query, type=InterfaceTypeChoices.TYPE_LAG)

# Add current site to VLANs query params
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)

Expand All @@ -2956,7 +2971,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,
Expand All @@ -2976,6 +2991,22 @@ class InterfaceBulkEditForm(
required=False,
widget=BulkEditNullBooleanSelect
)
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
display_field='display_name',
query_params={
'kind': 'physical',
}
)
lag = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
display_field='display_name',
query_params={
'type': 'lag',
}
)
mgmt_only = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect,
Expand Down Expand Up @@ -3006,25 +3037,24 @@ 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):
super().__init__(*args, **kwargs)

# Limit LAG choices to interfaces which belong to the parent device (or VC master)
if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG
)

# Restrict parent/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('device_id', device.pk)
self.fields['lag'].widget.add_query_param('device_id', device.pk)

# Add current site to VLANs query params
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')
Expand All @@ -3042,6 +3072,8 @@ def __init__(self, *args, **kwargs):
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['lag'].choices = ()
self.fields['lag'].widget.attrs['disabled'] = True

Expand All @@ -3064,6 +3096,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,
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/migrations/0129_interface_parent.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
2 changes: 0 additions & 2 deletions netbox/dcim/models/device_component_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@

from dcim.choices import *
from dcim.constants import *
from extras.models import ObjectChange
from extras.utils import extras_features
from netbox.models import BigIDModel, ChangeLoggingMixin
from utilities.fields import NaturalOrderingField
from utilities.querysets import RestrictedQuerySet
from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object
from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
)
Expand Down
Loading