Initial work on #91: Support for subdevices

This commit is contained in:
Jeremy Stretch 2016-07-01 17:12:43 -04:00
parent 35c5423127
commit 0123dbcf5f
17 changed files with 584 additions and 20 deletions

View File

@ -2,9 +2,9 @@ from django.contrib import admin
from django.db.models import Count from django.db.models import Count
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
Interface, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerPortTemplate, Rack, RackGroup, Site, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
) )
@ -61,6 +61,10 @@ class InterfaceTemplateAdmin(admin.TabularInline):
model = InterfaceTemplate model = InterfaceTemplate
class DeviceBayTemplateAdmin(admin.TabularInline):
model = DeviceBayTemplate
@admin.register(DeviceType) @admin.register(DeviceType)
class DeviceTypeAdmin(admin.ModelAdmin): class DeviceTypeAdmin(admin.ModelAdmin):
prepopulated_fields = { prepopulated_fields = {
@ -72,9 +76,10 @@ class DeviceTypeAdmin(admin.ModelAdmin):
PowerPortTemplateAdmin, PowerPortTemplateAdmin,
PowerOutletTemplateAdmin, PowerOutletTemplateAdmin,
InterfaceTemplateAdmin, InterfaceTemplateAdmin,
DeviceBayTemplateAdmin,
] ]
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports', list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
'power_outlets', 'interfaces'] 'power_outlets', 'interfaces', 'device_bays']
list_filter = ['manufacturer'] list_filter = ['manufacturer']
def get_queryset(self, request): def get_queryset(self, request):
@ -84,6 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
power_port_count=Count('power_port_templates', distinct=True), power_port_count=Count('power_port_templates', distinct=True),
power_outlet_count=Count('power_outlet_templates', distinct=True), power_outlet_count=Count('power_outlet_templates', distinct=True),
interface_count=Count('interface_templates', distinct=True), interface_count=Count('interface_templates', distinct=True),
devicebay_count=Count('devicebay_templates', distinct=True),
) )
def console_ports(self, instance): def console_ports(self, instance):
@ -101,6 +107,9 @@ class DeviceTypeAdmin(admin.ModelAdmin):
def interfaces(self, instance): def interfaces(self, instance):
return instance.interface_count return instance.interface_count
def device_bays(self, instance):
return instance.devicebay_count
# #
# Devices # Devices
@ -144,6 +153,11 @@ class InterfaceAdmin(admin.TabularInline):
model = Interface model = Interface
class DeviceBayAdmin(admin.TabularInline):
model = DeviceBay
fk_name = 'device'
class ModuleAdmin(admin.TabularInline): class ModuleAdmin(admin.TabularInline):
model = Module model = Module
readonly_fields = ['parent', 'discovered'] readonly_fields = ['parent', 'discovered']
@ -157,6 +171,7 @@ class DeviceAdmin(admin.ModelAdmin):
PowerPortAdmin, PowerPortAdmin,
PowerOutletAdmin, PowerOutletAdmin,
InterfaceAdmin, InterfaceAdmin,
DeviceBayAdmin,
ModuleAdmin, ModuleAdmin,
] ]
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial'] list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']

View File

