mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on #20 - patch panels
This commit is contained in:
@ -3,15 +3,13 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.constants import (
|
||||
CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES,
|
||||
RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||
)
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
DeviceBayTemplate, DeviceType, DeviceRole, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipam.models import IPAddress, VLAN
|
||||
@ -229,8 +227,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'instance_count',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'is_patch_panel', 'subdevice_role', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'instance_count',
|
||||
]
|
||||
|
||||
|
||||
@ -304,6 +302,49 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
#
|
||||
# Rear panel port templates
|
||||
#
|
||||
|
||||
class RearPanelPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PANELPORT_TYPE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'positions']
|
||||
|
||||
|
||||
class NestedRearPanelPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearpanelporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Front panel port templates
|
||||
#
|
||||
|
||||
class FrontPanelPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=PANELPORT_TYPE_CHOICES)
|
||||
rear_port = NestedRearPanelPortTemplateSerializer()
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
|
||||
|
||||
class NestedFrontPanelPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontpanelporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Device bay templates
|
||||
#
|
||||
@ -634,6 +675,51 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# Rear panel ports
|
||||
#
|
||||
|
||||
class RearPanelPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PANELPORT_TYPE_CHOICES)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPort
|
||||
fields = ['id', 'device', 'name', 'type', 'positions', 'tags']
|
||||
|
||||
|
||||
class NestedRearPanelPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearpanelport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPort
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Front panel ports
|
||||
#
|
||||
|
||||
class FrontPanelPortSerializer(ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PANELPORT_TYPE_CHOICES)
|
||||
rear_port = NestedRearPanelPortSerializer()
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPort
|
||||
fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'tags']
|
||||
|
||||
|
||||
class NestedFrontPanelPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontpanelport-detail')
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPort
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
@ -37,6 +37,8 @@ router.register(r'console-server-port-templates', views.ConsoleServerPortTemplat
|
||||
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
|
||||
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
|
||||
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
|
||||
router.register(r'front-panel-port-templates', views.FrontPanelPortTemplateViewSet)
|
||||
router.register(r'rear-panel-port-templates', views.RearPanelPortTemplateViewSet)
|
||||
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||
|
||||
# Devices
|
||||
@ -50,6 +52,8 @@ router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
|
||||
router.register(r'power-ports', views.PowerPortViewSet)
|
||||
router.register(r'power-outlets', views.PowerOutletViewSet)
|
||||
router.register(r'interfaces', views.InterfaceViewSet)
|
||||
router.register(r'front-panel-ports', views.FrontPanelPortViewSet)
|
||||
router.register(r'rear-panel-ports', views.RearPanelPortViewSet)
|
||||
router.register(r'device-bays', views.DeviceBayViewSet)
|
||||
router.register(r'inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
|
@ -14,9 +14,10 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
from dcim import filters
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
@ -191,6 +192,18 @@ class InterfaceTemplateViewSet(ModelViewSet):
|
||||
filter_class = filters.InterfaceTemplateFilter
|
||||
|
||||
|
||||
class FrontPanelPortTemplateViewSet(ModelViewSet):
|
||||
queryset = FrontPanelPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.FrontPanelPortTemplateSerializer
|
||||
filter_class = filters.FrontPanelPortTemplateFilter
|
||||
|
||||
|
||||
class RearPanelPortTemplateViewSet(ModelViewSet):
|
||||
queryset = RearPanelPortTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.RearPanelPortTemplateSerializer
|
||||
filter_class = filters.RearPanelPortTemplateFilter
|
||||
|
||||
|
||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
@ -352,6 +365,18 @@ class InterfaceViewSet(ModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class FrontPanelPortViewSet(ModelViewSet):
|
||||
queryset = FrontPanelPort.objects.select_related('device__device_type__manufacturer', 'rear_port')
|
||||
serializer_class = serializers.FrontPanelPortSerializer
|
||||
filter_class = filters.FrontPanelPortFilter
|
||||
|
||||
|
||||
class RearPanelPortViewSet(ModelViewSet):
|
||||
queryset = RearPanelPort.objects.select_related('device__device_type__manufacturer')
|
||||
serializer_class = serializers.RearPanelPortSerializer
|
||||
filter_class = filters.RearPanelPortFilter
|
||||
|
||||
|
||||
class DeviceBayViewSet(ModelViewSet):
|
||||
queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags')
|
||||
serializer_class = serializers.DeviceBaySerializer
|
||||
|
@ -209,6 +209,36 @@ IFACE_MODE_CHOICES = [
|
||||
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
|
||||
]
|
||||
|
||||
# Patch panel port types
|
||||
PANELPORT_TYPE_8P8C = 1000
|
||||
PANELPORT_TYPE_ST = 2000
|
||||
PANELPORT_TYPE_SC_SIMPLEX = 2100
|
||||
PANELPORT_TYPE_SC_DUPLEX = 2110
|
||||
PANELPORT_TYPE_FC = 2200
|
||||
PANELPORT_TYPE_LC = 2300
|
||||
PANELPORT_TYPE_MTRJ = 2400
|
||||
PANELPORT_TYPE_MPO = 2500
|
||||
PANELPORT_TYPE_CHOICES = [
|
||||
[
|
||||
'Copper',
|
||||
[
|
||||
[PANELPORT_TYPE_8P8C, '8P8C'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Fiber Optic',
|
||||
[
|
||||
[PANELPORT_TYPE_ST, 'ST'],
|
||||
[PANELPORT_TYPE_SC_SIMPLEX, 'SC (Simplex)'],
|
||||
[PANELPORT_TYPE_SC_DUPLEX, 'SC (Duplex)'],
|
||||
[PANELPORT_TYPE_FC, 'FC'],
|
||||
[PANELPORT_TYPE_LC, 'LC'],
|
||||
[PANELPORT_TYPE_MTRJ, 'MTRJ'],
|
||||
[PANELPORT_TYPE_MPO, 'MPO'],
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
# Device statuses
|
||||
DEVICE_STATUS_OFFLINE = 0
|
||||
DEVICE_STATUS_ACTIVE = 1
|
||||
|
@ -14,9 +14,10 @@ from .constants import (
|
||||
)
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@ -368,6 +369,20 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
fields = ['name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class FrontPanelPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPortTemplate
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class RearPanelPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPortTemplate
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
@ -667,6 +682,20 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class FrontPanelPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPort
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class RearPanelPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPort
|
||||
fields = ['name', 'type']
|
||||
|
||||
|
||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
|
@ -5,6 +5,8 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.db.models import Count, Q
|
||||
from mptt.forms import TreeNodeChoiceField
|
||||
from natsort import natsorted
|
||||
from operator import attrgetter
|
||||
from taggit.forms import TagField
|
||||
from timezone_field import TimeZoneFormField
|
||||
|
||||
@ -19,17 +21,13 @@ from utilities.forms import (
|
||||
FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .constants import (
|
||||
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_FF_LAG,
|
||||
IFACE_MODE_ACCESS, IFACE_MODE_CHOICES, IFACE_MODE_TAGGED_ALL, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES,
|
||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||
SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
|
||||
)
|
||||
from .constants import *
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||
RackRole, Region, Site, VirtualChassis
|
||||
Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
DEVICE_BY_PK_RE = r'{\d+\}'
|
||||
@ -532,7 +530,7 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'interface_ordering', 'comments', 'tags',
|
||||
'is_network_device', 'is_patch_panel', 'subdevice_role', 'interface_ordering', 'comments', 'tags',
|
||||
]
|
||||
labels = {
|
||||
'interface_ordering': 'Order interfaces by',
|
||||
@ -582,6 +580,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
|
||||
is_network_device = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
||||
)
|
||||
is_patch_panel = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a patch panel'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@ -602,6 +603,9 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
is_network_device = forms.BooleanField(
|
||||
required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
is_patch_panel = forms.BooleanField(
|
||||
required=False, label='Is a patch panel', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
subdevice_role = forms.NullBooleanField(
|
||||
required=False, label='Subdevice role', widget=forms.Select(choices=(
|
||||
('', '---------'),
|
||||
@ -696,6 +700,97 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class FrontPanelPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPortTemplate
|
||||
fields = ['device_type', 'name', 'type', 'rear_port', 'rear_port_position']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class FrontPanelPortTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(FrontPanelPortTemplateCreateForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in self.parent.front_panel_port_templates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = natsorted(RearPanelPortTemplate.objects.filter(device_type=self.parent), key=attrgetter('name'))
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class RearPanelPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPortTemplate
|
||||
fields = ['device_type', 'name', 'type', 'positions']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class RearPanelPortTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=64,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
@ -2087,6 +2182,122 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||
return interface
|
||||
|
||||
|
||||
#
|
||||
# Front panel ports
|
||||
#
|
||||
|
||||
class FrontPanelPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = FrontPanelPort
|
||||
fields = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'tags']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
# TODO: Merge with FrontPanelPortTemplateCreateForm to remove duplicate logic
|
||||
class FrontPanelPortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
rear_port_set = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label='Rear ports',
|
||||
help_text='Select one rear port assignment for each front port being created.'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(FrontPanelPortCreateForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in self.parent.front_panel_port_templates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = natsorted(RearPanelPort.objects.filter(device=self.parent), key=attrgetter('name'))
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port_set'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate that the number of ports being created equals the number of selected (rear port, position) tuples
|
||||
front_port_count = len(self.cleaned_data['name_pattern'])
|
||||
rear_port_count = len(self.cleaned_data['rear_port_set'])
|
||||
if front_port_count != rear_port_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
|
||||
'were selected. These counts must match.'.format(front_port_count, rear_port_count)
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
}
|
||||
|
||||
|
||||
class FrontPanelPortBulkRenameForm(BulkRenameForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=FrontPanelPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Rear panel ports
|
||||
#
|
||||
|
||||
class RearPanelPortForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = TagField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = RearPanelPort
|
||||
fields = ['device', 'name', 'type', 'positions', 'tags']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class RearPanelPortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
positions = forms.IntegerField(
|
||||
min_value=1,
|
||||
max_value=64,
|
||||
initial=1,
|
||||
help_text='The number of front ports which may be mapped to each rear port'
|
||||
)
|
||||
|
||||
|
||||
class RearPanelPortBulkRenameForm(BulkRenameForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=RearPanelPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
114
netbox/dcim/migrations/0065_patch_panel_ports.py
Normal file
114
netbox/dcim/migrations/0065_patch_panel_ports.py
Normal file
@ -0,0 +1,114 @@
|
||||
# Generated by Django 2.0.8 on 2018-10-03 17:26
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('dcim', '0064_remove_platform_rpc_client'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FrontPanelPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='front_panel_ports', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FrontPanelPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RearPanelPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rear_panel_ports', to='dcim.Device')),
|
||||
('tags', taggit.managers.TaggableManager(through='taggit.TaggedItem', to='taggit.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RearPanelPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64)),
|
||||
('type', models.PositiveSmallIntegerField()),
|
||||
('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='is_patch_panel',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearpanelporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rear_panel_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontpanelporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='front_panel_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontpanelporttemplate',
|
||||
name='rear_port',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='front_panel_port_templates', to='dcim.RearPanelPortTemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontpanelport',
|
||||
name='rear_port',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='front_panel_ports', to='dcim.RearPanelPort'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontpanelport',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(through='taggit.TaggedItem', to='taggit.Tag'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rearpanelporttemplate',
|
||||
unique_together={('device_type', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rearpanelport',
|
||||
unique_together={('device', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='frontpanelporttemplate',
|
||||
unique_together={('rear_port', 'rear_port_position'), ('device_type', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='frontpanelport',
|
||||
unique_together={('device', 'name'), ('rear_port', 'rear_port_position')},
|
||||
),
|
||||
]
|
@ -769,6 +769,11 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
verbose_name='Is a network device',
|
||||
help_text='This type of device has network interfaces'
|
||||
)
|
||||
is_patch_panel = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='Is a patch panel',
|
||||
help_text='This type of device has patch panel ports'
|
||||
)
|
||||
subdevice_role = models.NullBooleanField(
|
||||
default=None,
|
||||
verbose_name='Parent/child status',
|
||||
@ -789,7 +794,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
csv_headers = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments',
|
||||
'is_pdu', 'is_network_device', 'is_patch_panel', 'subdevice_role', 'interface_ordering', 'comments',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -822,6 +827,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
self.is_console_server,
|
||||
self.is_pdu,
|
||||
self.is_network_device,
|
||||
self.is_patch_panel,
|
||||
self.get_subdevice_role_display() if self.subdevice_role else None,
|
||||
self.get_interface_ordering_display(),
|
||||
self.comments,
|
||||
@ -861,6 +867,14 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
"device before declassifying it as a network device."
|
||||
})
|
||||
|
||||
if not self.is_patch_panel and (
|
||||
self.front_panel_port_templates.exists() or self.rear_panel_port_templates.exists()
|
||||
):
|
||||
raise ValidationError({
|
||||
'is_patch_panel': "Must delete all patch panel port templates associated with this device before "
|
||||
"declassifying it as a network device."
|
||||
})
|
||||
|
||||
if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count():
|
||||
raise ValidationError({
|
||||
'subdevice_role': "Must delete all device bay templates associated with this device before "
|
||||
@ -1000,6 +1014,86 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
return self.name
|
||||
|
||||
|
||||
class FrontPanelPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a front patch panel port on a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='front_panel_port_templates'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPanelPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='front_panel_port_templates'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = [
|
||||
['device_type', 'name'],
|
||||
['rear_port', 'rear_port_position'],
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
"Rear port ({}) must belong to the same device type".format(self.rear_port)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RearPanelPortTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a rear patch panel port on a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='rear_panel_port_templates'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DeviceBayTemplate(ComponentTemplateModel):
|
||||
"""
|
||||
A template for a DeviceBay to be created for a new parent Device.
|
||||
@ -1417,6 +1511,23 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
[Interface(device=self, name=template.name, form_factor=template.form_factor,
|
||||
mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
|
||||
)
|
||||
RearPanelPort.objects.bulk_create([
|
||||
RearPanelPort(
|
||||
device=self,
|
||||
name=template.name,
|
||||
type=template.type,
|
||||
positions=template.positions
|
||||
) for template in self.device_type.rear_panel_port_templates.all()
|
||||
])
|
||||
FrontPanelPort.objects.bulk_create([
|
||||
FrontPanelPort(
|
||||
device=self,
|
||||
name=template.name,
|
||||
type=template.type,
|
||||
rear_port=RearPanelPort.objects.get(device=self, name=template.rear_port.name),
|
||||
rear_port_position=template.rear_port_position,
|
||||
) for template in self.device_type.front_panel_port_templates.all()
|
||||
])
|
||||
DeviceBay.objects.bulk_create(
|
||||
[DeviceBay(device=self, name=template.name) for template in
|
||||
self.device_type.device_bay_templates.all()]
|
||||
@ -2040,6 +2151,94 @@ class InterfaceConnection(models.Model):
|
||||
).save()
|
||||
|
||||
|
||||
#
|
||||
# Patch panel ports
|
||||
#
|
||||
|
||||
class FrontPanelPort(ComponentModel):
|
||||
"""
|
||||
A port on the front of a patch panel.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='front_panel_ports'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPanelPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='front_panel_ports'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = [
|
||||
['device', 'name'],
|
||||
['rear_port', 'rear_port_position'],
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
raise ValidationError(
|
||||
"Rear port ({}) must belong to the same device".format(self.rear_port)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
"Invalid rear port position ({}); rear port {} has only {} positions".format(
|
||||
self.rear_port_position, self.rear_port.name, self.rear_port.positions
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RearPanelPort(ComponentModel):
|
||||
"""
|
||||
A port on the rear of a patch panel.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='rear_panel_ports'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=PANELPORT_TYPE_CHOICES
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
@ -5,9 +5,10 @@ from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem,
|
||||
Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, Region, Site, VirtualChassis,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
REGION_LINK = """
|
||||
@ -348,6 +349,7 @@ class DeviceTypeTable(BaseTable):
|
||||
is_console_server = BooleanColumn(verbose_name='CS')
|
||||
is_pdu = BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = BooleanColumn(verbose_name='Net')
|
||||
is_patch_panel = BooleanColumn(verbose_name='PP')
|
||||
subdevice_role = tables.TemplateColumn(
|
||||
template_code=SUBDEVICE_ROLE_TEMPLATE,
|
||||
verbose_name='Subdevice Role'
|
||||
@ -361,7 +363,7 @@ class DeviceTypeTable(BaseTable):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count',
|
||||
'is_network_device', 'is_patch_panel', 'subdevice_role', 'instance_count',
|
||||
)
|
||||
|
||||
|
||||
@ -415,6 +417,24 @@ class InterfaceTemplateTable(BaseTable):
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class FrontPanelPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPanelPortTemplate
|
||||
fields = ('pk', 'name', 'type', 'rear_port', 'rear_port_position')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPanelPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPanelPortTemplate
|
||||
fields = ('pk', 'name', 'type', 'positions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
|
||||
@ -574,6 +594,22 @@ class InterfaceTable(BaseTable):
|
||||
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class FrontPanelPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPanelPort
|
||||
fields = ('name', 'type', 'rear_port', 'rear_port_position')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class RearPanelPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPanelPort
|
||||
fields = ('name', 'type', 'positions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -109,6 +109,14 @@ urlpatterns = [
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Front panel port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/front-panel-ports/add/$', views.FrontPanelPortTemplateCreateView.as_view(), name='devicetype_add_frontpanelport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/front-panel-ports/delete/$', views.FrontPanelPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontpanelport'),
|
||||
|
||||
# Front panel port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/rear-panel-ports/add/$', views.RearPanelPortTemplateCreateView.as_view(), name='devicetype_add_rearpanelport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/rear-panel-ports/delete/$', views.RearPanelPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearpanelport'),
|
||||
|
||||
# Device bay templates
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
@ -204,6 +212,22 @@ urlpatterns = [
|
||||
url(r'^interfaces/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
url(r'^interfaces/rename/$', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
|
||||
# Front panel ports
|
||||
# url(r'^devices/front-panel-ports/add/$', views.DeviceBulkAddFrontPanelPortView.as_view(), name='device_bulk_add_frontpanelport'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-panel-ports/add/$', views.FrontPanelPortCreateView.as_view(), name='frontpanelport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/front-panel-ports/delete/$', views.FrontPanelPortBulkDeleteView.as_view(), name='frontpanelport_bulk_delete'),
|
||||
url(r'^front-panel-ports/(?P<pk>\d+)/edit/$', views.FrontPanelPortEditView.as_view(), name='frontpanelport_edit'),
|
||||
url(r'^front-panel-ports/(?P<pk>\d+)/delete/$', views.FrontPanelPortDeleteView.as_view(), name='frontpanelport_delete'),
|
||||
url(r'^front-panel-ports/rename/$', views.FrontPanelPortBulkRenameView.as_view(), name='frontpanelport_bulk_rename'),
|
||||
|
||||
# Rear panel ports
|
||||
# url(r'^devices/rear-panel-ports/add/$', views.DeviceBulkAddRearPanelPortView.as_view(), name='device_bulk_add_rearpanelport'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-panel-ports/add/$', views.RearPanelPortCreateView.as_view(), name='rearpanelport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/rear-panel-ports/delete/$', views.RearPanelPortBulkDeleteView.as_view(), name='rearpanelport_bulk_delete'),
|
||||
url(r'^rear-panel-ports/(?P<pk>\d+)/edit/$', views.RearPanelPortEditView.as_view(), name='rearpanelport_edit'),
|
||||
url(r'^rear-panel-ports/(?P<pk>\d+)/delete/$', views.RearPanelPortDeleteView.as_view(), name='rearpanelport_delete'),
|
||||
url(r'^rear-panel-ports/rename/$', views.RearPanelPortBulkRenameView.as_view(), name='rearpanelport_bulk_rename'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
|
@ -31,9 +31,10 @@ from . import filters, forms, tables
|
||||
from .constants import CONNECTION_STATUS_CONNECTED
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site, VirtualChassis,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
|
||||
@ -559,6 +560,14 @@ class DeviceTypeView(View):
|
||||
).filter(device_type=devicetype)),
|
||||
orderable=False
|
||||
)
|
||||
front_panel_port_table = tables.FrontPanelPortTemplateTable(
|
||||
natsorted(FrontPanelPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
orderable=False
|
||||
)
|
||||
rear_panel_port_table = tables.RearPanelPortTemplateTable(
|
||||
natsorted(RearPanelPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
orderable=False
|
||||
)
|
||||
devicebay_table = tables.DeviceBayTemplateTable(
|
||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
orderable=False
|
||||
@ -569,6 +578,8 @@ class DeviceTypeView(View):
|
||||
powerport_table.columns.show('pk')
|
||||
poweroutlet_table.columns.show('pk')
|
||||
interface_table.columns.show('pk')
|
||||
front_panel_port_table.columns.show('pk')
|
||||
rear_panel_port_table.columns.show('pk')
|
||||
devicebay_table.columns.show('pk')
|
||||
|
||||
return render(request, 'dcim/devicetype.html', {
|
||||
@ -578,6 +589,8 @@ class DeviceTypeView(View):
|
||||
'powerport_table': powerport_table,
|
||||
'poweroutlet_table': poweroutlet_table,
|
||||
'interface_table': interface_table,
|
||||
'front_panel_port_table': front_panel_port_table,
|
||||
'rear_panel_port_table': rear_panel_port_table,
|
||||
'devicebay_table': devicebay_table,
|
||||
})
|
||||
|
||||
@ -721,6 +734,40 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
table = tables.InterfaceTemplateTable
|
||||
|
||||
|
||||
class FrontPanelPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_frontpanelporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = FrontPanelPortTemplate
|
||||
form = forms.FrontPanelPortTemplateCreateForm
|
||||
model_form = forms.FrontPanelPortTemplateForm
|
||||
template_name = 'dcim/device_component_add.html'
|
||||
|
||||
|
||||
class FrontPanelPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontpanelporttemplate'
|
||||
queryset = FrontPanelPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.FrontPanelPortTemplateTable
|
||||
|
||||
|
||||
class RearPanelPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_rearpanelporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = RearPanelPortTemplate
|
||||
form = forms.RearPanelPortTemplateCreateForm
|
||||
model_form = forms.RearPanelPortTemplateForm
|
||||
template_name = 'dcim/device_component_add.html'
|
||||
|
||||
|
||||
class RearPanelPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearpanelporttemplate'
|
||||
queryset = RearPanelPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.RearPanelPortTemplateTable
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebaytemplate'
|
||||
parent_model = DeviceType
|
||||
@ -859,6 +906,12 @@ class DeviceView(View):
|
||||
'circuit_termination__circuit'
|
||||
).prefetch_related('ip_addresses')
|
||||
|
||||
# Front panel ports
|
||||
front_panel_ports = device.front_panel_ports.select_related('rear_port')
|
||||
|
||||
# Rear panel ports
|
||||
rear_panel_ports = device.rear_panel_ports.all()
|
||||
|
||||
# Device bays
|
||||
device_bays = natsorted(
|
||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||
@ -891,6 +944,8 @@ class DeviceView(View):
|
||||
'power_outlets': power_outlets,
|
||||
'interfaces': interfaces,
|
||||
'device_bays': device_bays,
|
||||
'front_panel_ports': front_panel_ports,
|
||||
'rear_panel_ports': rear_panel_ports,
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'vc_members': vc_members,
|
||||
@ -1701,6 +1756,82 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
table = tables.InterfaceTable
|
||||
|
||||
|
||||
#
|
||||
# Front panel ports
|
||||
#
|
||||
|
||||
class FrontPanelPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_frontpanelport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = FrontPanelPort
|
||||
form = forms.FrontPanelPortCreateForm
|
||||
model_form = forms.FrontPanelPortForm
|
||||
template_name = 'dcim/device_component_add.html'
|
||||
|
||||
|
||||
class FrontPanelPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_frontpanelport'
|
||||
model = FrontPanelPort
|
||||
model_form = forms.FrontPanelPortForm
|
||||
|
||||
|
||||
class FrontPanelPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_frontpanelport'
|
||||
model = FrontPanelPort
|
||||
|
||||
|
||||
class FrontPanelPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_frontpanelport'
|
||||
queryset = FrontPanelPort.objects.all()
|
||||
form = forms.FrontPanelPortBulkRenameForm
|
||||
|
||||
|
||||
class FrontPanelPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontpanelport'
|
||||
queryset = FrontPanelPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.FrontPanelPortTable
|
||||
|
||||
|
||||
#
|
||||
# Rear panel ports
|
||||
#
|
||||
|
||||
class RearPanelPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_rearpanelport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = RearPanelPort
|
||||
form = forms.RearPanelPortCreateForm
|
||||
model_form = forms.RearPanelPortForm
|
||||
template_name = 'dcim/device_component_add.html'
|
||||
|
||||
|
||||
class RearPanelPortEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rearpanelport'
|
||||
model = RearPanelPort
|
||||
model_form = forms.RearPanelPortForm
|
||||
|
||||
|
||||
class RearPanelPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rearpanelport'
|
||||
model = RearPanelPort
|
||||
|
||||
|
||||
class RearPanelPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
permission_required = 'dcim.change_rearpanelport'
|
||||
queryset = RearPanelPort.objects.all()
|
||||
form = forms.RearPanelPortBulkRenameForm
|
||||
|
||||
|
||||
class RearPanelPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearpanelport'
|
||||
queryset = RearPanelPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.RearPanelPortTable
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
@ -689,6 +689,111 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if front_panel_ports or device.device_type.is_patch_panel %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Front Panel Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_frontpanelport or perms.dcim.delete_frontpanelport %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Rear Port</th>
|
||||
<th>Position</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for frontpanelport in front_panel_ports %}
|
||||
{% include 'dcim/inc/frontpanelport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">— No front panel ports defined —</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{% if front_panel_ports and perms.dcim.change_frontpanelport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontpanelport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if front_panel_ports and perms.dcim.delete_frontpanelport %}
|
||||
<button type="submit" formaction="{% url 'dcim:frontpanelport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_frontpanelport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:frontpanelport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front panel ports
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if rear_panel_ports or device.device_type.is_patch_panel %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rear Panel Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover table-headings panel-body component-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if perms.dcim.change_rearpanelport or perms.dcim.delete_rearpanelport %}
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Positions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rearpanelport in rear_panel_ports %}
|
||||
{% include 'dcim/inc/rearpanelport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">— No rear panel ports defined —</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{% if rear_panel_ports and perms.dcim.change_rearpanelport %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearpanelport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if rear_panel_ports and perms.dcim.delete_rearpanelport %}
|
||||
<button type="submit" formaction="{% url 'dcim:rearpanelport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_rearpanelport %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'dcim:rearpanelport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear panel ports
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/graphs_modal.html' %}
|
||||
|
@ -135,6 +135,19 @@
|
||||
<small class="text-muted">This device {% if devicetype.is_network_device %}has{% else %}does not have{% endif %} network interfaces</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right">
|
||||
{% if devicetype.is_patch_panel %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<strong>Patch Panel</strong><br />
|
||||
<small class="text-muted">This device {% if devicetype.is_patch_panel %}has{% else %}does not have{% endif %} patch panel ports</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right">
|
||||
{% if devicetype.subdevice_role == True %}
|
||||
@ -188,6 +201,12 @@
|
||||
{% if devicetype.is_pdu or poweroutlet_table.rows %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_patch_panel or front_panel_port_table.rows %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=front_panel_port_table title='Front Panel Ports' add_url='dcim:devicetype_add_frontpanelport' delete_url='dcim:devicetype_delete_frontpanelport' %}
|
||||
{% endif %}
|
||||
{% if devicetype.is_patch_panel or rear_panel_port_table.rows %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=rear_panel_port_table title='Rear Panel Ports' add_url='dcim:devicetype_add_rearpanelport' delete_url='dcim:devicetype_delete_rearpanelport' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -20,6 +20,7 @@
|
||||
{% render_field form.is_console_server %}
|
||||
{% render_field form.is_pdu %}
|
||||
{% render_field form.is_network_device %}
|
||||
{% render_field form.is_patch_panel %}
|
||||
{% render_field form.subdevice_role %}
|
||||
</div>
|
||||
</div>
|
||||
|
25
netbox/templates/dcim/inc/frontpanelport.html
Normal file
25
netbox/templates/dcim/inc/frontpanelport.html
Normal file
@ -0,0 +1,25 @@
|
||||
<tr class="frontpanelport">
|
||||
{% if perms.dcim.change_frontpanelport or perms.dcim.delete_frontpanelport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ frontpanelport.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square-o"></i> {{ frontpanelport }}
|
||||
</td>
|
||||
<td>{{ frontpanelport.get_type_display }}</td>
|
||||
<td>{{ frontpanelport.rear_port }}</td>
|
||||
<td>{{ frontpanelport.rear_port_position }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.change_frontpanelport %}
|
||||
<a href="{% url 'dcim:frontpanelport_edit' pk=frontpanelport.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_frontpanelport %}
|
||||
<a href="{% url 'dcim:frontpanelport_delete' pk=frontpanelport.pk %}?return_url={{ device.get_absolute_url }}" title="Delete port" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
24
netbox/templates/dcim/inc/rearpanelport.html
Normal file
24
netbox/templates/dcim/inc/rearpanelport.html
Normal file
@ -0,0 +1,24 @@
|
||||
<tr class="rearpanelport">
|
||||
{% if perms.dcim.change_rearpanelport or perms.dcim.delete_rearpanelport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ rearpanelport.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-square-o"></i> {{ rearpanelport }}
|
||||
</td>
|
||||
<td>{{ rearpanelport.get_type_display }}</td>
|
||||
<td>{{ rearpanelport.positions }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.dcim.change_rearpanelport %}
|
||||
<a href="{% url 'dcim:rearpanelport_edit' pk=rearpanelport.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_rearpanelport %}
|
||||
<a href="{% url 'dcim:rearpanelport_delete' pk=rearpanelport.pk %}?return_url={{ device.get_absolute_url }}" title="Delete port" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
@ -643,6 +643,9 @@ class ComponentForm(BootstrapMixin, forms.Form):
|
||||
self.parent = parent
|
||||
super(ComponentForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
return {}
|
||||
|
||||
|
||||
class BulkEditForm(forms.Form):
|
||||
"""
|
||||
|
@ -711,10 +711,11 @@ class ComponentCreateView(View):
|
||||
data = deepcopy(request.POST)
|
||||
data[self.parent_field] = parent.pk
|
||||
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
for i, name in enumerate(form.cleaned_data['name_pattern']):
|
||||
|
||||
# Initialize the individual component form
|
||||
data['name'] = name
|
||||
data.update(form.get_iterative_data(i))
|
||||
component_form = self.model_form(data)
|
||||
|
||||
if component_form.is_valid():
|
||||
|
Reference in New Issue
Block a user