Closes #105: Interface groups (#919)

* 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:
Jeremy Stretch 2017-02-27 16:52:13 -05:00 committed by GitHub
parent c61bae3a33
commit c6970e1998
13 changed files with 247 additions and 52 deletions

View File

@ -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 = []

View File

@ -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',
]
#

View File

@ -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()

View File

@ -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(

View File

@ -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')

View 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),
),
]

View File

@ -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):
"""

View File

@ -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',

View File

@ -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 = []

View File

@ -396,6 +396,7 @@
{% if perms.dcim.delete_interface %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">

View File

@ -3,7 +3,7 @@
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
{% block content %}{{ form.errors }}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">

View File

@ -6,14 +6,20 @@
{% endif %}
<td>
<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 %}
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %}
{% if iface.is_lag %}
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
{% endif %}
</td>
<td>
<small>{{ iface.mac_address|default:'' }}</small>
</td>
{% if not iface.is_physical %}
{% if iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.connection %}
{% with iface.connected_interface as connected_iface %}
@ -48,7 +54,7 @@
{% endif %}
{% endif %}
{% if perms.dcim.change_interface %}
{% if iface.is_physical %}
{% if not iface.is_virtual %}
{% if iface.connection %}
{% if iface.connection.connection_status %}
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">

View File

@ -471,7 +471,9 @@ class BulkEditView(View):
return redirect(return_url)
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)
if not selected_objects: