diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 9975a3ba0..9e48c1ae1 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -2,9 +2,9 @@ from django.contrib import admin from django.db.models import Count 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 +class DeviceBayTemplateAdmin(admin.TabularInline): + model = DeviceBayTemplate + + @admin.register(DeviceType) class DeviceTypeAdmin(admin.ModelAdmin): prepopulated_fields = { @@ -72,9 +76,10 @@ class DeviceTypeAdmin(admin.ModelAdmin): PowerPortTemplateAdmin, PowerOutletTemplateAdmin, InterfaceTemplateAdmin, + DeviceBayTemplateAdmin, ] 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'] def get_queryset(self, request): @@ -84,6 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin): power_port_count=Count('power_port_templates', distinct=True), power_outlet_count=Count('power_outlet_templates', distinct=True), interface_count=Count('interface_templates', distinct=True), + devicebay_count=Count('devicebay_templates', distinct=True), ) def console_ports(self, instance): @@ -101,6 +107,9 @@ class DeviceTypeAdmin(admin.ModelAdmin): def interfaces(self, instance): return instance.interface_count + def device_bays(self, instance): + return instance.devicebay_count + # # Devices @@ -144,6 +153,11 @@ class InterfaceAdmin(admin.TabularInline): model = Interface +class DeviceBayAdmin(admin.TabularInline): + model = DeviceBay + fk_name = 'device' + + class ModuleAdmin(admin.TabularInline): model = Module readonly_fields = ['parent', 'discovered'] @@ -157,6 +171,7 @@ class DeviceAdmin(admin.ModelAdmin): PowerPortAdmin, PowerOutletAdmin, InterfaceAdmin, + DeviceBayAdmin, ModuleAdmin, ] list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial'] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 65b6e77cb..4c81ae9ff 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -10,10 +10,10 @@ from utilities.forms 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: model = DeviceType 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): @@ -283,6 +283,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin): 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 # @@ -1080,6 +1088,41 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin): 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 # diff --git a/netbox/dcim/migrations/0004_auto_20160701_2049.py b/netbox/dcim/migrations/0004_auto_20160701_2049.py new file mode 100644 index 000000000..e051daded --- /dev/null +++ b/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')]), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index aeee10ee5..c859508b3 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core.validators import MinValueValidator 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 utilities.fields import NullableCharField @@ -18,6 +18,14 @@ RACK_FACE_CHOICES = [ [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_GREEN = 'green' COLOR_BLUE = 'blue' @@ -274,6 +282,7 @@ class Rack(CreatedUpdatedModel): # Add devices to rack units list if self.pk: for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\ + .annotate(devicebay_count=Count('device_bays'))\ .exclude(pk=exclude)\ .filter(rack=self, position__gt=0)\ .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") is_network_device = models.BooleanField(default=True, verbose_name='Is a network device', 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: ordering = ['manufacturer', 'model'] @@ -389,11 +402,24 @@ class DeviceType(models.Model): ] def __unicode__(self): - return "{0} {1}".format(self.manufacturer, self.model) + return "{} {}".format(self.manufacturer, self.model) def get_absolute_url(self): 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): """ @@ -481,6 +507,21 @@ class InterfaceTemplate(models.Model): 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 # @@ -563,6 +604,10 @@ class Device(CreatedUpdatedModel): 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 if self.position and self.face is None: 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, 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): return ','.join([ @@ -643,6 +692,12 @@ class Device(CreatedUpdatedModel): return self.name 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): """ 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): """ A Module represents a piece of hardware within a Device, such as a line card or power supply. Modules are used only diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index ed3bbbda5..dc66c3ab1 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -4,8 +4,9 @@ from django_tables2.utils import Accessor from utilities.tables import BaseTable, ToggleColumn 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 # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 1665927a2..1c76e63d2 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -4,7 +4,8 @@ from secrets.views import secret_add from . import views from .models import ( - ConsolePortTemplate, ConsoleServerPortTemplate, PowerPortTemplate, PowerOutletTemplate, InterfaceTemplate, + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate, + InterfaceTemplate, ) @@ -70,6 +71,10 @@ urlpatterns = [ name='devicetype_add_interface'), url(r'^device-types/(?P\d+)/interfaces/delete/$', views.component_template_delete, {'model': InterfaceTemplate}, name='devicetype_delete_interface'), + url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), + name='devicetype_add_devicebay'), + url(r'^device-types/(?P\d+)/device-bays/delete/$', views.component_template_delete, + {'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'), # Device roles url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), @@ -125,6 +130,13 @@ urlpatterns = [ url(r'^power-outlets/(?P\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'), url(r'^power-outlets/(?P\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'), + # Device bays + url(r'^devices/(?P\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'), + url(r'^device-bays/(?P\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'), + url(r'^device-bays/(?P\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'), + url(r'^device-bays/(?P\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'), + url(r'^device-bays/(?P\d+)/depopulate/$', views.devicebay_depopulate, name='devicebay_depopulate'), + # Console/power/interface connections 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'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3cf157c7d..51d28c006 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -24,8 +24,9 @@ from utilities.views import ( from . import filters, forms, tables from .models import ( 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) - 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() 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)) poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.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'): consoleport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True powerport_table.base_columns['pk'].visible = True poweroutlet_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', { 'devicetype': devicetype, @@ -277,6 +281,7 @@ def devicetype(request, pk): 'powerport_table': powerport_table, 'poweroutlet_table': poweroutlet_table, 'interface_table': interface_table, + 'devicebay_table': devicebay_table, }) @@ -395,6 +400,11 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView): form = forms.InterfaceTemplateForm +class DeviceBayTemplateAddView(ComponentTemplateCreateView): + model = DeviceBayTemplate + form = forms.DeviceBayTemplateForm + + def component_template_delete(request, pk, model): 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') mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ .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 secrets = device.secrets.all() @@ -540,6 +551,7 @@ def device(request, pk): 'power_outlets': power_outlets, 'interfaces': interfaces, 'mgmt_interfaces': mgmt_interfaces, + 'device_bays': device_bays, 'ip_addresses': ip_addresses, 'secrets': secrets, 'related_devices': related_devices, @@ -550,7 +562,7 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_device' model = Device form_class = forms.DeviceForm - fields_initial = ['site', 'rack', 'position', 'face'] + fields_initial = ['site', 'rack', 'position', 'face', 'device_bay'] template_name = 'dcim/device_edit.html' cancel_url = 'dcim:device_list' @@ -1342,6 +1354,143 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView): 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 # diff --git a/netbox/templates/dcim/_rack_elevation.html b/netbox/templates/dcim/_rack_elevation.html index 002b9dd38..1313df8ea 100644 --- a/netbox/templates/dcim/_rack_elevation.html +++ b/netbox/templates/dcim/_rack_elevation.html @@ -31,7 +31,12 @@
  • {% ifequal u.device.face face_id %} {{ u.device.name|default:u.device.device_role }} + data-content="{{ u.device.device_role }}
    {{ 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 %} + {% else %} {{ u.device.name|default:u.device.device_role }} {% endifequal %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 9d9da6a18..0bf635e31 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -29,7 +29,12 @@ Position - {% if device.position %} + {% if device.parent_bay %} + {% with device.parent_bay.device as parent %} + U{{ parent.position }} / {{ parent.get_face_display }} + ({{ parent }} - {{ device.parent_bay.name }}) + {% endwith %} + {% elif device.position %} U{{ device.position }} / {{ device.get_face_display }} {% elif device.device_type.u_height %} Not racked @@ -268,6 +273,25 @@
    + {% if device_bays or device.device_type.is_parent_device %} +
    +
    + {% if perms.dcim.add_devicebay %} + Add Device Bays + {% endif %} + Device Bays +
    + + {% for devicebay in device_bays %} + {% include 'dcim/inc/_devicebay.html' %} + {% empty %} + + + + {% endfor %} +
    No device bays defined
    +
    + {% endif %} {% if interfaces or device.device_type.is_network_device %}
    diff --git a/netbox/templates/dcim/devicebay_delete.html b/netbox/templates/dcim/devicebay_delete.html new file mode 100644 index 000000000..dbd43b824 --- /dev/null +++ b/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 %} +

    Are you sure you want to delete this device bay from {{ devicebay.device }}?

    +{% endblock %} diff --git a/netbox/templates/dcim/devicebay_depopulate.html b/netbox/templates/dcim/devicebay_depopulate.html new file mode 100644 index 000000000..e8a3c344d --- /dev/null +++ b/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 %} +

    Are you sure you want to remove {{ device_bay.installed_device }} from {{ device_bay }}?

    +{% endblock %} diff --git a/netbox/templates/dcim/devicebay_edit.html b/netbox/templates/dcim/devicebay_edit.html new file mode 100644 index 000000000..507cf0eaf --- /dev/null +++ b/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 %} +
    + {% csrf_token %} +
    +
    + {% if form.non_field_errors %} +
    +
    Errors
    +
    + {{ form.non_field_errors }} +
    +
    + {% endif %} +
    +
    + {% if poweroutlet.pk %} + Editing {{ devicebay }} + {% else %} + Add a Device Bay + {% endif %} +
    +
    +
    + +
    +

    {% if devicebay %}{{ devicebay.device }}{% else %}{{ device }}{% endif %}

    +
    +
    + {% render_form form %} +
    +
    +
    +
    + {% if devicebay.pk %} + + {% else %} + + + {% endif %} + Cancel +
    +
    +
    +
    +
    +{% endblock %} diff --git a/netbox/templates/dcim/devicebay_populate.html b/netbox/templates/dcim/devicebay_populate.html new file mode 100644 index 000000000..a9d84c5e7 --- /dev/null +++ b/netbox/templates/dcim/devicebay_populate.html @@ -0,0 +1,46 @@ +{% extends '_base.html' %} +{% load form_helpers %} + +{% block title %}Populate {{ device_bay }}{% endblock %} + +{% block content %} +
    + {% csrf_token %} +
    +
    + {% if form.non_field_errors %} +
    +
    Errors
    +
    + {{ form.non_field_errors }} +
    +
    + {% endif %} +
    +
    Populate {{ device_bay }}
    +
    +
    + +
    +

    {{ device_bay.device }}

    +
    +
    +
    + +
    +

    {{ device_bay.name }}

    +
    +
    + {% render_form form %} +
    +
    +
    +
    + + Cancel +
    +
    +
    +
    +
    +{% endblock %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 4510e6e43..1ee2b90a8 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/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=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=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' %} diff --git a/netbox/templates/dcim/inc/_device_header.html b/netbox/templates/dcim/inc/_device_header.html index d147fb0d5..a038b96b2 100644 --- a/netbox/templates/dcim/inc/_device_header.html +++ b/netbox/templates/dcim/inc/_device_header.html @@ -5,6 +5,10 @@
  • {{ device.rack.site }}
  • Racks
  • {{ device.rack }}
  • + {% if device.parent_bay %} +
  • {{ device.parent_bay.device }}
  • +
  • {{ device.parent_bay.name }}
  • + {% endif %}
  • {{ device }}
  • {% endif %} diff --git a/netbox/templates/dcim/inc/_devicebay.html b/netbox/templates/dcim/inc/_devicebay.html new file mode 100644 index 000000000..e4fff1641 --- /dev/null +++ b/netbox/templates/dcim/inc/_devicebay.html @@ -0,0 +1,39 @@ + + + {{ devicebay.name }} + + + {% if devicebay.installed_device %} + {{ devicebay.installed_device }} + {% else %} + Vacant + {% endif %} + + + {% if perms.dcim.change_devicebay %} + {% if devicebay.installed_device %} + + + + {% else %} + + + + {% endif %} + + + + {% endif %} + {% if perms.dcim.delete_devicebay %} + {% if devicebay.installed_device %} + + {% else %} + + + + {% endif %} + {% endif %} + + diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 20b86dc85..70d8b3b9c 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -112,6 +112,12 @@ {% if nonracked_devices %} + + + + + + {% for device in nonracked_devices %} + {% endfor %}
    NameRoleTypeParent
    @@ -119,6 +125,7 @@ {{ device.device_role }} {{ device.device_type }}{% if device.parent_bay %}{{ device.parent_bay }}{% endif %}