Browse Source

Initial work on #91: Support for subdevices

Jeremy Stretch 8 years ago
parent
commit
0123dbcf5f

+ 19 - 4
netbox/dcim/admin.py

@@ -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,
-    Interface, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
-    PowerPortTemplate, Rack, RackGroup, Site,
+    ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
+    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']

+ 48 - 5
netbox/dcim/forms.py

@@ -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,
-    ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, IFACE_FF_VIRTUAL,
-    InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
-    PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES
+    DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
+    ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
+    Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
+    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
 #
 #
@@ -1081,6 +1089,41 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
 
 
 
 
 #
 #
+# 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
 #
 #
 
 

+ 56 - 0
netbox/dcim/migrations/0004_auto_20160701_2049.py

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

+ 84 - 2
netbox/dcim/models.py

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

+ 16 - 2
netbox/dcim/tables.py

@@ -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,
-    Interface, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
+    ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
+    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
 #
 #

+ 13 - 1
netbox/dcim/urls.py

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

+ 153 - 4
netbox/dcim/views.py

@@ -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,
-    PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
+    DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
+    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'
 
 
@@ -1343,6 +1355,143 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
 
 
 
 
 #
 #
+# 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
 #
 #
 
 

+ 6 - 1
netbox/templates/dcim/_rack_elevation.html

@@ -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 %}

+ 25 - 1
netbox/templates/dcim/device.html

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

+ 8 - 0
netbox/templates/dcim/devicebay_delete.html

@@ -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 %}

+ 8 - 0
netbox/templates/dcim/devicebay_depopulate.html

@@ -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 %}

+ 51 - 0
netbox/templates/dcim/devicebay_edit.html

@@ -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 %}

+ 46 - 0
netbox/templates/dcim/devicebay_populate.html

@@ -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 %}

+ 1 - 0
netbox/templates/dcim/devicetype.html

@@ -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' %}

+ 4 - 0
netbox/templates/dcim/inc/_device_header.html

@@ -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 %}

+ 39 - 0
netbox/templates/dcim/inc/_devicebay.html

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

+ 7 - 0
netbox/templates/dcim/rack.html

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