@ -10,10 +10,10 @@ from utilities.forms import (
) )
from .models import ( from .models import (
CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, IFACE_FF_VIRTUAL, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
) )
@ -216,7 +216,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device'] 'is_network_device', 'subdevice_role']
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin): class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
@ -283,6 +283,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
fields = ['name_pattern', 'form_factor', 'mgmt_only'] fields = ['name_pattern', 'form_factor', 'mgmt_only']
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
name_pattern = ExpandableNameField(label='Name')
class Meta:
model = DeviceBayTemplate
fields = ['name_pattern']
# #
# Device roles # Device roles
# #
@ -1080,6 +1088,41 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
#
# Device bays
#
class DeviceBayForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = DeviceBay
fields = ['device', 'name']
widgets = {
'device': forms.HiddenInput(),
}
class DeviceBayCreateForm(forms.Form, BootstrapMixin):
name_pattern = ExpandableNameField(label='Name')
class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
help_text="Child devices must first be created within the rack occupied "
"by the parent device. Then they can be assigned to a bay.")
def __init__(self, device_bay, *args, **kwargs):
super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
children_queryset = Device.objects.filter(rack=device_bay.device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
.exclude(pk=device_bay.device.pk)
self.fields['installed_device'].queryset = children_queryset
# #
# Connections # Connections
# #

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-01 20:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0003_auto_20160628_1721'),
]
operations = [
migrations.CreateModel(
name='DeviceBay',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name=b'Name')),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')),
('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')),
],
options={
'ordering': ['device', 'name'],
},
),
migrations.CreateModel(
name='DeviceBayTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30)),
],
options={
'ordering': ['device_type', 'name'],
},
),
migrations.AddField(
model_name='devicetype',
name='subdevice_role',
field=models.NullBooleanField(choices=[(None, b'N/A'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'),
),
migrations.AddField(
model_name='devicebaytemplate',
name='device_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType'),
),
migrations.AlterUniqueTogether(
name='devicebaytemplate',
unique_together=set([('device_type', 'name')]),
),
migrations.AlterUniqueTogether(
name='devicebay',
unique_together=set([('device', 'name')]),
),
]

View File

@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from extras.rpc import RPC_CLIENTS from extras.rpc import RPC_CLIENTS
from utilities.fields import NullableCharField from utilities.fields import NullableCharField
@ -18,6 +18,14 @@ RACK_FACE_CHOICES = [
[RACK_FACE_REAR, 'Rear'], [RACK_FACE_REAR, 'Rear'],
] ]
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
COLOR_TEAL = 'teal' COLOR_TEAL = 'teal'
COLOR_GREEN = 'green' COLOR_GREEN = 'green'
COLOR_BLUE = 'blue' COLOR_BLUE = 'blue'
@ -274,6 +282,7 @@ class Rack(CreatedUpdatedModel):
# Add devices to rack units list # Add devices to rack units list
if self.pk: if self.pk:
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
.annotate(devicebay_count=Count('device_bays'))\
.exclude(pk=exclude)\ .exclude(pk=exclude)\
.filter(rack=self, position__gt=0)\ .filter(rack=self, position__gt=0)\
.filter(Q(face=face) | Q(device_type__is_full_depth=True)): .filter(Q(face=face) | Q(device_type__is_full_depth=True)):
@ -380,6 +389,10 @@ class DeviceType(models.Model):
help_text="This type of device has power outlets") help_text="This type of device has power outlets")
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device', is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
help_text="This type of device has network interfaces") help_text="This type of device has network interfaces")
subdevice_role = models.NullBooleanField(default=None, verbose_name='Parent/child status',
choices=SUBDEVICE_ROLE_CHOICES,
help_text="Parent devices house child devices in device bays. Select "
"\"None\" if this device type is neither a parent nor a child.")
class Meta: class Meta:
ordering = ['manufacturer', 'model'] ordering = ['manufacturer', 'model']
@ -389,11 +402,24 @@ class DeviceType(models.Model):
] ]
def __unicode__(self): def __unicode__(self):
return "{0} {1}".format(self.manufacturer, self.model) return "{} {}".format(self.manufacturer, self.model)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:devicetype', args=[self.pk]) return reverse('dcim:devicetype', args=[self.pk])
def clean(self):
if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD:
raise ValidationError("Child device types must be 0U.")
@property
def is_parent_device(self):
return bool(self.subdevice_role)
@property
def is_child_device(self):
return bool(self.subdevice_role is False)
class ConsolePortTemplate(models.Model): class ConsolePortTemplate(models.Model):
""" """
@ -481,6 +507,21 @@ class InterfaceTemplate(models.Model):
return self.name return self.name
class DeviceBayTemplate(models.Model):
"""
A template for a DeviceBay to be created for a new parent Device.
"""
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
class Meta:
ordering = ['device_type', 'name']
unique_together = ['device_type', 'name']
def __unicode__(self):
return self.name
# #
# Devices # Devices
# #
@ -563,6 +604,10 @@ class Device(CreatedUpdatedModel):
def clean(self): def clean(self):
# Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and (self.face is not None or self.position):
raise ValidationError("Child device types cannot be assigned a rack face or position.")
# Validate position/face combination # Validate position/face combination
if self.position and self.face is None: if self.position and self.face is None:
raise ValidationError("Must specify rack face with rack position.") raise ValidationError("Must specify rack face with rack position.")
@ -610,6 +655,10 @@ class Device(CreatedUpdatedModel):
[Interface(device=self, name=template.name, form_factor=template.form_factor, [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()] mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
) )
DeviceBay.objects.bulk_create(
[DeviceBay(device=self, name=template.name) for template in
self.device_type.device_bay_templates.all()]
)
def to_csv(self): def to_csv(self):
return ','.join([ return ','.join([
@ -643,6 +692,12 @@ class Device(CreatedUpdatedModel):
return self.name return self.name
return '{{{}}}'.format(self.pk) return '{{{}}}'.format(self.pk)
def get_children(self):
"""
Return the set of child Devices installed in DeviceBays within this Device.
"""
return Device.objects.filter(parent_bay__device=self.pk)
def get_rpc_client(self): def get_rpc_client(self):
""" """
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined. Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
@ -860,6 +915,33 @@ class InterfaceConnection(models.Model):
]) ])
class DeviceBay(models.Model):
"""
An empty space within a Device which can house a child device
"""
device = models.ForeignKey('Device', related_name='device_bays', on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name')
installed_device = models.OneToOneField('Device', related_name='parent_bay', blank=True, null=True)
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
def __unicode__(self):
return '{} - {}'.format(self.device.name, self.name)
def clean(self):
# Validate that the parent Device can have DeviceBays
if not self.device.device_type.is_parent_device:
raise ValidationError("This type of device ({}) does not support device bays."
.format(self.device.device_type))
# Cannot install a device into itself, obviously
if self.device == self.installed_device:
raise ValidationError("Cannot install a device into itself.")
class Module(models.Model): class Module(models.Model):
""" """
A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only

View File

@ -4,8 +4,9 @@ from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, InterfaceTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, Site,
) )
@ -201,6 +202,19 @@ class InterfaceTemplateTable(tables.Table):
} }
class DeviceBayTemplateTable(tables.Table):
pk = ToggleColumn()
class Meta:
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
attrs = {
'class': 'table table-hover panel-body',
}
# #
# Device roles # Device roles
# #

