Skip to content

Commit

Permalink
Closes #105: Interface groups (#919)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jeremystretch authored Feb 27, 2017
1 parent c61bae3 commit c6970e1
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 50 deletions.
18 changes: 11 additions & 7 deletions netbox/circuits/forms.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -227,14 +227,18 @@ def __init__(self, *args, **kwargs):

# 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 = []
Expand Down
19 changes: 16 additions & 3 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,24 @@ class Meta(PowerPortSerializer.Meta):
# Interfaces
#

class LAGInterfaceNestedSerializer(serializers.ModelSerializer):
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')

class Meta:
model = Interface
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', 'mac_address', 'mgmt_only', 'description', 'is_connected']
fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
]


class InterfaceNestedSerializer(InterfaceSerializer):
Expand All @@ -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',
]


#
Expand Down
10 changes: 5 additions & 5 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
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
Expand Down Expand Up @@ -359,9 +359,9 @@ def get_queryset(self):
# 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()

Expand Down
19 changes: 17 additions & 2 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
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,
)


Expand Down Expand Up @@ -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(
Expand Down
103 changes: 79 additions & 24 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
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
)


Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -331,7 +341,7 @@ class Meta:
}


class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
class ConsolePortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand All @@ -345,7 +355,7 @@ class Meta:
}


class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand All @@ -359,7 +369,7 @@ class Meta:
}


class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
class PowerPortTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand All @@ -373,7 +383,7 @@ class Meta:
}


class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
class PowerOutletTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand All @@ -387,7 +397,7 @@ class Meta:
}


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')
Expand All @@ -411,7 +421,7 @@ class Meta:
}


class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
class DeviceBayTemplateCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down Expand Up @@ -743,7 +753,7 @@ class Meta:
}


class ConsolePortCreateForm(BootstrapMixin, forms.Form):
class ConsolePortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down Expand Up @@ -914,7 +924,7 @@ class Meta:
}


class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
class ConsoleServerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down Expand Up @@ -1012,7 +1022,7 @@ class Meta:
}


class PowerPortCreateForm(BootstrapMixin, forms.Form):
class PowerPortCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down Expand Up @@ -1181,7 +1191,7 @@ class Meta:
}


class PowerOutletCreateForm(BootstrapMixin, forms.Form):
class PowerOutletCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down Expand Up @@ -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)

# 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(BootstrapMixin, forms.Form):
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 = []


#
Expand Down Expand Up @@ -1360,8 +1408,11 @@ def __init__(self, device_a, *args, **kwargs):
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
]
Expand All @@ -1388,13 +1439,17 @@ def __init__(self, device_a, *args, **kwargs):

# 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 = [
Expand Down Expand Up @@ -1512,7 +1567,7 @@ class Meta:
}


class DeviceBayCreateForm(BootstrapMixin, forms.Form):
class DeviceBayCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')


Expand Down
Loading

0 comments on commit c6970e1

Please sign in to comment.