mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
* Initial work on interface groups * Simplify to a single LAG form factor * Correct interface serializer * Allow for bulk editing of interface LAG * Additional LAG interface validation * Fixed API tests
This commit is contained in:
parent
c61bae3a33
commit
c6970e1998
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
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 extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -227,14 +227,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
|
@ -390,13 +390,24 @@ class PowerPortNestedSerializer(PowerPortSerializer):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceSerializer(serializers.ModelSerializer):
|
class LAGInterfaceNestedSerializer(serializers.ModelSerializer):
|
||||||
device = DeviceNestedSerializer()
|
|
||||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
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):
|
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||||
@ -410,8 +421,10 @@ class InterfaceDetailSerializer(InterfaceSerializer):
|
|||||||
connected_interface = InterfaceSerializer()
|
connected_interface = InterfaceSerializer()
|
||||||
|
|
||||||
class Meta(InterfaceSerializer.Meta):
|
class Meta(InterfaceSerializer.Meta):
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
fields = [
|
||||||
'connected_interface']
|
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||||
|
'connected_interface',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -10,9 +10,9 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
|
||||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation,
|
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site,
|
||||||
RackRole, Site,
|
VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from extras.api.views import CustomFieldModelAPIView
|
from extras.api.views import CustomFieldModelAPIView
|
||||||
@ -359,9 +359,9 @@ class InterfaceListView(generics.ListAPIView):
|
|||||||
# Filter by type (physical or virtual)
|
# Filter by type (physical or virtual)
|
||||||
iface_type = self.request.query_params.get('type')
|
iface_type = self.request.query_params.get('type')
|
||||||
if iface_type == 'physical':
|
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':
|
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:
|
elif iface_type is not None:
|
||||||
queryset = queryset.empty()
|
queryset = queryset.empty()
|
||||||
|
|
||||||
|
@ -7,8 +7,9 @@ from extras.filters import CustomFieldFilterSet
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Site,
|
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',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
|
type = django_filters.MethodFilter(
|
||||||
|
action='filter_type',
|
||||||
|
label='Interface type',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name']
|
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):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.MethodFilter(
|
site = django_filters.MethodFilter(
|
||||||
|
@ -18,9 +18,10 @@ from .formfields import MACAddressFormField
|
|||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
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,
|
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))
|
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
|
# Sites
|
||||||
#
|
#
|
||||||
@ -331,7 +341,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
class ConsolePortTemplateCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
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')
|
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')
|
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')
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
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')
|
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')
|
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')
|
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')
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -1273,27 +1283,65 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
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')
|
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
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):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
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)
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
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)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
(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
|
# Initialize interface_b choices if device_b is set
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
|
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
elif self.initial.get('device_b'):
|
elif self.initial.get('device_b'):
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
|
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
).select_related(
|
||||||
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
device_b_interfaces = []
|
device_b_interfaces = []
|
||||||
self.fields['interface_b'].choices = [
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -68,6 +68,7 @@ IFACE_ORDERING_CHOICES = [
|
|||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
|
IFACE_FF_LAG = 200
|
||||||
# Ethernet
|
# Ethernet
|
||||||
IFACE_FF_100ME_FIXED = 800
|
IFACE_FF_100ME_FIXED = 800
|
||||||
IFACE_FF_1GE_FIXED = 1000
|
IFACE_FF_1GE_FIXED = 1000
|
||||||
@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [
|
|||||||
'Virtual interfaces',
|
'Virtual interfaces',
|
||||||
[
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[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_E1, 'E1 (2.048 Mbps)'],
|
||||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||||
[IFACE_FF_E3, 'E3 (34 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_ACTIVE = True
|
||||||
STATUS_OFFLINE = False
|
STATUS_OFFLINE = False
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
@ -1062,6 +1070,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console ports
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsolePort(models.Model):
|
class ConsolePort(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1091,6 +1103,10 @@ class ConsolePort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Console server ports
|
||||||
|
#
|
||||||
|
|
||||||
class ConsoleServerPortManager(models.Manager):
|
class ConsoleServerPortManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1123,6 +1139,10 @@ class ConsoleServerPort(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power ports
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerPort(models.Model):
|
class PowerPort(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1152,6 +1172,10 @@ class PowerPort(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power outlets
|
||||||
|
#
|
||||||
|
|
||||||
class PowerOutletManager(models.Manager):
|
class PowerOutletManager(models.Manager):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -1178,6 +1202,10 @@ class PowerOutlet(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interfaces
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Interface(models.Model):
|
class Interface(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1185,6 +1213,8 @@ class Interface(models.Model):
|
|||||||
of an InterfaceConnection.
|
of an InterfaceConnection.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
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)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
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')
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
@ -1203,15 +1233,42 @@ class Interface(models.Model):
|
|||||||
|
|
||||||
def clean(self):
|
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({
|
raise ValidationError({
|
||||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||||
"interface or choose a physical form factor."
|
"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
|
@property
|
||||||
def is_physical(self):
|
def is_virtual(self):
|
||||||
return self.form_factor != IFACE_FF_VIRTUAL
|
return self.form_factor in VIRTUAL_IFACE_TYPES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lag(self):
|
||||||
|
return self.form_factor == IFACE_FF_LAG
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
@ -1275,6 +1332,10 @@ class InterfaceConnection(models.Model):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device bays
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class DeviceBay(models.Model):
|
class DeviceBay(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -1305,6 +1366,10 @@ class DeviceBay(models.Model):
|
|||||||
raise ValidationError("Cannot install a device into itself.")
|
raise ValidationError("Cannot install a device into itself.")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Modules
|
||||||
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Module(models.Model):
|
class Module(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -576,6 +576,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
@ -589,6 +590,7 @@ class InterfaceTest(APITestCase):
|
|||||||
'device',
|
'device',
|
||||||
'name',
|
'name',
|
||||||
'form_factor',
|
'form_factor',
|
||||||
|
'lag',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'mgmt_only',
|
'mgmt_only',
|
||||||
'description',
|
'description',
|
||||||
|
@ -66,11 +66,12 @@ class ComponentCreateView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=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', {
|
return render(request, 'dcim/device_component_add.html', {
|
||||||
'parent': parent,
|
'parent': parent,
|
||||||
'component_type': self.model._meta.verbose_name,
|
'component_type': self.model._meta.verbose_name,
|
||||||
'form': self.form(initial=request.GET),
|
'form': form,
|
||||||
'return_url': parent.get_absolute_url(),
|
'return_url': parent.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ class ComponentCreateView(View):
|
|||||||
|
|
||||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
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():
|
if form.is_valid():
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
|
@ -396,6 +396,7 @@
|
|||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||||
|
|
||||||
{% block content %}{{ form.errors }}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -6,14 +6,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
||||||
|
{% if iface.lag %}
|
||||||
|
<span class="label label-primary">{{ iface.lag.name }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if iface.description %}
|
{% if iface.description %}
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if iface.is_lag %}
|
||||||
|
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<small>{{ iface.mac_address|default:'' }}</small>
|
<small>{{ iface.mac_address|default:'' }}</small>
|
||||||
</td>
|
</td>
|
||||||
{% if not iface.is_physical %}
|
{% if iface.is_virtual %}
|
||||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
{% elif iface.connection %}
|
{% elif iface.connection %}
|
||||||
{% with iface.connected_interface as connected_iface %}
|
{% with iface.connected_interface as connected_iface %}
|
||||||
@ -48,7 +54,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
{% if iface.is_physical %}
|
{% if not iface.is_virtual %}
|
||||||
{% if iface.connection %}
|
{% if iface.connection %}
|
||||||
{% if iface.connection.connection_status %}
|
{% if iface.connection.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">
|
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">
|
||||||
|
@ -471,7 +471,9 @@ class BulkEditView(View):
|
|||||||
return redirect(return_url)
|
return redirect(return_url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = self.form(self.cls, initial={'pk': pk_list})
|
initial_data = request.POST.copy()
|
||||||
|
initial_data['pk'] = pk_list
|
||||||
|
form = self.form(self.cls, initial=initial_data)
|
||||||
|
|
||||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||||
if not selected_objects:
|
if not selected_objects:
|
||||||
|
Loading…
Reference in New Issue
Block a user