View File

@ -4,7 +4,8 @@ from secrets.views import secret_add
from . import views from . import views
from .models import ( from .models import (
ConsolePortTemplate, ConsoleServerPortTemplate, PowerPortTemplate, PowerOutletTemplate, InterfaceTemplate, ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate,
InterfaceTemplate,
) )
@ -70,6 +71,10 @@ urlpatterns = [
name='devicetype_add_interface'), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete, url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
{'model': InterfaceTemplate}, name='devicetype_delete_interface'), {'model': InterfaceTemplate}, name='devicetype_delete_interface'),
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(),
name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.component_template_delete,
{'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'),
# Device roles # Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
@ -125,6 +130,13 @@ urlpatterns = [
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'), url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'), url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
# Device bays
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
url(r'^device-bays/(?P<pk>\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'),
# Console/power/interface connections # Console/power/interface connections
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'), url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),

View File

@ -24,8 +24,9 @@ from utilities.views import (
from . import filters, forms, tables from . import filters, forms, tables
from .models import ( from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
Site,
) )
@ -153,7 +154,8 @@ def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack, pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True) nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
.select_related('device_type__manufacturer')
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first() next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first() prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
@ -263,12 +265,14 @@ def devicetype(request, pk):
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype)) powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype))
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype)) poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype))
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype)) interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype))
if request.user.has_perm('dcim.change_devicetype'): if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True consoleport_table.base_columns['pk'].visible = True
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True poweroutlet_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True
return render(request, 'dcim/devicetype.html', { return render(request, 'dcim/devicetype.html', {
'devicetype': devicetype, 'devicetype': devicetype,
@ -277,6 +281,7 @@ def devicetype(request, pk):
'powerport_table': powerport_table, 'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table, 'poweroutlet_table': poweroutlet_table,
'interface_table': interface_table, 'interface_table': interface_table,
'devicebay_table': devicebay_table,
}) })
@ -395,6 +400,11 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView):
form = forms.InterfaceTemplateForm form = forms.InterfaceTemplateForm
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
model = DeviceBayTemplate
form = forms.DeviceBayTemplateForm
def component_template_delete(request, pk, model): def component_template_delete(request, pk, model):
devicetype = get_object_or_404(DeviceType, pk=pk) devicetype = get_object_or_404(DeviceType, pk=pk)
@ -510,6 +520,7 @@ def device(request, pk):
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'circuit')
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'circuit')
device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device')
# Gather any secrets which belong to this device # Gather any secrets which belong to this device
secrets = device.secrets.all() secrets = device.secrets.all()
@ -540,6 +551,7 @@ def device(request, pk):
'power_outlets': power_outlets, 'power_outlets': power_outlets,
'interfaces': interfaces, 'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces, 'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays,
'ip_addresses': ip_addresses, 'ip_addresses': ip_addresses,
'secrets': secrets, 'secrets': secrets,
'related_devices': related_devices, 'related_devices': related_devices,
@ -550,7 +562,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
model = Device model = Device
form_class = forms.DeviceForm form_class = forms.DeviceForm
fields_initial = ['site', 'rack', 'position', 'face'] fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
template_name = 'dcim/device_edit.html' template_name = 'dcim/device_edit.html'
cancel_url = 'dcim:device_list' cancel_url = 'dcim:device_list'
@ -1342,6 +1354,143 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
len(selected_devices))) len(selected_devices)))
#
# Device bays
#
@permission_required('dcim.add_devicebay')
def devicebay_add(request, pk):
device = get_object_or_404(Device, pk=pk)
if request.method == 'POST':
form = forms.DeviceBayCreateForm(request.POST)
if form.is_valid():
device_bays = []
for name in form.cleaned_data['name_pattern']:
devicebay_form = forms.DeviceBayForm({
'device': device.pk,
'name': name,
})
if devicebay_form.is_valid():
device_bays.append(devicebay_form.save(commit=False))
else:
for err in devicebay_form.errors.get('__all__', []):
form.add_error('name_pattern', err)
if not form.errors:
DeviceBay.objects.bulk_create(device_bays)
messages.success(request, "Added {} device bay(s) to {}".format(len(device_bays), device))
if '_addanother' in request.POST:
return redirect('dcim:devicebay_add', pk=device.pk)
else:
return redirect('dcim:device', pk=device.pk)
else:
form = forms.DeviceBayCreateForm()
return render(request, 'dcim/devicebay_edit.html', {
'device': device,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})
@permission_required('dcim.change_devicebay')
def devicebay_edit(request, pk):
devicebay = get_object_or_404(DeviceBay, pk=pk)
if request.method == 'POST':
form = forms.DeviceBayForm(request.POST, instance=devicebay)
if form.is_valid():
devicebay = form.save()
messages.success(request, "Modified {} bay {}".format(devicebay.device.name, devicebay.name))
return redirect('dcim:device', pk=devicebay.device.pk)
else:
form = forms.DeviceBayForm(instance=devicebay)
return render(request, 'dcim/devicebay_edit.html', {
'devicebay': devicebay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
})
@permission_required('dcim.delete_devicebay')
def devicebay_delete(request, pk):
devicebay = get_object_or_404(DeviceBay, pk=pk)
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
devicebay.delete()
messages.success(request, "Device bay {} has been deleted from {}".format(devicebay, devicebay.device))
return redirect('dcim:device', pk=devicebay.device.pk)
else:
form = ConfirmationForm()
return render(request, 'dcim/devicebay_delete.html', {
'devicebay': devicebay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': devicebay.device.pk}),
})
@permission_required('dcim.change_devicebay')
def devicebay_populate(request, pk):
device_bay = get_object_or_404(DeviceBay, pk=pk)
if request.method == 'POST':
form = forms.PopulateDeviceBayForm(device_bay, request.POST)
if form.is_valid():
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
if not form.errors:
messages.success(request, "Added {} to {}".format(device_bay.installed_device, device_bay))
return redirect('dcim:device', pk=device_bay.device.pk)
else:
form = forms.PopulateDeviceBayForm(device_bay)
return render(request, 'dcim/devicebay_populate.html', {
'device_bay': device_bay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
})
@permission_required('dcim.change_devicebay')
def devicebay_depopulate(request, pk):
device_bay = get_object_or_404(DeviceBay, pk=pk)
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
removed_device = device_bay.installed_device
device_bay.installed_device = None
device_bay.save()
messages.success(request, "{} has been removed from {}".format(removed_device, device_bay))
return redirect('dcim:device', pk=device_bay.device.pk)
else:
form = ConfirmationForm()
return render(request, 'dcim/devicebay_depopulate.html', {
'device_bay': device_bay,
'form': form,
'cancel_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
})
# #
# Interface connections # Interface connections
# #

