Просмотр исходного кода

Merge branch 'develop-2.9' into 2006-scripts-reports-background

John Anderson 5 лет назад
Родитель
Сommit
4a74927fa2
31 измененных файлов с 1118 добавлено и 1084 удалено
  1. 1 0
      docs/release-notes/version-2.9.md
  2. 2 0
      netbox/circuits/tests/test_api.py
  3. 7 5
      netbox/dcim/api/serializers.py
  4. 11 10
      netbox/dcim/filters.py
  5. 46 81
      netbox/dcim/forms.py
  6. 33 10
      netbox/dcim/migrations/0107_component_labels.py
  7. 9 3
      netbox/dcim/migrations/0109_interface_remove_vm.py
  8. 120 0
      netbox/dcim/migrations/0112_standardize_components.py
  9. 26 26
      netbox/dcim/models/__init__.py
  10. 19 136
      netbox/dcim/models/device_component_templates.py
  11. 44 173
      netbox/dcim/models/device_components.py
  12. 27 49
      netbox/dcim/tables.py
  13. 4 0
      netbox/dcim/tests/test_api.py
  14. 10 8
      netbox/dcim/tests/test_views.py
  15. 8 0
      netbox/dcim/urls.py
  16. 42 10
      netbox/dcim/views.py
  17. 2 0
      netbox/secrets/tests/test_views.py
  18. 471 416
      netbox/templates/dcim/device.html
  19. 139 131
      netbox/templates/dcim/devicetype.html
  20. 0 2
      netbox/templates/dcim/inc/consoleport.html
  21. 40 0
      netbox/templates/dcim/inc/device_component_table.html
  22. 11 11
      netbox/templates/dcim/inc/devicetype_component_table.html
  23. 1 1
      netbox/templates/dcim/inc/interface.html
  24. 1 1
      netbox/templates/extras/inc/tags_panel.html
  25. 1 0
      netbox/templates/utilities/templatetags/badge.html
  26. 15 7
      netbox/utilities/tables.py
  27. 11 0
      netbox/utilities/templatetags/helpers.py
  28. 1 1
      netbox/utilities/utils.py
  29. 2 2
      netbox/virtualization/migrations/0016_replicate_interfaces.py
  30. 11 0
      netbox/virtualization/models.py
  31. 3 1
      netbox/virtualization/tests/test_api.py

+ 1 - 0
docs/release-notes/version-2.9.md

