diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 7390c2216..036b74810 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -1,7 +1,7 @@ from django import forms from django.db.models import Count -from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL +from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.models import Tenant from utilities.forms import ( @@ -227,14 +227,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): # Limit interface choices if self.is_bound and self.data.get('device'): - interfaces = Interface.objects.filter(device=self.data['device'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a', - 'connected_as_b') + interfaces = Interface.objects.filter(device=self.data['device']).exclude( + form_factor__in=VIRTUAL_IFACE_TYPES + ).select_related( + 'circuit_termination', 'connected_as_a', 'connected_as_b' + ) self.fields['interface'].widget.attrs['initial'] = self.data.get('interface') elif self.initial.get('device'): - interfaces = Interface.objects.filter(device=self.initial['device'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a', - 'connected_as_b') + interfaces = Interface.objects.filter(device=self.initial['device']).exclude( + form_factor__in=VIRTUAL_IFACE_TYPES + ).select_related( + 'circuit_termination', 'connected_as_a', 'connected_as_b' + ) self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface') else: interfaces = [] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 358fcd1f2..548e84f88 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -390,13 +390,24 @@ class PowerPortNestedSerializer(PowerPortSerializer): # Interfaces # -class InterfaceSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() +class ParentInterfaceNestedSerializer(serializers.ModelSerializer): form_factor = serializers.ReadOnlyField(source='get_form_factor_display') class Meta: model = Interface - fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] + fields = ['id', 'name', 'form_factor'] + + +class InterfaceSerializer(serializers.ModelSerializer): + device = DeviceNestedSerializer() + form_factor = serializers.ReadOnlyField(source='get_form_factor_display') + parent = ParentInterfaceNestedSerializer() + + class Meta: + model = Interface + fields = [ + 'id', 'device', 'name', 'form_factor', 'parent', 'mac_address', 'mgmt_only', 'description', 'is_connected', + ] class InterfaceNestedSerializer(InterfaceSerializer): @@ -410,8 +421,10 @@ class InterfaceDetailSerializer(InterfaceSerializer): connected_interface = InterfaceSerializer() class Meta(InterfaceSerializer.Meta): - fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', - 'connected_interface'] + fields = [ + 'id', 'device', 'name', 'form_factor', 'parent', 'mac_address', 'mgmt_only', 'description', 'is_connected', + 'connected_interface', + ] # diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 5679df579..f733e7e0d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -10,9 +10,9 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 from dcim.models import ( - ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, - InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, - RackRole, Site, + ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection, + Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site, + VIRTUAL_IFACE_TYPES, ) from dcim import filters from extras.api.views import CustomFieldModelAPIView @@ -359,9 +359,9 @@ class InterfaceListView(generics.ListAPIView): # Filter by type (physical or virtual) iface_type = self.request.query_params.get('type') if iface_type == 'physical': - queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL) + queryset = queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES) elif iface_type == 'virtual': - queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL) + queryset = queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES) elif iface_type is not None: queryset = queryset.empty() diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 58e339278..5c633292a 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,7 +8,8 @@ from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter from .models import ( ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, - Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site, + LAG_IFACE_TYPES, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site, + VIRTUAL_IFACE_TYPES, ) @@ -374,11 +375,25 @@ class InterfaceFilter(django_filters.FilterSet): to_field_name='name', label='Device (name)', ) + type = django_filters.MethodFilter( + action='filter_type', + label='Interface type', + ) class Meta: model = Interface fields = ['name'] + def filter_type(self, queryset, value): + value = value.strip().lower() + if value == 'physical': + return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES) + elif value == 'virtual': + return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES) + elif value == 'parent': + return queryset.filter(form_factor__in=LAG_IFACE_TYPES) + return queryset + class ConsoleConnectionFilter(django_filters.FilterSet): site = django_filters.MethodFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f9e3dcf62..e5eb9e8b2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -18,9 +18,10 @@ from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, - Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, - Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, - RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD + Interface, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, + LAG_IFACE_TYPES, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, + RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, + VIRTUAL_IFACE_TYPES ) @@ -53,6 +54,15 @@ def validate_connection_status(value): raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value)) +class DeviceComponentForm(BootstrapMixin, forms.Form): + """ + Allow inclusion of the parent device as context for limiting field choices. + """ + def __init__(self, device, *args, **kwargs): + self.device = device + super(DeviceComponentForm, self).__init__(*args, **kwargs) + + # # Sites # @@ -331,7 +341,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form): +class ConsolePortTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -345,7 +355,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form): +class ConsoleServerPortTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -359,7 +369,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): } -class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form): +class PowerPortTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -373,7 +383,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): } -class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form): +class PowerOutletTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -387,7 +397,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): } -class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form): +class InterfaceTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) mgmt_only = forms.BooleanField(required=False, label='OOB Management') @@ -411,7 +421,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form): +class DeviceBayTemplateCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -743,7 +753,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): } -class ConsolePortCreateForm(BootstrapMixin, forms.Form): +class ConsolePortCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -914,7 +924,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): } -class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form): +class ConsoleServerPortCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -1012,7 +1022,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): } -class PowerPortCreateForm(BootstrapMixin, forms.Form): +class PowerPortCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -1181,7 +1191,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): } -class PowerOutletCreateForm(BootstrapMixin, forms.Form): +class PowerOutletCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') @@ -1273,19 +1283,44 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): class Meta: model = Interface - fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] + fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] widgets = { 'device': forms.HiddenInput(), } + def __init__(self, *args, **kwargs): + super(InterfaceForm, self).__init__(*args, **kwargs) -class InterfaceCreateForm(BootstrapMixin, forms.Form): + # Limit LAG choices to interfaces belonging to this device + if self.is_bound: + self.fields['lag'].queryset = Interface.objects.order_naturally().filter( + device_id=self.data['device'], form_factor__in=LAG_IFACE_TYPES + ) + else: + self.fields['lag'].queryset = Interface.objects.order_naturally().filter( + device=self.instance.device, form_factor__in=LAG_IFACE_TYPES + ) + + +class InterfaceCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) + lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='LAG Interface') mac_address = MACAddressFormField(required=False, label='MAC Address') mgmt_only = forms.BooleanField(required=False, label='OOB Management') description = forms.CharField(max_length=100, required=False) + def __init__(self, *args, **kwargs): + super(InterfaceCreateForm, self).__init__(*args, **kwargs) + + # Limit LAG choices to interfaces belonging to this device + if self.device is not None: + self.fields['lag'].queryset = Interface.objects.order_naturally().filter( + device=self.device, form_factor__in=LAG_IFACE_TYPES + ) + else: + self.fields['lag'].queryset = Interface.objects.none() + class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) @@ -1360,8 +1395,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm): super(InterfaceConnectionForm, self).__init__(*args, **kwargs) # Initialize interface A choices - device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\ - .select_related('circuit_termination', 'connected_as_a', 'connected_as_b') + device_a_interfaces = Interface.objects.filter(device=device_a).exclude( + form_factor__in=VIRTUAL_IFACE_TYPES + ).select_related( + 'circuit_termination', 'connected_as_a', 'connected_as_b' + ) self.fields['interface_a'].choices = [ (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces ] @@ -1388,13 +1426,17 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm): # Initialize interface_b choices if device_b is set if self.is_bound: - device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL)\ - .select_related('circuit_termination', 'connected_as_a', 'connected_as_b') + device_b_interfaces = Interface.objects.filter(device=self.data['device_b']).exclude( + form_factor__in=VIRTUAL_IFACE_TYPES + ).select_related( + 'circuit_termination', 'connected_as_a', 'connected_as_b' + ) elif self.initial.get('device_b'): - device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\ - .exclude(form_factor=IFACE_FF_VIRTUAL)\ - .select_related('circuit_termination', 'connected_as_a', 'connected_as_b') + device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude( + form_factor__in=VIRTUAL_IFACE_TYPES + ).select_related( + 'circuit_termination', 'connected_as_a', 'connected_as_b' + ) else: device_b_interfaces = [] self.fields['interface_b'].choices = [ @@ -1512,7 +1554,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): } -class DeviceBayCreateForm(BootstrapMixin, forms.Form): +class DeviceBayCreateForm(DeviceComponentForm): name_pattern = ExpandableNameField(label='Name') diff --git a/netbox/dcim/migrations/0030_interface_add_lag.py b/netbox/dcim/migrations/0030_interface_add_lag.py new file mode 100644 index 000000000..e4e7d8e39 --- /dev/null +++ b/netbox/dcim/migrations/0030_interface_add_lag.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-02-24 20:49 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0029_allow_rackless_devices'), + ] + + operations = [ + migrations.AddField( + model_name='interface', + name='lag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name=b'LAG Interface'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Link aggregation', [[2000, b'Ethernet LAG (Static)'], [2101, b'Ethernet LAG (LACP/Active)'], [2102, b'Ethernet LAG (LACP/Passive)'], [2201, b'Ethernet LAG (PAgP/Desirable)'], [2202, b'Ethernet LAG (PAgP/Auto)'], [2700, b'Multilink PPP (MLPPP)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Link aggregation', [[2000, b'Ethernet LAG (Static)'], [2101, b'Ethernet LAG (LACP/Active)'], [2102, b'Ethernet LAG (LACP/Passive)'], [2201, b'Ethernet LAG (PAgP/Desirable)'], [2202, b'Ethernet LAG (PAgP/Auto)'], [2700, b'Multilink PPP (MLPPP)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 64b0458b4..1d2986869 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -82,6 +82,13 @@ IFACE_FF_25GE_SFP28 = 1350 IFACE_FF_40GE_QSFP_PLUS = 1400 IFACE_FF_100GE_CFP = 1500 IFACE_FF_100GE_QSFP28 = 1600 +# Link aggregation +IFACE_FF_LAG_STATIC = 2000 +IFACE_FF_LAG_LACP_ACTIVE = 2101 +IFACE_FF_LAG_LACP_PASSIVE = 2102 +IFACE_FF_LAG_PAGP_DESIRABLE = 2201 +IFACE_FF_LAG_PAGP_AUTO = 2202 +IFACE_FF_LAG_MLPPP = 2700 # Fibrechannel IFACE_FF_1GFC_SFP = 3010 IFACE_FF_2GFC_SFP = 3020 @@ -131,6 +138,17 @@ IFACE_FF_CHOICES = [ [IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'], ] ], + [ + 'Link aggregation', + [ + [IFACE_FF_LAG_STATIC, 'Ethernet LAG (Static)'], + [IFACE_FF_LAG_LACP_ACTIVE, 'Ethernet LAG (LACP/Active)'], + [IFACE_FF_LAG_LACP_PASSIVE, 'Ethernet LAG (LACP/Passive)'], + [IFACE_FF_LAG_PAGP_DESIRABLE, 'Ethernet LAG (PAgP/Desirable)'], + [IFACE_FF_LAG_PAGP_AUTO, 'Ethernet LAG (PAgP/Auto)'], + [IFACE_FF_LAG_MLPPP, 'Multilink PPP (MLPPP)'], + ] + ], [ 'FibreChannel', [ @@ -148,6 +166,7 @@ IFACE_FF_CHOICES = [ [IFACE_FF_E1, 'E1 (2.048 Mbps)'], [IFACE_FF_T3, 'T3 (45 Mbps)'], [IFACE_FF_E3, 'E3 (34 Mbps)'], + [IFACE_FF_E3, 'E3 (34 Mbps)'], ] ], [ @@ -167,6 +186,19 @@ IFACE_FF_CHOICES = [ ], ] +LAG_IFACE_TYPES = [ + IFACE_FF_LAG_STATIC, + IFACE_FF_LAG_LACP_ACTIVE, + IFACE_FF_LAG_LACP_PASSIVE, + IFACE_FF_LAG_PAGP_DESIRABLE, + IFACE_FF_LAG_PAGP_AUTO, + IFACE_FF_LAG_MLPPP, +] + +VIRTUAL_IFACE_TYPES = [ + IFACE_FF_VIRTUAL, +] + LAG_IFACE_TYPES + STATUS_ACTIVE = True STATUS_OFFLINE = False STATUS_CHOICES = [ @@ -1062,6 +1094,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return RPC_CLIENTS.get(self.platform.rpc_client) +# +# Console ports +# + @python_2_unicode_compatible class ConsolePort(models.Model): """ @@ -1091,6 +1127,10 @@ class ConsolePort(models.Model): ]) +# +# Console server ports +# + class ConsoleServerPortManager(models.Manager): def get_queryset(self): @@ -1123,6 +1163,10 @@ class ConsoleServerPort(models.Model): return self.name +# +# Power ports +# + @python_2_unicode_compatible class PowerPort(models.Model): """ @@ -1152,6 +1196,10 @@ class PowerPort(models.Model): ]) +# +# Power outlets +# + class PowerOutletManager(models.Manager): def get_queryset(self): @@ -1178,6 +1226,10 @@ class PowerOutlet(models.Model): return self.name +# +# Interfaces +# + @python_2_unicode_compatible class Interface(models.Model): """ @@ -1185,6 +1237,8 @@ class Interface(models.Model): of an InterfaceConnection. """ device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE) + lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL, + verbose_name='LAG Interface') name = models.CharField(max_length=30) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') @@ -1203,15 +1257,34 @@ class Interface(models.Model): def clean(self): - if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected: + # Virtual interfaces cannot be connected + if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected: raise ValidationError({ 'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the " "interface or choose a physical form factor." }) + # An interface's LAG must belong to the same device + if self.lag and self.lag.device != self.device: + raise ValidationError({ + 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( + self.lag.name, self.lag.device.name + ) + }) + + # A LAG interface cannot have a parent LAG + if self.form_factor in LAG_IFACE_TYPES and self.lag is not None: + raise ValidationError({ + 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) + }) + @property - def is_physical(self): - return self.form_factor != IFACE_FF_VIRTUAL + def is_virtual(self): + return self.form_factor in VIRTUAL_IFACE_TYPES + + @property + def is_lag(self): + return self.form_factor in LAG_IFACE_TYPES @property def is_connected(self): @@ -1275,6 +1348,10 @@ class InterfaceConnection(models.Model): ]) +# +# Device bays +# + @python_2_unicode_compatible class DeviceBay(models.Model): """ @@ -1305,6 +1382,10 @@ class DeviceBay(models.Model): raise ValidationError("Cannot install a device into itself.") +# +# Modules +# + @python_2_unicode_compatible class Module(models.Model): """ diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 604a952b7..95dc21aad 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -576,6 +576,7 @@ class InterfaceTest(APITestCase): 'device', 'name', 'form_factor', + 'parent', 'mac_address', 'mgmt_only', 'description', @@ -589,6 +590,7 @@ class InterfaceTest(APITestCase): 'device', 'name', 'form_factor', + 'parent', 'mac_address', 'mgmt_only', 'description', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index df93e61f3..243d97e92 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -66,11 +66,12 @@ class ComponentCreateView(View): def get(self, request, pk): parent = get_object_or_404(self.parent_model, pk=pk) + form = self.form(parent, initial=request.GET) return render(request, 'dcim/device_component_add.html', { 'parent': parent, 'component_type': self.model._meta.verbose_name, - 'form': self.form(initial=request.GET), + 'form': form, 'return_url': parent.get_absolute_url(), }) @@ -78,7 +79,7 @@ class ComponentCreateView(View): parent = get_object_or_404(self.parent_model, pk=pk) - form = self.form(request.POST) + form = self.form(parent, request.POST) if form.is_valid(): new_components = [] diff --git a/netbox/templates/dcim/device_component_add.html b/netbox/templates/dcim/device_component_add.html index ab8f3bb21..91f39ab9b 100644 --- a/netbox/templates/dcim/device_component_add.html +++ b/netbox/templates/dcim/device_component_add.html @@ -3,7 +3,7 @@ {% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} -{% block content %}{{ form.errors }} +{% block content %}