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..bad6202c8 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 LAGInterfaceNestedSerializer(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') + lag = LAGInterfaceNestedSerializer() + + class Meta: + model = Interface + fields = [ + 'id', 'device', 'name', 'form_factor', 'lag', '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', 'lag', '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..eefea3bb2 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -7,8 +7,9 @@ from extras.filters import CustomFieldFilterSet 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, + ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, + Manufacturer, 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 == 'lag': + return queryset.filter(form_factor=IFACE_FF_LAG) + 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..efd1860e3 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, + Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, 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 + 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,27 +1283,65 @@ 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=IFACE_FF_LAG + ) + else: + self.fields['lag'].queryset = Interface.objects.order_naturally().filter( + device=self.instance.device, form_factor=IFACE_FF_LAG + ) + + +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='Parent LAG') 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=IFACE_FF_LAG + ) + else: + self.fields['lag'].queryset = Interface.objects.none() + class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) + device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) + lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG') form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) description = forms.CharField(max_length=100, required=False) class Meta: - nullable_fields = ['description'] + nullable_fields = ['lag', 'description'] + + def __init__(self, *args, **kwargs): + super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) + + # Limit LAG choices to interfaces which belong to the parent device. + if self.initial.get('device'): + self.fields['lag'].queryset = Interface.objects.filter( + device=self.initial['device'], form_factor=IFACE_FF_LAG + ) + else: + self.fields['lag'].choices = [] # @@ -1360,8 +1408,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 +1439,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 +1567,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..6f5be67a4 --- /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-27 19:55 +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'Parent LAG'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [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'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'], [200, b'Link Aggregation Group (LAG)']]], [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'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..f589c49a2 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -68,6 +68,7 @@ IFACE_ORDERING_CHOICES = [ # Virtual IFACE_FF_VIRTUAL = 0 +IFACE_FF_LAG = 200 # Ethernet IFACE_FF_100ME_FIXED = 800 IFACE_FF_1GE_FIXED = 1000 @@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [ 'Virtual interfaces', [ [IFACE_FF_VIRTUAL, 'Virtual'], + [IFACE_FF_LAG, 'Link Aggregation Group (LAG)'], ] ], [ @@ -148,6 +150,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 +170,11 @@ IFACE_FF_CHOICES = [ ], ] +VIRTUAL_IFACE_TYPES = [ + IFACE_FF_VIRTUAL, + IFACE_FF_LAG, +] + STATUS_ACTIVE = True STATUS_OFFLINE = False STATUS_CHOICES = [ @@ -1062,6 +1070,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 +1103,10 @@ class ConsolePort(models.Model): ]) +# +# Console server ports +# + class ConsoleServerPortManager(models.Manager): def get_queryset(self): @@ -1123,6 +1139,10 @@ class ConsoleServerPort(models.Model): return self.name +# +# Power ports +# + @python_2_unicode_compatible class PowerPort(models.Model): """ @@ -1152,6 +1172,10 @@ class PowerPort(models.Model): ]) +# +# Power outlets +# + class PowerOutletManager(models.Manager): def get_queryset(self): @@ -1178,6 +1202,10 @@ class PowerOutlet(models.Model): return self.name +# +# Interfaces +# + @python_2_unicode_compatible class Interface(models.Model): """ @@ -1185,6 +1213,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='Parent LAG') 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 +1233,42 @@ 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': u"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 == IFACE_FF_LAG and self.lag is not None: + raise ValidationError({ + 'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) + }) + + # Only a LAG can have LAG members + if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists(): + raise ValidationError({ + 'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format( + u", ".join([iface.name for iface in self.member_interfaces.all()]) + ) + }) + @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 == IFACE_FF_LAG @property def is_connected(self): @@ -1275,6 +1332,10 @@ class InterfaceConnection(models.Model): ]) +# +# Device bays +# + @python_2_unicode_compatible class DeviceBay(models.Model): """ @@ -1305,6 +1366,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..b7c417111 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', + 'lag', 'mac_address', 'mgmt_only', 'description', @@ -589,6 +590,7 @@ class InterfaceTest(APITestCase): 'device', 'name', 'form_factor', + 'lag', '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.html b/netbox/templates/dcim/device.html index 653575f2b..99e06177b 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -396,6 +396,7 @@ {% if perms.dcim.delete_interface %}