@@ -19,6 +19,7 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that suppo
 * [#4793](https://github.com/netbox-community/netbox/issues/4793) - Add `description` field to device component templates
 * [#4795](https://github.com/netbox-community/netbox/issues/4795) - Add bulk disconnect capability for console and power ports
 * [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
+* [#4817](https://github.com/netbox-community/netbox/issues/4817) - Standardize device/VM component `name` field to 64 characters
 
 ### Configuration Changes
 

+ 2 - 0
netbox/circuits/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 
 from circuits.choices import *
@@ -45,6 +46,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
         )
         Provider.objects.bulk_create(providers)
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_provider_graphs(self):
         """
         Test retrieval of Graphs assigned to Providers.

+ 7 - 5
netbox/dcim/api/serializers.py

@@ -311,7 +311,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
 
     class Meta:
         model = RearPortTemplate
-        fields = ['id', 'device_type', 'name', 'type', 'positions', 'description']
+        fields = ['id', 'device_type', 'name', 'label', 'type', 'positions', 'description']
 
 
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
@@ -321,7 +321,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
 
     class Meta:
         model = FrontPortTemplate
-        fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
+        fields = ['id', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
 
 
 class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@@ -559,7 +559,7 @@ class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 
     class Meta:
         model = RearPort
-        fields = ['id', 'device', 'name', 'type', 'positions', 'description', 'cable', 'tags']
+        fields = ['id', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags']
 
 
 class FrontPortRearPortSerializer(WritableNestedSerializer):
@@ -570,7 +570,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
 
     class Meta:
         model = RearPort
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'name', 'label']
 
 
 class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
@@ -581,7 +581,9 @@ class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
 
     class Meta:
         model = FrontPort
-        fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
+        fields = [
+            'id', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
+        ]
 
 
 class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):

+ 11 - 10
netbox/dcim/filters.py

@@ -384,28 +384,28 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
         )
 
     def _console_ports(self, queryset, name, value):
-        return queryset.exclude(consoleport_templates__isnull=value)
+        return queryset.exclude(consoleporttemplates__isnull=value)
 
     def _console_server_ports(self, queryset, name, value):
-        return queryset.exclude(consoleserverport_templates__isnull=value)
+        return queryset.exclude(consoleserverporttemplates__isnull=value)
 
     def _power_ports(self, queryset, name, value):
-        return queryset.exclude(powerport_templates__isnull=value)
+        return queryset.exclude(powerporttemplates__isnull=value)
 
     def _power_outlets(self, queryset, name, value):
-        return queryset.exclude(poweroutlet_templates__isnull=value)
+        return queryset.exclude(poweroutlettemplates__isnull=value)
 
     def _interfaces(self, queryset, name, value):
-        return queryset.exclude(interface_templates__isnull=value)
+        return queryset.exclude(interfacetemplates__isnull=value)
 
     def _pass_through_ports(self, queryset, name, value):
         return queryset.exclude(
-            frontport_templates__isnull=value,
-            rearport_templates__isnull=value
+            frontporttemplates__isnull=value,
+            rearporttemplates__isnull=value
         )
 
     def _device_bays(self, queryset, name, value):
-        return queryset.exclude(device_bay_templates__isnull=value)
+        return queryset.exclude(devicebaytemplates__isnull=value)
 
 
 class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
@@ -656,7 +656,7 @@ class DeviceFilterSet(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
-            Q(inventory_items__serial__icontains=value.strip()) |
+            Q(inventoryitems__serial__icontains=value.strip()) |
             Q(asset_tag__icontains=value.strip()) |
             Q(comments__icontains=value)
         ).distinct()
@@ -698,7 +698,7 @@ class DeviceFilterSet(
         )
 
     def _device_bays(self, queryset, name, value):
-        return queryset.exclude(device_bays__isnull=value)
+        return queryset.exclude(devicebays__isnull=value)
 
 
 class DeviceComponentFilterSet(django_filters.FilterSet):
@@ -747,6 +747,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
             return queryset
         return queryset.filter(
             Q(name__icontains=value) |
+            Q(label__icontains=value) |
             Q(description__icontains=value)
         )
 

+ 46 - 81
netbox/dcim/forms.py

@@ -59,7 +59,6 @@ def get_device_by_name_or_pk(name):
 
 
 class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
-
     field_order = [
         'q', 'region', 'site'
     ]
@@ -127,7 +126,11 @@ class InterfaceCommonForm:
                 })
 
 
-class LabeledComponentForm(BootstrapMixin, forms.Form):
+class ComponentForm(BootstrapMixin, forms.Form):
+    """
+    Subclass this form when facilitating the creation of one or more device component or component templates based on
+    a name pattern.
+    """
     name_pattern = ExpandableNameField(
         label='Name'
     )
@@ -1033,7 +1036,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # Device component templates
 #
 
-class ComponentTemplateCreateForm(LabeledComponentForm):
+class ComponentTemplateCreateForm(ComponentForm):
     """
     Base form for the creation of device component templates.
     """
@@ -1350,7 +1353,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = FrontPortTemplate
         fields = [
-            'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'description',
+            'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
         ]
         widgets = {
             'device_type': forms.HiddenInput(),
@@ -1389,7 +1392,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
         # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
         occupied_port_positions = [
             (front_port.rear_port_id, front_port.rear_port_position)
-            for front_port in device_type.frontport_templates.all()
+            for front_port in device_type.frontporttemplates.all()
         ]
 
         # Populate rear port choices
@@ -1430,6 +1433,10 @@ class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=FrontPortTemplate.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
+    label = forms.CharField(
+        max_length=64,
+        required=False
+    )
     type = forms.ChoiceField(
         choices=add_blank_choice(PortTypeChoices),
         required=False,
@@ -1448,7 +1455,7 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = RearPortTemplate
         fields = [
-            'device_type', 'name', 'type', 'positions', 'description',
+            'device_type', 'name', 'label', 'type', 'positions', 'description',
         ]
         widgets = {
             'device_type': forms.HiddenInput(),
@@ -1474,6 +1481,10 @@ class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
         queryset=RearPortTemplate.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
+    label = forms.CharField(
+        max_length=64,
+        required=False
+    )
     type = forms.ChoiceField(
         choices=add_blank_choice(PortTypeChoices),
         required=False,
@@ -2248,7 +2259,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
 # Device components
 #
 
-class ComponentCreateForm(LabeledComponentForm):
+class ComponentCreateForm(ComponentForm):
     """
     Base form for the creation of device components.
     """
@@ -2261,7 +2272,7 @@ class ComponentCreateForm(LabeledComponentForm):
     )
 
 
-class DeviceBulkAddComponentForm(LabeledComponentForm):
+class DeviceBulkAddComponentForm(ComponentForm):
     pk = forms.ModelMultipleChoiceField(
         queryset=Device.objects.all(),
         widget=forms.MultipleHiddenInput()
@@ -2967,13 +2978,12 @@ class InterfaceCSVForm(CSVModelForm):
         super().__init__(*args, **kwargs)
 
         # Limit LAG choices to interfaces belonging to this device (or VC master)
+        device = None
         if self.is_bound and 'device' in self.data:
             try:
                 device = self.fields['device'].to_python(self.data['device'])
             except forms.ValidationError:
-                device = None
-        else:
-            device = self.instance.device
+                pass
 
         if device:
             self.fields['lag'].queryset = Interface.objects.filter(
@@ -3013,7 +3023,7 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = FrontPort
         fields = [
-            'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
+            'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
         ]
         widgets = {
             'device': forms.HiddenInput(),
@@ -3094,14 +3104,14 @@ class FrontPortCreateForm(ComponentCreateForm):
 
 
 # class FrontPortBulkCreateForm(
-#     form_from_model(FrontPort, ['type', 'description', 'tags']),
+#     form_from_model(FrontPort, ['label', 'type', 'description', 'tags']),
 #     DeviceBulkAddComponentForm
 # ):
 #     pass
 
 
 class FrontPortBulkEditForm(
-    form_from_model(FrontPort, ['type', 'description']),
+    form_from_model(FrontPort, ['label', 'type', 'description']),
     BootstrapMixin,
     AddRemoveTagsForm,
     BulkEditForm
@@ -3112,9 +3122,7 @@ class FrontPortBulkEditForm(
     )
 
     class Meta:
-        nullable_fields = [
-            'description',
-        ]
+        nullable_fields = ('label', 'description')
 
 
 class FrontPortCSVForm(CSVModelForm):
@@ -3185,7 +3193,7 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = RearPort
         fields = [
-            'device', 'name', 'type', 'positions', 'description', 'tags',
+            'device', 'name', 'label', 'type', 'positions', 'description', 'tags',
         ]
         widgets = {
             'device': forms.HiddenInput(),
@@ -3210,14 +3218,14 @@ class RearPortCreateForm(ComponentCreateForm):
 
 
 class RearPortBulkCreateForm(
-    form_from_model(RearPort, ['type', 'positions', 'description', 'tags']),
+    form_from_model(RearPort, ['label', 'type', 'positions', 'description', 'tags']),
     DeviceBulkAddComponentForm
 ):
     pass
 
 
 class RearPortBulkEditForm(
-    form_from_model(RearPort, ['type', 'description']),
+    form_from_model(RearPort, ['label', 'type', 'description']),
     BootstrapMixin,
     AddRemoveTagsForm,
     BulkEditForm
@@ -3228,9 +3236,7 @@ class RearPortBulkEditForm(
     )
 
     class Meta:
-        nullable_fields = [
-            'description',
-        ]
+        nullable_fields = ('label', 'description')
 
 
 class RearPortCSVForm(CSVModelForm):
@@ -3392,17 +3398,11 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = InventoryItem
         fields = [
-            'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
+            'name', 'label', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
         ]
 
 
-class InventoryItemCreateForm(BootstrapMixin, forms.Form):
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.prefetch_related('device_type__manufacturer')
-    )
-    name_pattern = ExpandableNameField(
-        label='Name'
-    )
+class InventoryItemCreateForm(ComponentCreateForm):
     manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False
@@ -3443,7 +3443,7 @@ class InventoryItemCSVForm(CSVModelForm):
 
 
 class InventoryItemBulkCreateForm(
-    form_from_model(InventoryItem, ['manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'tags']),
+    form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'tags']),
     DeviceBulkAddComponentForm
 ):
     tags = DynamicModelMultipleChoiceField(
@@ -3452,68 +3452,27 @@ class InventoryItemBulkCreateForm(
     )
 
 
-class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
+class InventoryItemBulkEditForm(
+    form_from_model(InventoryItem, ['label', 'manufacturer', 'part_id', 'description']),
+    BootstrapMixin,
+    AddRemoveTagsForm,
+    BulkEditForm
+):
     pk = forms.ModelMultipleChoiceField(
         queryset=InventoryItem.objects.all(),
         widget=forms.MultipleHiddenInput()
     )
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        required=False
-    )
     manufacturer = DynamicModelChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False
     )
-    part_id = forms.CharField(
-        max_length=50,
-        required=False,
-        label='Part ID'
-    )
-    description = forms.CharField(
-        max_length=100,
-        required=False
-    )
 
     class Meta:
-        nullable_fields = [
-            'manufacturer', 'part_id', 'description',
-        ]
+        nullable_fields = ('label', 'manufacturer', 'part_id', 'description')
 
 
-class InventoryItemFilterForm(BootstrapMixin, forms.Form):
+class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
-    q = forms.CharField(
-        required=False,
-        label='Search'
-    )
-    region = DynamicModelMultipleChoiceField(
-        queryset=Region.objects.all(),
-        to_field_name='slug',
-        required=False,
-        widget=APISelectMultiple(
-            value_field="slug",
-            filter_for={
-                'site': 'region'
-            }
-        )
-    )
-    site = DynamicModelMultipleChoiceField(
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        required=False,
-        widget=APISelectMultiple(
-            value_field="slug",
-            filter_for={
-                'device_id': 'site'
-            }
-        )
-    )
-    device_id = DynamicModelMultipleChoiceField(
-        queryset=Device.objects.all(),
-        required=False,
-        label='Device'
-    )
     manufacturer = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         to_field_name='slug',
@@ -3522,6 +3481,12 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
             value_field="slug",
         )
     )
+    serial = forms.CharField(
+        required=False
+    )
+    asset_tag = forms.CharField(
+        required=False
+    )
     discovered = forms.NullBooleanField(
         required=False,
         widget=StaticSelect2(

+ 33 - 10
netbox/dcim/migrations/0107_component_labels.py

@@ -1,5 +1,3 @@
-# Generated by Django 3.0.7 on 2020-06-04 20:37
-
 from django.db import migrations, models
 
 
@@ -11,32 +9,57 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.AddField(
-            model_name='interface',
+            model_name='consoleport',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='interfacetemplate',
+            model_name='consoleporttemplate',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='consoleport',
+            model_name='consoleserverport',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='consoleporttemplate',
+            model_name='consoleserverporttemplate',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='consoleserverport',
+            model_name='devicebay',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='consoleserverporttemplate',
+            model_name='devicebaytemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='frontport',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='frontporttemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='interface',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='interfacetemplate',
+            name='label',
+            field=models.CharField(blank=True, max_length=64),
+        ),
+        migrations.AddField(
+            model_name='inventoryitem',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
@@ -61,12 +84,12 @@ class Migration(migrations.Migration):
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='devicebay',
+            model_name='rearport',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),
         migrations.AddField(
-            model_name='devicebaytemplate',
+            model_name='rearporttemplate',
             name='label',
             field=models.CharField(blank=True, max_length=64),
         ),

+ 9 - 3
netbox/dcim/migrations/0109_interface_remove_vm.py

@@ -1,6 +1,5 @@
-# Generated by Django 3.0.6 on 2020-06-22 16:03
-
-from django.db import migrations
+from django.db import migrations, models
+import django.db.models.deletion
 
 
 class Migration(migrations.Migration):
@@ -15,4 +14,11 @@ class Migration(migrations.Migration):
             model_name='interface',
             name='virtual_machine',
         ),
+        # device is now a required field
+        migrations.AlterField(
+            model_name='interface',
+            name='device',
+            field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'),
+            preserve_default=False,
+        ),
     ]

+ 120 - 0
netbox/dcim/migrations/0112_standardize_components.py

@@ -0,0 +1,120 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0111_component_template_description'),
+    ]
+
+    operations = [
+        # Set max_length=64 for all name fields
+        migrations.AlterField(
+            model_name='consoleport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='devicebay',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='devicebaytemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerport',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+        migrations.AlterField(
+            model_name='powerporttemplate',
+            name='name',
+            field=models.CharField(max_length=64),
+        ),
+
+        # Update related_name for necessary component and component template models
+        migrations.AlterField(
+            model_name='consoleporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='devicebay',
+            name='device',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devicebays', to='dcim.Device'),
+        ),
+        migrations.AlterField(
+            model_name='devicebaytemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='devicebaytemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='frontporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='interfacetemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='inventoryitem',
+            name='device',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventoryitems', to='dcim.Device'),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlettemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='powerporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.DeviceType'),
+        ),
+        migrations.AlterField(
+            model_name='rearporttemplate',
+            name='device_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.DeviceType'),
+        ),
+    ]

+ 26 - 26
netbox/dcim/models/__init__.py

@@ -678,7 +678,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
                 'device_type__manufacturer',
                 'device_role'
             ).annotate(
-                devicebay_count=Count('device_bays')
+                devicebay_count=Count('devicebays')
             ).exclude(
                 pk=exclude
             ).filter(
@@ -1049,23 +1049,23 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         ))
 
         # Component templates
-        if self.consoleport_templates.exists():
+        if self.consoleporttemplates.exists():
             data['console-ports'] = [
                 {
                     'name': c.name,
                     'type': c.type,
                 }
-                for c in self.consoleport_templates.all()
+                for c in self.consoleporttemplates.all()
             ]
-        if self.consoleserverport_templates.exists():
+        if self.consoleserverporttemplates.exists():
             data['console-server-ports'] = [
                 {
                     'name': c.name,
                     'type': c.type,
                 }
-                for c in self.consoleserverport_templates.all()
+                for c in self.consoleserverporttemplates.all()
             ]
-        if self.powerport_templates.exists():
+        if self.powerporttemplates.exists():
             data['power-ports'] = [
                 {
                     'name': c.name,
@@ -1073,9 +1073,9 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'maximum_draw': c.maximum_draw,
                     'allocated_draw': c.allocated_draw,
                 }
-                for c in self.powerport_templates.all()
+                for c in self.powerporttemplates.all()
             ]
-        if self.poweroutlet_templates.exists():
+        if self.poweroutlettemplates.exists():
             data['power-outlets'] = [
                 {
                     'name': c.name,
@@ -1083,18 +1083,18 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'power_port': c.power_port.name if c.power_port else None,
                     'feed_leg': c.feed_leg,
                 }
-                for c in self.poweroutlet_templates.all()
+                for c in self.poweroutlettemplates.all()
             ]
-        if self.interface_templates.exists():
+        if self.interfacetemplates.exists():
             data['interfaces'] = [
                 {
                     'name': c.name,
                     'type': c.type,
                     'mgmt_only': c.mgmt_only,
                 }
-                for c in self.interface_templates.all()
+                for c in self.interfacetemplates.all()
             ]
-        if self.frontport_templates.exists():
+        if self.frontporttemplates.exists():
             data['front-ports'] = [
                 {
                     'name': c.name,
@@ -1102,23 +1102,23 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                     'rear_port': c.rear_port.name,
                     'rear_port_position': c.rear_port_position,
                 }
-                for c in self.frontport_templates.all()
+                for c in self.frontporttemplates.all()
             ]
-        if self.rearport_templates.exists():
+        if self.rearporttemplates.exists():
             data['rear-ports'] = [
                 {
                     'name': c.name,
                     'type': c.type,
                     'positions': c.positions,
                 }
-                for c in self.rearport_templates.all()
+                for c in self.rearporttemplates.all()
             ]
-        if self.device_bay_templates.exists():
+        if self.devicebaytemplates.exists():
             data['device-bays'] = [
                 {
                     'name': c.name,
                 }
-                for c in self.device_bay_templates.all()
+                for c in self.devicebaytemplates.all()
             ]
 
         return yaml.dump(dict(data), sort_keys=False)
@@ -1159,7 +1159,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
 
         if (
                 self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
-        ) and self.device_bay_templates.count():
+        ) and self.devicebaytemplates.count():
             raise ValidationError({
                 'subdevice_role': "Must delete all device bay templates associated with this device before "
                                   "declassifying it as a parent device."
@@ -1634,28 +1634,28 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
         # If this is a new Device, instantiate all of the related components per the DeviceType definition
         if is_new:
             ConsolePort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.consoleport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.consoleporttemplates.unrestricted()]
             )
             ConsoleServerPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.consoleserverport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.consoleserverporttemplates.unrestricted()]
             )
             PowerPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.powerport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.powerporttemplates.unrestricted()]
             )
             PowerOutlet.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.poweroutlet_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.poweroutlettemplates.unrestricted()]
             )
             Interface.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.interface_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.interfacetemplates.unrestricted()]
             )
             RearPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.rearport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.rearporttemplates.unrestricted()]
             )
             FrontPort.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.frontport_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.frontporttemplates.unrestricted()]
             )
             DeviceBay.objects.bulk_create(
-                [x.instantiate(self) for x in self.device_type.device_bay_templates.unrestricted()]
+                [x.instantiate(self) for x in self.device_type.devicebaytemplates.unrestricted()]
             )
 
         # Update Site and Rack assignment for any child Devices

+ 19 - 136
netbox/dcim/models/device_component_templates.py

@@ -27,6 +27,24 @@ __all__ = (
 
 
 class ComponentTemplateModel(models.Model):
+    device_type = models.ForeignKey(
+        to='dcim.DeviceType',
+        on_delete=models.CASCADE,
+        related_name='%(class)ss'
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        max_length=100,
+        blank=True
+    )
+    label = models.CharField(
+        max_length=64,
+        blank=True,
+        help_text="Physical label"
+    )
     description = models.CharField(
         max_length=200,
         blank=True
@@ -68,24 +86,6 @@ class ConsolePortTemplate(ComponentTemplateModel):
     """
     A template for a ConsolePort to be created for a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='consoleport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=ConsolePortTypeChoices,
@@ -108,24 +108,6 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
     """
     A template for a ConsoleServerPort to be created for a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='consoleserverport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=ConsolePortTypeChoices,
@@ -148,24 +130,6 @@ class PowerPortTemplate(ComponentTemplateModel):
     """
     A template for a PowerPort to be created for a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='powerport_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=PowerPortTypeChoices,
@@ -202,24 +166,6 @@ class PowerOutletTemplate(ComponentTemplateModel):
     """
     A template for a PowerOutlet to be created for a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='poweroutlet_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=PowerOutletTypeChoices,
@@ -269,25 +215,13 @@ class InterfaceTemplate(ComponentTemplateModel):
     """
     A template for a physical data interface on a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='interface_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
+    # Override ComponentTemplateModel._name to specify naturalize_interface function
     _name = NaturalOrderingField(
         target_field='name',
         naturalize_function=naturalize_interface,
         max_length=100,
         blank=True
     )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=InterfaceTypeChoices
@@ -314,19 +248,6 @@ class FrontPortTemplate(ComponentTemplateModel):
     """
     Template for a pass-through port on the front of a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='frontport_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
         max_length=50,
         choices=PortTypeChoices
@@ -348,9 +269,6 @@ class FrontPortTemplate(ComponentTemplateModel):
             ('rear_port', 'rear_port_position'),
         )
 
-    def __str__(self):
-        return self.name
-
     def clean(self):
 
         # Validate rear port assignment
@@ -385,19 +303,6 @@ class RearPortTemplate(ComponentTemplateModel):
     """
     Template for a pass-through port on the rear of a new Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='rearport_templates'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
         max_length=50,
         choices=PortTypeChoices
@@ -411,9 +316,6 @@ class RearPortTemplate(ComponentTemplateModel):
         ordering = ('device_type', '_name')
         unique_together = ('device_type', 'name')
 
-    def __str__(self):
-        return self.name
-
     def instantiate(self, device):
         return RearPort(
             device=device,
@@ -427,25 +329,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
     """
     A template for a DeviceBay to be created for a new parent Device.
     """
-    device_type = models.ForeignKey(
-        to='dcim.DeviceType',
-        on_delete=models.CASCADE,
-        related_name='device_bay_templates'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
-
     class Meta:
         ordering = ('device_type', '_name')
         unique_together = ('device_type', 'name')

+ 44 - 173
netbox/dcim/models/device_components.py

@@ -36,6 +36,24 @@ __all__ = (
 
 
 class ComponentModel(models.Model):
+    device = models.ForeignKey(
+        to='dcim.Device',
+        on_delete=models.CASCADE,
+        related_name='%(class)ss'
+    )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        max_length=100,
+        blank=True
+    )
+    label = models.CharField(
+        max_length=64,
+        blank=True,
+        help_text="Physical label"
+    )
     description = models.CharField(
         max_length=200,
         blank=True
@@ -233,24 +251,6 @@ class ConsolePort(CableTermination, ComponentModel):
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='consoleports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
         max_length=50,
         choices=ConsolePortTypeChoices,
@@ -270,7 +270,7 @@ class ConsolePort(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'description']
 
     class Meta:
         ordering = ('device', '_name')
@@ -283,6 +283,7 @@ class ConsolePort(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.type,
             self.description,
         )
@@ -297,24 +298,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='consoleserverports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=ConsolePortTypeChoices,
@@ -327,7 +310,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'description']
 
     class Meta:
         ordering = ('device', '_name')
@@ -340,6 +323,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.type,
             self.description,
         )
@@ -354,24 +338,6 @@ class PowerPort(CableTermination, ComponentModel):
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='powerports'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=PowerPortTypeChoices,
@@ -410,7 +376,7 @@ class PowerPort(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
 
     class Meta:
         ordering = ('device', '_name')
@@ -423,6 +389,7 @@ class PowerPort(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.get_type_display(),
             self.maximum_draw,
             self.allocated_draw,
@@ -519,24 +486,6 @@ class PowerOutlet(CableTermination, ComponentModel):
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='poweroutlets'
-    )
-    name = models.CharField(
-        max_length=50
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     type = models.CharField(
         max_length=50,
         choices=PowerOutletTypeChoices,
@@ -562,7 +511,7 @@ class PowerOutlet(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
 
     class Meta:
         ordering = ('device', '_name')
@@ -575,6 +524,7 @@ class PowerOutlet(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.get_type_display(),
             self.power_port.name if self.power_port else None,
             self.get_feed_leg_display(),
@@ -595,15 +545,9 @@ class PowerOutlet(CableTermination, ComponentModel):
 #
 
 class BaseInterface(models.Model):
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        naturalize_function=naturalize_interface,
-        max_length=100,
-        blank=True
-    )
+    """
+    Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
+    """
     enabled = models.BooleanField(
         default=True
     )
@@ -633,18 +577,13 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
     """
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     """
-    device = models.ForeignKey(
-        to='Device',
-        on_delete=models.CASCADE,
-        related_name='interfaces',
-        null=True,
+    # Override ComponentModel._name to specify naturalize_interface function
+    _name = NaturalOrderingField(
+        target_field='name',
+        naturalize_function=naturalize_interface,
+        max_length=100,
         blank=True
     )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     _connected_interface = models.OneToOneField(
         to='self',
         on_delete=models.SET_NULL,
@@ -703,7 +642,7 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = [
-        'device', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
+        'device', 'name', 'label', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'description', 'mode',
     ]
 
     class Meta:
@@ -717,6 +656,7 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
         return (
             self.device.identifier if self.device else None,
             self.name,
+            self.label,
             self.lag.name if self.lag else None,
             self.get_type_display(),
             self.enabled,
@@ -849,19 +789,6 @@ class FrontPort(CableTermination, ComponentModel):
     """
     A pass-through port on the front of a Device.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='frontports'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
         max_length=50,
         choices=PortTypeChoices
@@ -877,7 +804,7 @@ class FrontPort(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
 
     class Meta:
         ordering = ('device', '_name')
@@ -886,9 +813,6 @@ class FrontPort(CableTermination, ComponentModel):
             ('rear_port', 'rear_port_position'),
         )
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
         return reverse('dcim:frontport', kwargs={'pk': self.pk})
 
@@ -896,6 +820,7 @@ class FrontPort(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.get_type_display(),
             self.rear_port.name,
             self.rear_port_position,
@@ -924,19 +849,6 @@ class RearPort(CableTermination, ComponentModel):
     """
     A pass-through port on the rear of a Device.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='rearports'
-    )
-    name = models.CharField(
-        max_length=64
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     type = models.CharField(
         max_length=50,
         choices=PortTypeChoices
@@ -947,15 +859,12 @@ class RearPort(CableTermination, ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'type', 'positions', 'description']
+    csv_headers = ['device', 'name', 'label', 'type', 'positions', 'description']
 
     class Meta:
         ordering = ('device', '_name')
         unique_together = ('device', 'name')
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
         return reverse('dcim:rearport', kwargs={'pk': self.pk})
 
@@ -963,6 +872,7 @@ class RearPort(CableTermination, ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.get_type_display(),
             self.positions,
             self.description,
@@ -978,25 +888,6 @@ class DeviceBay(ComponentModel):
     """
     An empty space within a Device which can house a child device
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='device_bays'
-    )
-    name = models.CharField(
-        max_length=50,
-        verbose_name='Name'
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
-    label = models.CharField(
-        max_length=64,
-        blank=True,
-        help_text="Physical label"
-    )
     installed_device = models.OneToOneField(
         to='dcim.Device',
         on_delete=models.SET_NULL,
@@ -1006,17 +897,12 @@ class DeviceBay(ComponentModel):
     )
     tags = TaggableManager(through=TaggedItem)
 
-    csv_headers = ['device', 'name', 'installed_device', 'description']
+    csv_headers = ['device', 'name', 'label', 'installed_device', 'description']
 
     class Meta:
         ordering = ('device', '_name')
         unique_together = ('device', 'name')
 
-    def __str__(self):
-        if self.label:
-            return '{} - {} ({})'.format(self.device.name, self.name, self.label)
-        return '{} - {}'.format(self.device.name, self.name)
-
     def get_absolute_url(self):
         return reverse('dcim:devicebay', kwargs={'pk': self.pk})
 
@@ -1024,6 +910,7 @@ class DeviceBay(ComponentModel):
         return (
             self.device.identifier,
             self.name,
+            self.label,
             self.installed_device.identifier if self.installed_device else None,
             self.description,
         )
@@ -1061,11 +948,6 @@ class InventoryItem(ComponentModel):
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     InventoryItems are used only for inventory purposes.
     """
-    device = models.ForeignKey(
-        to='dcim.Device',
-        on_delete=models.CASCADE,
-        related_name='inventory_items'
-    )
     parent = models.ForeignKey(
         to='self',
         on_delete=models.CASCADE,
@@ -1073,15 +955,6 @@ class InventoryItem(ComponentModel):
         blank=True,
         null=True
     )
-    name = models.CharField(
-        max_length=50,
-        verbose_name='Name'
-    )
-    _name = NaturalOrderingField(
-        target_field='name',
-        max_length=100,
-        blank=True
-    )
     manufacturer = models.ForeignKey(
         to='dcim.Manufacturer',
         on_delete=models.PROTECT,
@@ -1116,16 +989,13 @@ class InventoryItem(ComponentModel):
     tags = TaggableManager(through=TaggedItem)
 
     csv_headers = [
-        'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
+        'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
     ]
 
     class Meta:
         ordering = ('device__id', 'parent__id', '_name')
         unique_together = ('device', 'parent', 'name')
 
-    def __str__(self):
-        return self.name
-
     def get_absolute_url(self):
         return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
 
@@ -1133,6 +1003,7 @@ class InventoryItem(ComponentModel):
         return (
             self.device.name or '{{{}}}'.format(self.device.pk),
             self.name,
+            self.label,
             self.manufacturer.name if self.manufacturer else None,
             self.part_id,
             self.serial,

+ 27 - 49
netbox/dcim/tables.py

@@ -110,21 +110,6 @@ POWERPANEL_POWERFEED_COUNT = """
 """
 
 
-def get_component_template_actions(model_name):
-    return """
-        {{% if perms.dcim.change_{model_name} %}}
-            <a href="{{% url 'dcim:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning">
-                <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
-            </a>
-        {{% endif %}}
-        {{% if perms.dcim.delete_{model_name} %}}
-            <a href="{{% url 'dcim:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger">
-                <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
-            </a>
-        {{% endif %}}
-    """.format(model_name=model_name).strip()
-
-
 #
 # Regions
 #
@@ -401,10 +386,9 @@ class ComponentTemplateTable(BaseTable):
 
 
 class ConsolePortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('consoleporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=ConsolePortTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -414,10 +398,9 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
 
 
 class ConsoleServerPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('consoleserverporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=ConsoleServerPortTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -427,10 +410,9 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
 
 
 class PowerPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('powerporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=PowerPortTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -440,10 +422,9 @@ class PowerPortTemplateTable(ComponentTemplateTable):
 
 
 class PowerOutletTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('poweroutlettemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=PowerOutletTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -456,10 +437,9 @@ class InterfaceTemplateTable(ComponentTemplateTable):
     mgmt_only = BooleanColumn(
         verbose_name='Management Only'
     )
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('interfacetemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=InterfaceTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -472,10 +452,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
     rear_port_position = tables.Column(
         verbose_name='Position'
     )
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('frontporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=FrontPortTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -485,10 +464,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
 
 
 class RearPortTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('rearporttemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=RearPortTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -498,10 +476,9 @@ class RearPortTemplateTable(ComponentTemplateTable):
 
 
 class DeviceBayTemplateTable(ComponentTemplateTable):
-    actions = tables.TemplateColumn(
-        template_code=get_component_template_actions('devicebaytemplate'),
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=DeviceBayTemplate,
+        buttons=('edit', 'delete')
     )
 
     class Meta(BaseTable.Meta):
@@ -784,9 +761,10 @@ class InventoryItemTable(DeviceComponentTable):
     class Meta(DeviceComponentTable.Meta):
         model = InventoryItem
         fields = (
-            'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered'
+            'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
+            'discovered',
         )
-        default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag')
+        default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
 
 
 #

+ 4 - 0
netbox/dcim/tests/test_api.py

@@ -1,5 +1,6 @@
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
 
@@ -131,6 +132,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
             },
         ]
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_site_graphs(self):
         """
         Test retrieval of Graphs assigned to Sites.
@@ -900,6 +902,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
             },
         ]
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_device_graphs(self):
         """
         Test retrieval of Graphs assigned to Devices.
@@ -1156,6 +1159,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
             },
         ]
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_interface_graphs(self):
         """
         Test retrieval of Graphs assigned to Devices.

+ 10 - 8
netbox/dcim/tests/test_views.py

@@ -4,6 +4,7 @@ import pytz
 import yaml
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from netaddr import EUI
 
@@ -376,6 +377,7 @@ class DeviceTypeTestCase(
             'is_full_depth': False,
         }
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_import_objects(self):
         """
         Custom import test for YAML-based imports (versus CSV)
@@ -479,45 +481,45 @@ device-bays:
         self.assertEqual(dt.comments, 'test comment')
 
         # Verify all of the components were created
-        self.assertEqual(dt.consoleport_templates.count(), 3)
+        self.assertEqual(dt.consoleporttemplates.count(), 3)
         cp1 = ConsolePortTemplate.objects.first()
         self.assertEqual(cp1.name, 'Console Port 1')
         self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
 
-        self.assertEqual(dt.consoleserverport_templates.count(), 3)
+        self.assertEqual(dt.consoleserverporttemplates.count(), 3)
         csp1 = ConsoleServerPortTemplate.objects.first()
         self.assertEqual(csp1.name, 'Console Server Port 1')
         self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
 
-        self.assertEqual(dt.powerport_templates.count(), 3)
+        self.assertEqual(dt.powerporttemplates.count(), 3)
         pp1 = PowerPortTemplate.objects.first()
         self.assertEqual(pp1.name, 'Power Port 1')
         self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
 
-        self.assertEqual(dt.poweroutlet_templates.count(), 3)
+        self.assertEqual(dt.poweroutlettemplates.count(), 3)
         po1 = PowerOutletTemplate.objects.first()
         self.assertEqual(po1.name, 'Power Outlet 1')
         self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
         self.assertEqual(po1.power_port, pp1)
         self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
 
-        self.assertEqual(dt.interface_templates.count(), 3)
+        self.assertEqual(dt.interfacetemplates.count(), 3)
         iface1 = InterfaceTemplate.objects.first()
         self.assertEqual(iface1.name, 'Interface 1')
         self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
         self.assertTrue(iface1.mgmt_only)
 
-        self.assertEqual(dt.rearport_templates.count(), 3)
+        self.assertEqual(dt.rearporttemplates.count(), 3)
         rp1 = RearPortTemplate.objects.first()
         self.assertEqual(rp1.name, 'Rear Port 1')
 
-        self.assertEqual(dt.frontport_templates.count(), 3)
+        self.assertEqual(dt.frontporttemplates.count(), 3)
         fp1 = FrontPortTemplate.objects.first()
         self.assertEqual(fp1.name, 'Front Port 1')
         self.assertEqual(fp1.rear_port, rp1)
         self.assertEqual(fp1.rear_port_position, 1)
 
-        self.assertEqual(dt.device_bay_templates.count(), 3)
+        self.assertEqual(dt.devicebaytemplates.count(), 3)
         db1 = DeviceBayTemplate.objects.first()
         self.assertEqual(db1.name, 'Device Bay 1')
 

+ 8 - 0
netbox/dcim/urls.py

@@ -98,6 +98,7 @@ urlpatterns = [
     # Console port templates
     path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
     path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
+    path('console-port-templates/rename/', views.ConsolePortTemplateBulkRenameView.as_view(), name='consoleporttemplate_bulk_rename'),
     path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
     path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
     path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
@@ -105,6 +106,7 @@ urlpatterns = [
     # Console server port templates
     path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
     path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
+    path('console-server-port-templates/rename/', views.ConsoleServerPortTemplateBulkRenameView.as_view(), name='consoleserverporttemplate_bulk_rename'),
     path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
     path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
     path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
@@ -112,6 +114,7 @@ urlpatterns = [
     # Power port templates
     path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
     path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
+    path('power-port-templates/rename/', views.PowerPortTemplateBulkRenameView.as_view(), name='powerporttemplate_bulk_rename'),
     path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
     path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
     path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
@@ -119,6 +122,7 @@ urlpatterns = [
     # Power outlet templates
     path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
     path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
+    path('power-outlet-templates/rename/', views.PowerOutletTemplateBulkRenameView.as_view(), name='poweroutlettemplate_bulk_rename'),
     path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
     path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
     path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
@@ -126,6 +130,7 @@ urlpatterns = [
     # Interface templates
     path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
     path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
+    path('interface-templates/rename/', views.InterfaceTemplateBulkRenameView.as_view(), name='interfacetemplate_bulk_rename'),
     path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
     path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
     path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
@@ -133,6 +138,7 @@ urlpatterns = [
     # Front port templates
     path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
     path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
+    path('front-port-templates/rename/', views.FrontPortTemplateBulkRenameView.as_view(), name='frontporttemplate_bulk_rename'),
     path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
     path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
     path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
@@ -140,6 +146,7 @@ urlpatterns = [
     # Rear port templates
     path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
     path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
+    path('rear-port-templates/rename/', views.RearPortTemplateBulkRenameView.as_view(), name='rearporttemplate_bulk_rename'),
     path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
     path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
     path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
@@ -147,6 +154,7 @@ urlpatterns = [
     # Device bay templates
     path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
     path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
+    path('device-bay-templates/rename/', views.DeviceBayTemplateBulkRenameView.as_view(), name='devicebaytemplate_bulk_rename'),
     path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
     path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
     path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),

+ 42 - 10
netbox/dcim/views.py

@@ -640,6 +640,10 @@ class ConsolePortTemplateBulkEditView(BulkEditView):
     form = forms.ConsolePortTemplateBulkEditForm
 
 
+class ConsolePortTemplateBulkRenameView(BulkRenameView):
+    queryset = ConsolePortTemplate.objects.all()
+
+
 class ConsolePortTemplateBulkDeleteView(BulkDeleteView):
     queryset = ConsolePortTemplate.objects.all()
     table = tables.ConsolePortTemplateTable
@@ -671,6 +675,10 @@ class ConsoleServerPortTemplateBulkEditView(BulkEditView):
     form = forms.ConsoleServerPortTemplateBulkEditForm
 
 
+class ConsoleServerPortTemplateBulkRenameView(BulkRenameView):
+    queryset = ConsoleServerPortTemplate.objects.all()
+
+
 class ConsoleServerPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = ConsoleServerPortTemplate.objects.all()
     table = tables.ConsoleServerPortTemplateTable
@@ -702,6 +710,10 @@ class PowerPortTemplateBulkEditView(BulkEditView):
     form = forms.PowerPortTemplateBulkEditForm
 
 
+class PowerPortTemplateBulkRenameView(BulkRenameView):
+    queryset = PowerPortTemplate.objects.all()
+
+
 class PowerPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = PowerPortTemplate.objects.all()
     table = tables.PowerPortTemplateTable
@@ -733,6 +745,10 @@ class PowerOutletTemplateBulkEditView(BulkEditView):
     form = forms.PowerOutletTemplateBulkEditForm
 
 
+class PowerOutletTemplateBulkRenameView(BulkRenameView):
+    queryset = PowerOutletTemplate.objects.all()
+
+
 class PowerOutletTemplateBulkDeleteView(BulkDeleteView):
     queryset = PowerOutletTemplate.objects.all()
     table = tables.PowerOutletTemplateTable
@@ -764,6 +780,10 @@ class InterfaceTemplateBulkEditView(BulkEditView):
     form = forms.InterfaceTemplateBulkEditForm
 
 
+class InterfaceTemplateBulkRenameView(BulkRenameView):
+    queryset = InterfaceTemplate.objects.all()
+
+
 class InterfaceTemplateBulkDeleteView(BulkDeleteView):
     queryset = InterfaceTemplate.objects.all()
     table = tables.InterfaceTemplateTable
@@ -795,6 +815,10 @@ class FrontPortTemplateBulkEditView(BulkEditView):
     form = forms.FrontPortTemplateBulkEditForm
 
 
+class FrontPortTemplateBulkRenameView(BulkRenameView):
+    queryset = FrontPortTemplate.objects.all()
+
+
 class FrontPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = FrontPortTemplate.objects.all()
     table = tables.FrontPortTemplateTable
@@ -826,6 +850,10 @@ class RearPortTemplateBulkEditView(BulkEditView):
     form = forms.RearPortTemplateBulkEditForm
 
 
+class RearPortTemplateBulkRenameView(BulkRenameView):
+    queryset = RearPortTemplate.objects.all()
+
+
 class RearPortTemplateBulkDeleteView(BulkDeleteView):
     queryset = RearPortTemplate.objects.all()
     table = tables.RearPortTemplateTable
@@ -857,6 +885,10 @@ class DeviceBayTemplateBulkEditView(BulkEditView):
     form = forms.DeviceBayTemplateBulkEditForm
 
 
+class DeviceBayTemplateBulkRenameView(BulkRenameView):
+    queryset = DeviceBayTemplate.objects.all()
+
+
 class DeviceBayTemplateBulkDeleteView(BulkDeleteView):
     queryset = DeviceBayTemplate.objects.all()
     table = tables.DeviceBayTemplateTable
@@ -952,7 +984,7 @@ class DeviceView(ObjectView):
             vc_members = []
 
         # Console ports
-        console_ports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'connected_endpoint__device', 'cable',
         )
 
@@ -964,7 +996,7 @@ class DeviceView(ObjectView):
         )
 
         # Power ports
-        power_ports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             '_connected_poweroutlet__device', 'cable',
         )
 
@@ -982,15 +1014,15 @@ class DeviceView(ObjectView):
         )
 
         # Front ports
-        front_ports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'rear_port', 'cable',
         )
 
         # Rear ports
-        rear_ports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
+        rearports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
 
         # Device bays
-        device_bays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
+        devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'installed_device__device_type__manufacturer',
         )
 
@@ -1011,14 +1043,14 @@ class DeviceView(ObjectView):
 
         return render(request, 'dcim/device.html', {
             'device': device,
-            'console_ports': console_ports,
+            'consoleports': consoleports,
             'consoleserverports': consoleserverports,
-            'power_ports': power_ports,
+            'powerports': powerports,
             'poweroutlets': poweroutlets,
             'interfaces': interfaces,
-            'device_bays': device_bays,
-            'front_ports': front_ports,
-            'rear_ports': rear_ports,
+            'devicebays': devicebays,
+            'frontports': frontports,
+            'rearports': rearports,
             'services': services,
             'secrets': secrets,
             'vc_members': vc_members,

+ 2 - 0
netbox/secrets/tests/test_views.py

@@ -1,5 +1,6 @@
 import base64
 
+from django.test import override_settings
 from django.urls import reverse
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
@@ -96,6 +97,7 @@ class SecretTestCase(
         self.session_key = SessionKey(userkey=userkey)
         self.session_key.save(master_key)
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_import_objects(self):
         self.add_permissions('secrets.add_secret')
 

+ 471 - 416
netbox/templates/dcim/device.html

@@ -101,7 +101,7 @@
         </li>
         <li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
             <a href="{% url 'dcim:device_inventory' pk=device.pk %}">
-                Inventory <span class="badge">{{ device.inventory_items.count }}</span>
+                Inventory <span class="badge">{{ device.inventoryitems.unrestricted.count }}</span>
             </a>
         </li>
         {% if perms.dcim.napalm_read_device %}
@@ -329,86 +329,6 @@
             {% plugin_left_page device %}
         </div>
         <div class="col-md-6">
-            {% if console_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Console Ports</strong>
-                        </div>
-                        <table class="table table-hover panel-body component-list">
-                            {% for cp in console_ports %}
-                                {% include 'dcim/inc/consoleport.html' %}
-                            {% endfor %}
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if console_ports and perms.dcim.change_consoleport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if console_ports and perms.dcim.delete_consoleport %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if console_ports and perms.dcim.add_consoleport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
-                                    </a>
-                                </div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if power_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Power Ports</strong>
-                        </div>
-                        <table class="table table-hover panel-body component-list">
-                            {% for pp in power_ports %}
-                                {% include 'dcim/inc/powerport.html' %}
-                            {% endfor %}
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if power_ports and perms.dcim.change_powerport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if power_ports and perms.dcim.delete_powerport %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if power_ports and perms.dcim.add_powerport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
-                                    </a>
-                                </div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
             {% if power_ports and poweroutlets %}
                 <div class="panel panel-default">
                     <div class="panel-heading">
@@ -554,355 +474,490 @@
     </div>
     <div class="row">
         <div class="col-md-12">
-            {% if device_bays or device.device_type.is_parent_device %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Device Bays</strong>
-                        </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Status</th>
-                                    <th>Description</th>
-                                    <th colspan="2">Installed Device</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for devicebay in device_bays %}
-                                    {% include 'dcim/inc/devicebay.html' %}
-                                {% empty %}
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="active">
+                    <a href="#interfaces" role="tab" data-toggle="tab">Interfaces {% badge interfaces|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#frontports" role="tab" data-toggle="tab">Front Ports {% badge frontports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#rearports" role="tab" data-toggle="tab">Rear Ports {% badge rearports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleports" role="tab" data-toggle="tab">Console Ports {% badge consoleports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleserverports" role="tab" data-toggle="tab">Console Server Ports {% badge consoleserverports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#powerports" role="tab" data-toggle="tab">Power Ports {% badge powerports|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#poweroutlets" role="tab" data-toggle="tab">Power Outlets {% badge poweroutlets|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#devicebays" role="tab" data-toggle="tab">Device Bays {% badge devicebays|length %}</a>
+                </li>
+            </ul>
+            <div class="tab-content">
+                <div role="tabpanel" class="tab-pane active" id="interfaces">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Interfaces</strong>
+                                <div class="pull-right noprint">
+                                    <button class="btn btn-default btn-xs toggle-ips" selected="selected">
+                                        <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
+                                    </button>
+                                </div>
+                                <div class="col-md-2 pull-right noprint">
+                                    <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
+                                </div>
+                            </div>
+                            <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
+                                <thead>
                                     <tr>
-                                        <td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td>
+                                        {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>LAG</th>
+                                        <th>Description</th>
+                                        <th>MTU</th>
+                                        <th>Mode</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
                                     </tr>
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if device_bays and perms.dcim.change_devicebay %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                            {% endif %}
-                            {% if device_bays and perms.dcim.delete_devicebay %}
-                                <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_devicebay %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if interfaces %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Interfaces</strong>
-                            <div class="pull-right noprint">
-                                <button class="btn btn-default btn-xs toggle-ips" selected="selected">
-                                    <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
-                                </button>
+                                </thead>
+                                <tbody>
+                                    {% for iface in interfaces %}
+                                        {% include 'dcim/inc/interface.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if interfaces and perms.dcim.change_interface %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                {% endif %}
+                                {% if interfaces and perms.dcim.change_interface %}
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if interfaces and perms.dcim.delete_interface %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_interface %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                             </div>
+                        </div>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="frontports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Front Ports</strong>
                             </div>
-                            <div class="col-md-2 pull-right noprint">
-                                <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Rear Port</th>
+                                        <th>Position</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for frontport in frontports %}
+                                        {% include 'dcim/inc/frontport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if frontports and perms.dcim.change_frontport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if frontports and perms.dcim.delete_frontport %}
+                                    <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_frontport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
                             </div>
                         </div>
-                        <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>LAG</th>
-                                    <th>Description</th>
-                                    <th>MTU</th>
-                                    <th>Mode</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for iface in interfaces %}
-                                    {% include 'dcim/inc/interface.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if interfaces and perms.dcim.change_interface %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                            {% endif %}
-                            {% if interfaces and perms.dcim.change_interface %}
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if interfaces and perms.dcim.delete_interface %}
-                                <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_interface %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if consoleserverports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Console Server Ports</strong>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="rearports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Rear Ports</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Positions</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for rearport in rearports %}
+                                        {% include 'dcim/inc/rearport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if rearports and perms.dcim.change_rearport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if rearports and perms.dcim.delete_rearport %}
+                                    <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_rearport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Type</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for csp in consoleserverports %}
-                                    {% include 'dcim/inc/consoleserverport.html' %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Console Ports</strong>
+                            </div>
+                            <table class="table table-hover panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                {% for cp in consoleports %}
+                                    {% include 'dcim/inc/consoleport.html' %}
                                 {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if consoleserverports and perms.dcim.change_consoleport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if consoleserverports and perms.dcim.delete_consoleserverport %}
-                                <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_consoleserverport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if consoleports and perms.dcim.change_consoleport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if consoleports and perms.dcim.delete_consoleport %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if consoleports and perms.dcim.add_consoleport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
+                                        </a>
+                                    </div>
+                                {% endif %}
+                            </div>
                         </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if poweroutlets %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Power Outlets</strong>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleserverports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Console Server Ports</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for csp in consoleserverports %}
+                                        {% include 'dcim/inc/consoleserverport.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if consoleserverports and perms.dcim.change_consoleport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if consoleserverports and perms.dcim.delete_consoleserverport %}
+                                    <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_consoleserverport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                <th>Type</th>
-                                    <th>Input/Leg</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="3">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for po in poweroutlets %}
-                                    {% include 'dcim/inc/poweroutlet.html' %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="powerports">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Power Ports</strong>
+                            </div>
+                            <table class="table table-hover panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Type</th>
+                                        <th>Draw</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="2">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                {% for pp in powerports %}
+                                    {% include 'dcim/inc/powerport.html' %}
                                 {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if poweroutlets and perms.dcim.change_powerport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if poweroutlets and perms.dcim.delete_poweroutlet %}
-                                <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_poweroutlet %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if front_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Front Ports</strong>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if powerports and perms.dcim.change_powerport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if powerports and perms.dcim.delete_powerport %}
+                                    <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if powerports and perms.dcim.add_powerport %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
+                                        </a>
+                                    </div>
+                                {% endif %}
+                            </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="poweroutlets">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Power Outlets</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
                                     <th>Type</th>
-                                    <th>Rear Port</th>
-                                    <th>Position</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for frontport in front_ports %}
-                                    {% include 'dcim/inc/frontport.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if front_ports and perms.dcim.change_frontport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if front_ports and perms.dcim.delete_frontport %}
-                                <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_frontport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
-                        </div>
-                    </div>
-                </form>
-            {% endif %}
-            {% if rear_ports %}
-                <form method="post">
-                    {% csrf_token %}
-                    <div class="panel panel-default">
-                        <div class="panel-heading">
-                            <strong>Rear Ports</strong>
+                                        <th>Input/Leg</th>
+                                        <th>Description</th>
+                                        <th>Cable</th>
+                                        <th colspan="3">Connection</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for po in poweroutlets %}
+                                        {% include 'dcim/inc/poweroutlet.html' %}
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if poweroutlets and perms.dcim.change_powerport %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                    <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                                    </button>
+                                    <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                                    </button>
+                                {% endif %}
+                                {% if poweroutlets and perms.dcim.delete_poweroutlet %}
+                                    <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_poweroutlet %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                            </div>
                         </div>
-                        <table class="table table-hover table-headings panel-body component-list">
-                            <thead>
-                                <tr>
-                                    {% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
-                                        <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
-                                    {% endif %}
-                                    <th>Name</th>
-                                    <th>Type</th>
-                                    <th>Positions</th>
-                                    <th>Description</th>
-                                    <th>Cable</th>
-                                    <th colspan="2">Connection</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for rearport in rear_ports %}
-                                    {% include 'dcim/inc/rearport.html' %}
-                                {% endfor %}
-                            </tbody>
-                        </table>
-                        <div class="panel-footer noprint">
-                            {% if rear_ports and perms.dcim.change_rearport %}
-                                <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
-                                </button>
-                                <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
-                                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
-                                </button>
-                                <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
-                                </button>
-                            {% endif %}
-                            {% if rear_ports and perms.dcim.delete_rearport %}
-                                <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
-                                </button>
-                            {% endif %}
-                            {% if perms.dcim.add_rearport %}
-                                <div class="pull-right">
-                                    <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
-                                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
-                                    </a>
-                                </div>
-                                <div class="clearfix"></div>
-                            {% endif %}
+                    </form>
+                </div>
+                <div role="tabpanel" class="tab-pane" id="devicebays">
+                    <form method="post">
+                        {% csrf_token %}
+                        <div class="panel panel-default">
+                            <div class="panel-heading">
+                                <strong>Device Bays</strong>
+                            </div>
+                            <table class="table table-hover table-headings panel-body component-list">
+                                <thead>
+                                    <tr>
+                                        {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
+                                            <th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
+                                        {% endif %}
+                                        <th>Name</th>
+                                        <th>Status</th>
+                                        <th>Description</th>
+                                        <th colspan="2">Installed Device</th>
+                                        <th></th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for devicebay in devicebays %}
+                                        {% include 'dcim/inc/devicebay.html' %}
+                                    {% empty %}
+                                        <tr>
+                                            <td colspan="5" class="text-center text-muted">&mdash; No device bays defined &mdash;</td>
+                                        </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                            <div class="panel-footer noprint">
+                                {% if devicebays and perms.dcim.change_devicebay %}
+                                    <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                                    </button>
+                                {% endif %}
+                                {% if devicebays and perms.dcim.delete_devicebay %}
+                                    <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
+                                    </button>
+                                {% endif %}
+                                {% if perms.dcim.add_devicebay %}
+                                    <div class="pull-right">
+                                        <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
+                                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
+                                        </a>
+                                    </div>
+                                    <div class="clearfix"></div>
+                                {% endif %}
+                             </div>
                         </div>
-                    </div>
-                </form>
-            {% endif %}
+                    </form>
+                </div>
+            </div>
         </div>
     </div>
 {% include 'inc/modal.html' with name='graphs' title='Graphs' %}

+ 139 - 131
netbox/templates/dcim/devicetype.html

@@ -63,149 +63,157 @@
 {% endblock %}
 
 {% block content %}
-<div class="row">
-    <div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Chassis</strong>
-            </div>
-            <table class="table table-hover panel-body attr-table">
-                <tr>
-                    <td>Manufacturer</td>
-                    <td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
-                </tr>
-                <tr>
-                    <td>Model Name</td>
-                    <td>
-                        {{ devicetype.model }}<br/>
-                        <small class="text-muted">{{ devicetype.slug }}</small>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Part Number</td>
-                    <td>{{ devicetype.part_number|placeholder }}</td>
-                </tr>
-                <tr>
-                    <td>Height (U)</td>
-                    <td>{{ devicetype.u_height }}</td>
-                </tr>
-                <tr>
-                    <td>Full Depth</td>
-                    <td>
-                        {% if devicetype.is_full_depth %}
-                            <i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
-                        {% else %}
-                            <i class="glyphicon glyphicon-remove text-danger" title="No"></i>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Parent/Child</td>
-                    <td>
-                        {{ devicetype.get_subdevice_role_display|placeholder }}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Front Image</td>
-                    <td>
-                        {% if devicetype.front_image %}
-                            <a href="{{ devicetype.front_image.url }}">
-                                <img src="{{ devicetype.front_image.url }}" alt="{{ devicetype.front_image.name }}" class="img-responsive" />
-                            </a>
-                        {% else %}
-                            <span class="text-muted">&mdash;</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Rear Image</td>
-                    <td>
-                        {% if devicetype.rear_image %}
-                            <a href="{{ devicetype.rear_image.url }}">
-                                <img src="{{ devicetype.rear_image.url }}" alt="{{ devicetype.rear_image.name }}" class="img-responsive" />
-                            </a>
-                        {% else %}
-                            <span class="text-muted">&mdash;</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Instances</td>
-                    <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
-                </tr>
-            </table>
-        </div>
-        {% plugin_left_page devicetype %}
-    </div>
-    <div class="col-md-6">
-        {% include 'inc/custom_fields_panel.html' with obj=devicetype %}
-        {% include 'extras/inc/tags_panel.html' with tags=devicetype.tags.all url='dcim:devicetype_list' %}
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Comments</strong>
-            </div>
-            <div class="panel-body rendered-markdown">
-                {% if devicetype.comments %}
-                    {{ devicetype.comments|render_markdown }}
-                {% else %}
-                    <span class="text-muted">None</span>
-                {% endif %}
-            </div>
-        </div>
-        {% plugin_right_page devicetype %}
-    </div>
-</div>
-{% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %}
     <div class="row">
         <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:consoleporttemplate_add' edit_url='dcim:consoleporttemplate_bulk_edit' delete_url='dcim:consoleporttemplate_bulk_delete' %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Chassis</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <td>Manufacturer</td>
+                        <td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
+                    </tr>
+                    <tr>
+                        <td>Model Name</td>
+                        <td>
+                            {{ devicetype.model }}<br/>
+                            <small class="text-muted">{{ devicetype.slug }}</small>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Part Number</td>
+                        <td>{{ devicetype.part_number|placeholder }}</td>
+                    </tr>
+                    <tr>
+                        <td>Height (U)</td>
+                        <td>{{ devicetype.u_height }}</td>
+                    </tr>
+                    <tr>
+                        <td>Full Depth</td>
+                        <td>
+                            {% if devicetype.is_full_depth %}
+                                <i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
+                            {% else %}
+                                <i class="glyphicon glyphicon-remove text-danger" title="No"></i>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Parent/Child</td>
+                        <td>
+                            {{ devicetype.get_subdevice_role_display|placeholder }}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Front Image</td>
+                        <td>
+                            {% if devicetype.front_image %}
+                                <a href="{{ devicetype.front_image.url }}">
+                                    <img src="{{ devicetype.front_image.url }}" alt="{{ devicetype.front_image.name }}" class="img-responsive" />
+                                </a>
+                            {% else %}
+                                <span class="text-muted">&mdash;</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Rear Image</td>
+                        <td>
+                            {% if devicetype.rear_image %}
+                                <a href="{{ devicetype.rear_image.url }}">
+                                    <img src="{{ devicetype.rear_image.url }}" alt="{{ devicetype.rear_image.name }}" class="img-responsive" />
+                                </a>
+                            {% else %}
+                                <span class="text-muted">&mdash;</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>Instances</td>
+                        <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ instance_count }}</a></td>
+                    </tr>
+                </table>
+            </div>
+            {% plugin_left_page devicetype %}
         </div>
         <div class="col-md-6">
-             {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:powerporttemplate_add' edit_url='dcim:powerporttemplate_bulk_edit' delete_url='dcim:powerporttemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-<div class="row">
-    <div class="col-md-12">
-        {% plugin_full_width_page devicetype %}
-    </div>
-</div>
-{% if devicetype.is_parent_device or devicebay_table.rows %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url='dcim:devicebaytemplate_bulk_edit' delete_url='dcim:devicebaytemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-{% if devicetype.consoleserverport_templates.exists %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:consoleserverporttemplate_add' edit_url='dcim:consoleserverporttemplate_bulk_edit' delete_url='dcim:consoleserverporttemplate_bulk_delete' %}
+            {% include 'inc/custom_fields_panel.html' with obj=devicetype %}
+            {% include 'extras/inc/tags_panel.html' with tags=devicetype.tags.all url='dcim:devicetype_list' %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Comments</strong>
+                </div>
+                <div class="panel-body rendered-markdown">
+                    {% if devicetype.comments %}
+                        {{ devicetype.comments|render_markdown }}
+                    {% else %}
+                        <span class="text-muted">None</span>
+                    {% endif %}
+                </div>
+            </div>
+            {% plugin_right_page devicetype %}
         </div>
     </div>
-{% endif %}
-{% if devicetype.poweroutlet_templates.exists %}
     <div class="row">
         <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:poweroutlettemplate_add' edit_url='dcim:poweroutlettemplate_bulk_edit' delete_url='dcim:poweroutlettemplate_bulk_delete' %}
+            {% plugin_full_width_page devicetype %}
         </div>
     </div>
-{% endif %}
-{% if devicetype.interface_templates.exists %}
     <div class="row">
         <div class="col-md-12">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:interfacetemplate_add' edit_url='dcim:interfacetemplate_bulk_edit' delete_url='dcim:interfacetemplate_bulk_delete' %}
-        </div>
-    </div>
-{% endif %}
-{% if devicetype.frontport_templates.exists or devicetype.rearport_templates.exists %}
-    <div class="row">
-        <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:frontporttemplate_add' edit_url='dcim:frontporttemplate_bulk_edit' delete_url='dcim:frontporttemplate_bulk_delete' %}
-        </div>
-        <div class="col-md-6">
-            {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:rearporttemplate_add' edit_url='dcim:rearporttemplate_bulk_edit' delete_url='dcim:rearporttemplate_bulk_delete' %}
+            <ul class="nav nav-tabs" role="tablist">
+                <li role="presentation" class="active">
+                    <a href="#interfaces" role="tab" data-toggle="tab">Interfaces {% badge interface_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#frontports" role="tab" data-toggle="tab">Front Ports {% badge front_port_table_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#rearports" role="tab" data-toggle="tab">Rear Ports {% badge rear_port_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleports" role="tab" data-toggle="tab">Console Ports {% badge consoleport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#consoleserverports" role="tab" data-toggle="tab">Console Server Ports {% badge consoleserverport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#powerports" role="tab" data-toggle="tab">Power Ports {% badge powerport_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#poweroutlets" role="tab" data-toggle="tab">Power Outlets {% badge poweroutlet_table.rows|length %}</a>
+                </li>
+                <li role="presentation">
+                    <a href="#devicebays" role="tab" data-toggle="tab">Device Bays {% badge devicebay_table.rows|length %}</a>
+                </li>
+            </ul>
+            <div class="tab-content">
+                <div role="tabpanel" class="tab-pane active" id="interfaces">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="frontports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="rearports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="consoleserverports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="powerports">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="poweroutlets">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' %}
+                </div>
+                <div role="tabpanel" class="tab-pane" id="devicebays">
+                    {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' %}
+                </div>
+            </div>
         </div>
     </div>
-{% endif %}
 {% endblock %}

+ 0 - 2
netbox/templates/dcim/inc/consoleport.html

@@ -18,8 +18,6 @@
         {% if cp.type %}{{ cp.get_type_display }}{% else %}&mdash;{% endif %}
     </td>
 
-    <td></td>
-
     {# Description #}
     <td>
         {{ cp.description }}

+ 40 - 0
netbox/templates/dcim/inc/device_component_table.html

@@ -0,0 +1,40 @@
+{% load helpers %}
+{% load perms %}
+<form method="post">
+    {% csrf_token %}
+    <div class="panel panel-default">
+        <div class="panel-heading">
+            <strong>{{ title }}</strong>
+        </div>
+        <table class="table table-hover panel-body component-list">
+            {% for obj in components %}
+                {% include component_template %}
+            {% endfor %}
+        </table>
+        <div class="panel-footer noprint">
+            {% if components and perms.dcim.change_consoleport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
+                    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                    <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if components and perms.dcim.delete_consoleport %}
+                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                </button>
+            {% endif %}
+            {% if components and perms.dcim.add_consoleport %}
+                <div class="pull-right">
+                    <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
+                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
+                    </a>
+                </div>
+            {% endif %}
+        </div>
+    </div>
+</form>

+ 11 - 11
netbox/templates/dcim/inc/devicetype_component_table.html

@@ -1,3 +1,4 @@
+{% load helpers %}
 {% if perms.dcim.change_devicetype %}
     <form method="post">
         {% csrf_token %}
@@ -8,19 +9,18 @@
             {% include 'responsive_table.html' %}
             <div class="panel-footer noprint">
                 {% if table.rows %}
-                    {% if edit_url %}
-                        <button type="submit" name="_edit" formaction="{% url edit_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
-                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
-                        </button>
-                    {% endif %}
-                    {% if delete_url %}
-                        <button type="submit" name="_delete" formaction="{% url delete_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
-                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
-                        </button>
-                    {% endif %}
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
+                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
+                    </button>
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
+                        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
+                    </button>
+                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
+                        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
+                    </button>
                 {% endif %}
                 <div class="pull-right">
-                    <a href="{% url add_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-primary btn-xs">
+                    <a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-primary btn-xs">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                         Add {{ title }}
                     </a>

+ 1 - 1
netbox/templates/dcim/inc/interface.html

@@ -31,7 +31,7 @@
         {% if iface.description %}
             {{ iface.description }}<br/>
         {% endif %}
-        {% for tag in iface.tags.all %}
+        {% for tag in iface.tags.all.unrestricted %}
             {% tag tag %}
         {% empty %}
             {% if not iface.description %}&mdash;{% endif %}

+ 1 - 1
netbox/templates/extras/inc/tags_panel.html

@@ -4,7 +4,7 @@
         <strong>Tags</strong>
     </div>
     <div class="panel-body">
-        {% for tag in tags.all %}
+        {% for tag in tags.all.unrestricted %}
             {% tag tag url %}
         {% empty %}
             <span class="text-muted">No tags assigned</span>

+ 1 - 0
netbox/templates/utilities/templatetags/badge.html

@@ -0,0 +1 @@
+{% if value or show_empty %}<span class="badge">{{ value }}</span>{% endif %}

+ 15 - 7
netbox/utilities/tables.py

@@ -130,25 +130,28 @@ class ButtonsColumn(tables.TemplateColumn):
     :param model: Model class to use for calculating URL view names
     :param prepend_content: Additional template content to render in the column (optional)
     """
+    buttons = ('changelog', 'edit', 'delete')
     attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
     # Note that braces are escaped to allow for string formatting prior to template rendering
     template_code = """
-    <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
-        <i class="fa fa-history"></i>
-    </a>
-    {{% if perms.{app_label}.change_{model_name} %}}
+    {{% if "changelog" in buttons %}}
+        <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
+            <i class="fa fa-history"></i>
+        </a>
+    {{% endif %}}
+    {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
         <a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
             <i class="fa fa-pencil"></i>
         </a>
     {{% endif %}}
-    {{% if perms.{app_label}.delete_{model_name} %}}
+    {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
         <a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
             <i class="fa fa-trash"></i>
         </a>
     {{% endif %}}
     """
 
-    def __init__(self, model, *args, pk_field='pk', prepend_template=None, **kwargs):
+    def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, **kwargs):
         if prepend_template:
             prepend_template = prepend_template.replace('{', '{{')
             prepend_template = prepend_template.replace('}', '}}')
@@ -157,11 +160,16 @@ class ButtonsColumn(tables.TemplateColumn):
         template_code = self.template_code.format(
             app_label=model._meta.app_label,
             model_name=model._meta.model_name,
-            pk_field=pk_field
+            pk_field=pk_field,
+            buttons=buttons
         )
 
         super().__init__(template_code=template_code, *args, **kwargs)
 
+        self.extra_context.update({
+            'buttons': buttons or self.buttons,
+        })
+
     def header(self):
         return ''
 

+ 11 - 0
netbox/utilities/templatetags/helpers.py

@@ -242,3 +242,14 @@ def tag(tag, url_name=None):
         'tag': tag,
         'url_name': url_name,
     }
+
+
+@register.inclusion_tag('utilities/templatetags/badge.html')
+def badge(value, show_empty=False):
+    """
+    Display the specified number as a badge.
+    """
+    return {
+        'value': value,
+        'show_empty': show_empty,
+    }

+ 1 - 1
netbox/utilities/utils.py

@@ -217,7 +217,7 @@ def prepare_cloned_fields(instance):
 
     # Copy tags
     if is_taggable(instance):
-        params['tags'] = ','.join([t.name for t in instance.tags.all()])
+        params['tags'] = ','.join([t.name for t in instance.tags.all().unrestricted()])
 
     # Concatenate parameters into a URL query string
     param_string = '&'.join(

+ 2 - 2
netbox/virtualization/migrations/0016_replicate_interfaces.py

@@ -51,8 +51,8 @@ def replicate_interfaces(apps, schema_editor):
     # Verify that all interfaces have been replicated
     assert replicated_count == original_interfaces.count(), "Replicated interfaces count does not match original count!"
 
-    # Delete original VM interfaces
-    original_interfaces.delete()
+    # Delete all interfaces not assigned to a Device
+    Interface.objects.filter(device__isnull=True).delete()
 
 
 class Migration(migrations.Migration):

+ 11 - 0
netbox/virtualization/models.py

@@ -9,7 +9,9 @@ from dcim.choices import InterfaceModeChoices
 from dcim.models import BaseInterface, Device
 from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
 from extras.utils import extras_features
+from utilities.fields import NaturalOrderingField
 from utilities.models import ChangeLoggedModel
+from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import serialize_object
@@ -387,6 +389,15 @@ class VMInterface(BaseInterface):
         on_delete=models.CASCADE,
         related_name='interfaces'
     )
+    name = models.CharField(
+        max_length=64
+    )
+    _name = NaturalOrderingField(
+        target_field='name',
+        naturalize_function=naturalize_interface,
+        max_length=100,
+        blank=True
+    )
     description = models.CharField(
         max_length=200,
         blank=True

+ 3 - 1
netbox/virtualization/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
 
@@ -244,7 +245,8 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
             },
         ]
 
-    def test_get_interface_graphs(self):
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+    def test_get_vminterface_graphs(self):
         """
         Test retrieval of Graphs assigned to VM interfaces.
         """