View File

@ -31,7 +31,12 @@
<li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}"> <li class="occupied h{{ u.device.device_type.u_height }}u{% ifequal u.device.face face_id %} {{ u.device.device_role.color }}{% endifequal %}">
{% ifequal u.device.face face_id %} {% ifequal u.device.face face_id %}
<a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true" <a href="{% url 'dcim:device' pk=u.device.pk %}" data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">{{ u.device.name|default:u.device.device_role }}</a> data-content="{{ u.device.device_role }}<br />{{ u.device.device_type }} ({{ u.device.device_type.u_height }}U)">
{{ u.device.name|default:u.device.device_role }}
{% if u.device.devicebay_count %}
({{ u.device.get_children.count }}/{{ u.device.devicebay_count }})
{% endif %}
</a>
{% else %} {% else %}
<span>{{ u.device.name|default:u.device.device_role }}</span> <span>{{ u.device.name|default:u.device.device_role }}</span>
{% endifequal %} {% endifequal %}

View File

@ -29,7 +29,12 @@
<tr> <tr>
<td>Position</td> <td>Position</td>
<td> <td>
{% if device.position %} {% if device.parent_bay %}
{% with device.parent_bay.device as parent %}
<span>U{{ parent.position }} / {{ parent.get_face_display }}
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
{% endwith %}
{% elif device.position %}
<span>U{{ device.position }} / {{ device.get_face_display }}</span> <span>U{{ device.position }} / {{ device.get_face_display }}</span>
{% elif device.device_type.u_height %} {% elif device.device_type.u_height %}
<span class="label label-warning">Not racked</span> <span class="label label-warning">Not racked</span>
@ -268,6 +273,25 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% if device_bays or device.device_type.is_parent_device %}
<div class="panel panel-default">
<div class="panel-heading">
{% if perms.dcim.add_devicebay %}
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Device Bays</a>
{% endif %}
<strong>Device Bays</strong>
</div>
<table class="table table-hover panel-body">
{% for devicebay in device_bays %}
{% include 'dcim/inc/_devicebay.html' %}
{% empty %}
<tr>
<td colspan="4">No device bays defined</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if interfaces or device.device_type.is_network_device %} {% if interfaces or device.device_type.is_network_device %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">

View File

@ -0,0 +1,8 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
{% block message %}
<p>Are you sure you want to delete this device bay from <strong>{{ devicebay.device }}</strong>?</p>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Remove {{ device_bay.installed_device }} from {{ device_bay }}?{% endblock %}
{% block message %}
<p>Are you sure you want to remove <strong>{{ device_bay.installed_device }}</strong> from <strong>{{ device_bay }}</strong>?</p>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}{% if devicebay.pk %}Editing {{ devicebay.device }} {{ devicebay }}{% else %}Add a Device Bay ({{ device }}){% endif %}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
{% if poweroutlet.pk %}
<strong>Editing {{ devicebay }}</strong>
{% else %}
<strong>Add a Device Bay</strong>
{% endif %}
</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{% if devicebay %}{{ devicebay.device }}{% else %}{{ device }}{% endif %}</p>
</div>
</div>
{% render_form form %}
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
{% if devicebay.pk %}
<button type="submit" name="_update" class="btn btn-primary">Save</button>
{% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
{% endif %}
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Populate {{ device_bay }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">Populate {{ device_bay }}</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label required">Parent Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ device_bay.device }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Bay</label>
<div class="col-md-9">
<p class="form-control-static">{{ device_bay.name }}</p>
</div>
</div>
{% render_form form %}
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@ -77,6 +77,7 @@
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %} {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
{% 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' %} {% 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' %}

View File

@ -5,6 +5,10 @@
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li> <li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li> <li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li>
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li> <li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
{% if device.parent_bay %}
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
<li>{{ device.parent_bay.name }}</li>
{% endif %}
<li>{{ device }}</li> <li>{{ device }}</li>
</ol> </ol>
{% endif %} {% endif %}

View File

@ -0,0 +1,39 @@
<tr>
<td>
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
</td>
<td>
{% if devicebay.installed_device %}
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
{% else %}
<span class="text-muted">Vacant</span>
{% endif %}
</td>
<td class="text-right">
{% if perms.dcim.change_devicebay %}
{% if devicebay.installed_device %}
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i>
</a>
{% else %}
<a href="{% url 'dcim:devicebay_populate' pk=devicebay.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
</a>
{% endif %}
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
</a>
{% endif %}
{% if perms.dcim.delete_devicebay %}
{% if devicebay.installed_device %}
<button class="btn btn-danger btn-xs" disabled="disabled">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:devicebay_delete' pk=devicebay.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete device bay"></i>
</a>
{% endif %}
{% endif %}
</td>
</tr>

View File

@ -112,6 +112,12 @@
</div> </div>
{% if nonracked_devices %} {% if nonracked_devices %}
<table class="table table-hover panel-body"> <table class="table table-hover panel-body">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th>Parent</th>
</tr>
{% for device in nonracked_devices %} {% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}> <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td> <td>
@ -119,6 +125,7 @@
</td> </td>
<td>{{ device.device_role }}</td> <td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td> <td>{{ device.device_type }}</td>
<td>{% if device.parent_bay %}<a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay }}</a>{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>