Add ModuleBay and ModuleBayTemplate models

This commit is contained in:
jeremystretch 2021-12-17 09:35:57 -05:00
parent 5f9f0e3ed3
commit e529d7fd3b
33 changed files with 1008 additions and 21 deletions

View File

@ -20,6 +20,8 @@ __all__ = [
'NestedInterfaceTemplateSerializer',
'NestedInventoryItemSerializer',
'NestedManufacturerSerializer',
'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer',
'NestedPlatformSerializer',
'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer',
@ -195,6 +197,14 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name']
class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
class Meta:
model = models.ModuleBayTemplate
fields = ['id', 'url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
@ -298,6 +308,15 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
# module = NestedModuleSerializer(read_only=True)
class Meta:
model = models.DeviceBay
fields = ['id', 'url', 'display', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer(read_only=True)

View File

@ -409,6 +409,15 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
]
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
device_type = NestedDeviceTypeSerializer()
class Meta:
model = ModuleBayTemplate
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = NestedDeviceTypeSerializer()
@ -707,6 +716,19 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
]
class ModuleBaySerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'tags', 'custom_fields', 'created',
'last_updated',
]
class DeviceBaySerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer()

View File

@ -28,6 +28,7 @@ router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register('rear-port-templates', views.RearPortTemplateViewSet)
router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
# Devices
@ -43,6 +44,7 @@ router.register('power-outlets', views.PowerOutletViewSet)
router.register('interfaces', views.InterfaceViewSet)
router.register('front-ports', views.FrontPortViewSet)
router.register('rear-ports', views.RearPortViewSet)
router.register('module-bays', views.ModuleBayViewSet)
router.register('device-bays', views.DeviceBayViewSet)
router.register('inventory-items', views.InventoryItemViewSet)

View File

@ -329,6 +329,12 @@ class RearPortTemplateViewSet(ModelViewSet):
filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(ModelViewSet):
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
@ -569,15 +575,22 @@ class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class ModuleBayViewSet(ModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags')
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device']

View File

@ -41,6 +41,8 @@ __all__ = (
'InventoryItemFilterSet',
'LocationFilterSet',
'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
'PathEndpointFilterSet',
'PlatformFilterSet',
'PowerConnectionFilterSet',
@ -447,6 +449,10 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
method='_pass_through_ports',
label='Has pass-through ports',
)
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter(
method='_device_bays',
label='Has device bays',
@ -490,6 +496,9 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
rearporttemplates__isnull=value
)
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebaytemplates__isnull=value)
def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebaytemplates__isnull=value)
@ -576,6 +585,13 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
fields = ['id', 'name', 'type', 'color', 'positions']
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
model = ModuleBayTemplate
fields = ['id', 'name']
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
@ -760,6 +776,10 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
method='_pass_through_ports',
label='Has pass-through ports',
)
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter(
method='_device_bays',
label='Has device bays',
@ -811,6 +831,9 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
rearports__isnull=value
)
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebays__isnull=value)
def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebays__isnull=value)
@ -1104,6 +1127,13 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
class ModuleBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta:
model = ModuleBay
fields = ['id', 'name', 'label', 'description']
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta:

View File

@ -13,6 +13,7 @@ __all__ = (
# 'FrontPortBulkCreateForm',
'InterfaceBulkCreateForm',
'InventoryItemBulkCreateForm',
'ModuleBayBulkCreateForm',
'PowerOutletBulkCreateForm',
'PowerPortBulkCreateForm',
'RearPortBulkCreateForm',
@ -95,6 +96,11 @@ class RearPortBulkCreateForm(
field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
model = DeviceBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags')

View File

@ -7,7 +7,6 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
from ipam.models import VLAN, ASN
from tenancy.models import Tenant
from utilities.forms import (
@ -33,6 +32,8 @@ __all__ = (
'InventoryItemBulkEditForm',
'LocationBulkEditForm',
'ManufacturerBulkEditForm',
'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm',
'PlatformBulkEditForm',
'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm',
@ -823,6 +824,23 @@ class RearPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('description',)
class ModuleBayTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBayTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
max_length=64,
required=False
)
description = forms.CharField(
required=False
)
class Meta:
nullable_fields = ('label', 'description')
class DeviceBayTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=DeviceBayTemplate.objects.all(),
@ -1076,6 +1094,20 @@ class RearPortBulkEditForm(
nullable_fields = ['label', 'description']
class ModuleBayBulkEditForm(
form_from_model(DeviceBay, ['label', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBay.objects.all(),
widget=forms.MultipleHiddenInput()
)
class Meta:
nullable_fields = ['label', 'description']
class DeviceBayBulkEditForm(
form_from_model(DeviceBay, ['label', 'description']),
AddRemoveTagsForm,

View File

@ -26,6 +26,7 @@ __all__ = (
'InventoryItemCSVForm',
'LocationCSVForm',
'ManufacturerCSVForm',
'ModuleBayCSVForm',
'PlatformCSVForm',
'PowerFeedCSVForm',
'PowerOutletCSVForm',
@ -678,6 +679,17 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
}
class ModuleBayCSVForm(CustomFieldModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleBay
fields = ('device', 'name', 'label', 'description')
class DeviceBayCSVForm(CustomFieldModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),

View File

@ -29,6 +29,7 @@ __all__ = (
'InventoryItemFilterForm',
'LocationFilterForm',
'ManufacturerFilterForm',
'ModuleBayFilterForm',
'PlatformFilterForm',
'PowerConnectionFilterForm',
'PowerFeedFilterForm',
@ -970,6 +971,16 @@ class RearPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
field_groups = [
['q', 'tag'],
['name', 'label'],
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
]
tag = TagFilterField(model)
class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
field_groups = [

View File

@ -39,6 +39,8 @@ __all__ = (
'InventoryItemForm',
'LocationForm',
'ManufacturerForm',
'ModuleBayForm',
'ModuleBayTemplateForm',
'PlatformForm',
'PopulateDeviceBayForm',
'PowerFeedForm',
@ -984,6 +986,17 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = DeviceBayTemplate
@ -1222,6 +1235,22 @@ class RearPortForm(CustomFieldModelForm):
}
class ModuleBayForm(CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ModuleBay
fields = [
'device', 'name', 'label', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
class DeviceBayForm(CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),

View File

@ -25,6 +25,8 @@ __all__ = (
'InterfaceCreateForm',
'InterfaceTemplateCreateForm',
'InventoryItemCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
'PowerPortCreateForm',
@ -327,6 +329,10 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
)
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
@ -619,6 +625,11 @@ class RearPortCreateForm(ComponentCreateForm):
)
class ModuleBayCreateForm(ComponentCreateForm):
model = ModuleBay
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
class DeviceBayCreateForm(ComponentCreateForm):
model = DeviceBay
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')

View File

@ -11,6 +11,7 @@ __all__ = (
'DeviceTypeImportForm',
'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm',
'ModuleBayTemplateImportForm',
'PowerOutletTemplateImportForm',
'PowerPortTemplateImportForm',
'RearPortTemplateImportForm',
@ -139,6 +140,15 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
]
class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'description',
]
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:

View File

@ -56,6 +56,12 @@ class DCIMQuery(graphene.ObjectType):
manufacturer = ObjectField(ManufacturerType)
manufacturer_list = ObjectListField(ManufacturerType)
module_bay = ObjectField(ModuleBayType)
module_bay_list = ObjectListField(ModuleBayType)
module_bay_template = ObjectField(ModuleBayTemplateType)
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
platform = ObjectField(PlatformType)
platform_list = ObjectListField(PlatformType)

View File

@ -27,6 +27,8 @@ __all__ = (
'InventoryItemType',
'LocationType',
'ManufacturerType',
'ModuleBayType',
'ModuleBayTemplateType',
'PlatformType',
'PowerFeedType',
'PowerOutletType',
@ -254,6 +256,22 @@ class ManufacturerType(OrganizationalObjectType):
filterset_class = filtersets.ManufacturerFilterSet
class ModuleBayType(ComponentObjectType):
class Meta:
model = models.ModuleBay
fields = '__all__'
filterset_class = filtersets.ModuleBayFilterSet
class ModuleBayTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ModuleBayTemplate
fields = '__all__'
filterset_class = filtersets.ModuleBayTemplateFilterSet
class PlatformType(OrganizationalObjectType):
class Meta:

View File

@ -0,0 +1,53 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.fields
import utilities.ordering
class Migration(migrations.Migration):
dependencies = [
('extras', '0066_customfield_name_validation'),
('dcim', '0144_site_remove_deprecated_fields'),
]
operations = [
migrations.CreateModel(
name='ModuleBayTemplate',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
('label', models.CharField(blank=True, max_length=64)),
('description', models.CharField(blank=True, max_length=200)),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modulebaytemplates', to='dcim.devicetype')),
],
options={
'ordering': ('device_type', '_name'),
'unique_together': {('device_type', 'name')},
},
),
migrations.CreateModel(
name='ModuleBay',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
('label', models.CharField(blank=True, max_length=64)),
('description', models.CharField(blank=True, max_length=200)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modulebays', to='dcim.device')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('device', '_name'),
'unique_together': {('device', 'name')},
},
),
]

View File

@ -27,6 +27,8 @@ __all__ = (
'InventoryItem',
'Location',
'Manufacturer',
'ModuleBay',
'ModuleBayTemplate',
'Platform',
'PowerFeed',
'PowerOutlet',

View File

@ -9,7 +9,7 @@ from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField
from utilities.ordering import naturalize_interface
from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, ModuleBay, PowerOutlet, PowerPort, RearPort,
)
@ -19,6 +19,7 @@ __all__ = (
'DeviceBayTemplate',
'FrontPortTemplate',
'InterfaceTemplate',
'ModuleBayTemplate',
'PowerOutletTemplate',
'PowerPortTemplate',
'RearPortTemplate',
@ -360,6 +361,23 @@ class RearPortTemplate(ComponentTemplateModel):
)
@extras_features('webhooks')
class ModuleBayTemplate(ComponentTemplateModel):
"""
A template for a ModuleBay to be created for a new parent Device.
"""
class Meta:
ordering = ('device_type', '_name')
unique_together = ('device_type', 'name')
def instantiate(self, device):
return ModuleBay(
device=device,
name=self.name,
label=self.label
)
@extras_features('webhooks')
class DeviceBayTemplate(ComponentTemplateModel):
"""

View File

@ -30,6 +30,7 @@ __all__ = (
'FrontPort',
'Interface',
'InventoryItem',
'ModuleBay',
'PathEndpoint',
'PowerOutlet',
'PowerPort',
@ -229,7 +230,7 @@ class PathEndpoint(models.Model):
#
# Console ports
# Console components
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
@ -260,10 +261,6 @@ class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
#
# Console server ports
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
"""
@ -293,7 +290,7 @@ class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
#
# Power ports
# Power components
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
@ -389,10 +386,6 @@ class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
}
#
# Power outlets
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerOutlet(ComponentModel, LinkTermination, PathEndpoint):
"""
@ -866,9 +859,24 @@ class RearPort(ComponentModel, LinkTermination):
#
# Device bays
# Bays
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ModuleBay(ComponentModel):
"""
An empty space within a Device which can house a child device
"""
clone_fields = ['device']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self):
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceBay(ComponentModel):
"""

View File

@ -786,6 +786,9 @@ class Device(PrimaryModel, ConfigContextModel):
FrontPort.objects.bulk_create(
[x.instantiate(self) for x in self.device_type.frontporttemplates.all()]
)
ModuleBay.objects.bulk_create(
[x.instantiate(self) for x in self.device_type.modulebaytemplates.all()]
)
DeviceBay.objects.bulk_create(
[x.instantiate(self) for x in self.device_type.devicebaytemplates.all()]
)

View File

@ -2,8 +2,8 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform,
PowerOutlet, PowerPort, RearPort, VirtualChassis,
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, ModuleBay,
Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
)
from tenancy.tables import TenantColumn
from utilities.tables import (
@ -25,6 +25,7 @@ __all__ = (
'DeviceImportTable',
'DeviceInterfaceTable',
'DeviceInventoryItemTable',
'DeviceModuleBayTable',
'DevicePowerPortTable',
'DevicePowerOutletTable',
'DeviceRearPortTable',
@ -33,6 +34,7 @@ __all__ = (
'FrontPortTable',
'InterfaceTable',
'InventoryItemTable',
'ModuleBayTable',
'PlatformTable',
'PowerOutletTable',
'PowerPortTable',
@ -716,6 +718,35 @@ class DeviceDeviceBayTable(DeviceBayTable):
)
class ModuleBayTable(DeviceComponentTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_modulebays',
'args': [Accessor('device_id')],
}
)
tags = TagColumn(
url_name='dcim:modulebay_list'
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'device', 'label', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'description')
class DeviceModuleBayTable(ModuleBayTable):
actions = ButtonsColumn(
model=ModuleBay,
buttons=('edit', 'delete')
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'label', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'label', 'description', 'actions')
class InventoryItemTable(DeviceComponentTable):
device = tables.Column(
linkify={

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
Manufacturer, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
)
from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
@ -16,6 +16,7 @@ __all__ = (
'FrontPortTemplateTable',
'InterfaceTemplateTable',
'ManufacturerTable',
'ModuleBayTemplateTable',
'PowerOutletTemplateTable',
'PowerPortTemplateTable',
'RearPortTemplateTable',
@ -207,6 +208,19 @@ class RearPortTemplateTable(ComponentTemplateTable):
empty_text = "None"
class ModuleBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=ModuleBayTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_modulebays'
)
class Meta(ComponentTemplateTable.Meta):
model = ModuleBayTemplate
fields = ('pk', 'name', 'label', 'description', 'actions')
empty_text = "None"
class DeviceBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=DeviceBayTemplate,

View File

@ -778,6 +778,46 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
]
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer,
model='Device Type 1',
slug='device-type-1',
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
)
module_bay_templates = (
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
)
ModuleBayTemplate.objects.bulk_create(module_bay_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 4',
},
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 5',
},
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 6',
},
]
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate
brief_fields = ['display', 'id', 'name', 'url']
@ -1369,6 +1409,45 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
]
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
device = Device.objects.create(device_type=device_type, device_role=devicerole, name='Device 1', site=site)
device_bays = (
ModuleBay(device=device, name='Device Bay 1'),
ModuleBay(device=device, name='Device Bay 2'),
ModuleBay(device=device, name='Device Bay 3'),
)
ModuleBay.objects.bulk_create(device_bays)
cls.create_data = [
{
'device': device.pk,
'name': 'Device Bay 4',
},
{
'device': device.pk,
'name': 'Device Bay 5',
},
{
'device': device.pk,
'name': 'Device Bay 6',
},
]
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay
brief_fields = ['device', 'display', 'id', 'name', 'url']

View File

@ -678,6 +678,10 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
))
DeviceBayTemplate.objects.bulk_create((
DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
@ -762,6 +766,12 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_module_bays(self):
params = {'module_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()
@ -1036,6 +1046,38 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleBayTemplate.objects.all()
filterset = ModuleBayTemplateFilterSet
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_types = (
DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'),
DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'),
DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'),
)
DeviceType.objects.bulk_create(device_types)
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3'),
))
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicetype_id(self):
device_types = DeviceType.objects.all()[:2]
params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceBayTemplate.objects.all()
filterset = DeviceBayTemplateFilterSet
@ -1280,6 +1322,10 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
ModuleBay.objects.bulk_create((
ModuleBay(device=devices[0], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
))
DeviceBay.objects.bulk_create((
DeviceBay(device=devices[0], name='Device Bay 1'),
DeviceBay(device=devices[1], name='Device Bay 2'),
@ -1465,6 +1511,12 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'pass_through_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_module_bays(self):
params = {'module_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_device_bays(self):
params = {'device_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -2508,6 +2560,109 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleBay.objects.all()
filterset = ModuleBayFilterSet
@classmethod
def setUpTestData(cls):
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for group in groups:
group.save()
sites = Site.objects.bulk_create((
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
Site(name='Site X', slug='site-x'),
))
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
)
Device.objects.bulk_create(devices)
module_bays = (
ModuleBay(device=devices[0], name='Module Bay 1', label='A', description='First'),
ModuleBay(device=devices[1], name='Module Bay 2', label='B', description='Second'),
ModuleBay(device=devices[2], name='Module Bay 3', label='C', description='Third'),
)
ModuleBay.objects.bulk_create(module_bays)
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceBay.objects.all()
filterset = DeviceBayFilterSet

View File

@ -308,6 +308,11 @@ class DeviceTestCase(TestCase):
rear_port_position=2
).save()
ModuleBayTemplate(
device_type=self.device_type,
name='Module Bay 1'
).save()
DeviceBayTemplate(
device_type=self.device_type,
name='Device Bay 1'
@ -371,6 +376,11 @@ class DeviceTestCase(TestCase):
rear_port_position=2
)
ModuleBay.objects.get(
device=d,
name='Module Bay 1'
)
DeviceBay.objects.get(
device=d,
name='Device Bay 1'

View File

@ -554,6 +554,19 @@ class DeviceTypeTestCase(
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_devicetype_modulebays(self):
devicetype = DeviceType.objects.first()
module_bays = (
ModuleBayTemplate(device_type=devicetype, name='Module Bay 1'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay 2'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay 3'),
)
ModuleBayTemplate.objects.bulk_create(module_bays)
url = reverse('dcim:devicetype_modulebays', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_devicetype_devicebays(self):
devicetype = DeviceType.objects.first()
@ -638,6 +651,10 @@ front-ports:
- name: Front Port 3
type: 8p8c
rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
- name: Module Bay 2
- name: Module Bay 3
device-bays:
- name: Device Bay 1
- name: Device Bay 2
@ -658,6 +675,7 @@ device-bays:
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
)
@ -710,6 +728,10 @@ device-bays:
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(dt.modulebaytemplates.count(), 3)
db1 = ModuleBayTemplate.objects.first()
self.assertEqual(db1.name, 'Module Bay 1')
self.assertEqual(dt.devicebaytemplates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1')
@ -1011,6 +1033,39 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
}
class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ModuleBayTemplate
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'name': 'Module Bay Template X',
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Module Bay Template [4-6]',
}
cls.bulk_edit_data = {
'description': 'Foo bar',
}
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = DeviceBayTemplate
@ -1307,6 +1362,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_device_modulebays(self):
device = Device.objects.first()
device_bays = (
ModuleBay(device=device, name='Module Bay 1'),
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
)
ModuleBay.objects.bulk_create(device_bays)
url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_device_devicebays(self):
device = Device.objects.first()
@ -1807,6 +1875,47 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
self.assertHttpStatus(response, 200)
class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ModuleBay
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
ModuleBay.objects.bulk_create([
ModuleBay(device=device, name='Module Bay 1'),
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': device.pk,
'name': 'Module Bay X',
'description': 'A device bay',
'tags': [t.pk for t in tags],
}
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Module Bay [4-6]',
'description': 'A module bay',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'description': 'New description',
}
cls.csv_data = (
"device,name",
"Device 1,Module Bay 4",
"Device 1,Module Bay 5",
"Device 1,Module Bay 6",
)
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay

View File

@ -113,6 +113,7 @@ urlpatterns = [
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
@ -183,6 +184,14 @@ urlpatterns = [
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'),
# Device bay templates
path('module-bay-templates/add/', views.ModuleBayTemplateCreateView.as_view(), name='modulebaytemplate_add'),
path('module-bay-templates/edit/', views.ModuleBayTemplateBulkEditView.as_view(), name='modulebaytemplate_bulk_edit'),
path('module-bay-templates/rename/', views.ModuleBayTemplateBulkRenameView.as_view(), name='modulebaytemplate_bulk_rename'),
path('module-bay-templates/delete/', views.ModuleBayTemplateBulkDeleteView.as_view(), name='modulebaytemplate_bulk_delete'),
path('module-bay-templates/<int:pk>/edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'),
path('module-bay-templates/<int:pk>/delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'),
# Device roles
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
@ -222,6 +231,7 @@ urlpatterns = [
path('devices/<int:pk>/interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'),
path('devices/<int:pk>/front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'),
path('devices/<int:pk>/rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'),
path('devices/<int:pk>/module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'),
path('devices/<int:pk>/device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'),
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
@ -343,6 +353,19 @@ urlpatterns = [
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Module bays
path('module-bays/', views.ModuleBayListView.as_view(), name='modulebay_list'),
path('module-bays/add/', views.ModuleBayCreateView.as_view(), name='modulebay_add'),
path('module-bays/import/', views.ModuleBayBulkImportView.as_view(), name='modulebay_import'),
path('module-bays/edit/', views.ModuleBayBulkEditView.as_view(), name='modulebay_bulk_edit'),
path('module-bays/rename/', views.ModuleBayBulkRenameView.as_view(), name='modulebay_bulk_rename'),
path('module-bays/delete/', views.ModuleBayBulkDeleteView.as_view(), name='modulebay_bulk_delete'),
path('module-bays/<int:pk>/', views.ModuleBayView.as_view(), name='modulebay'),
path('module-bays/<int:pk>/edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'),
path('module-bays/<int:pk>/delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'),
path('module-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}),
path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'),
# Device bays
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),

View File

@ -30,9 +30,9 @@ from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import (
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
SiteGroup, VirtualChassis,
InventoryItem, Manufacturer, ModuleBay, ModuleBayTemplate, PathEndpoint, Platform, PowerFeed, PowerOutlet,
PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort,
RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
)
@ -836,6 +836,12 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView):
filterset = filtersets.RearPortTemplateFilterSet
class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
child_model = ModuleBayTemplate
table = tables.ModuleBayTemplateTable
filterset = filtersets.ModuleBayTemplateFilterSet
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
child_model = DeviceBayTemplate
table = tables.DeviceBayTemplateTable
@ -861,6 +867,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
]
queryset = DeviceType.objects.all()
@ -873,6 +880,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
('module-bays', forms.ModuleBayTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm),
))
@ -1132,6 +1140,40 @@ class RearPortTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.RearPortTemplateTable
#
# Module bay templates
#
class ModuleBayTemplateCreateView(generic.ComponentCreateView):
queryset = ModuleBayTemplate.objects.all()
form = forms.ModuleBayTemplateCreateForm
model_form = forms.ModuleBayTemplateForm
class ModuleBayTemplateEditView(generic.ObjectEditView):
queryset = ModuleBayTemplate.objects.all()
model_form = forms.ModuleBayTemplateForm
class ModuleBayTemplateDeleteView(generic.ObjectDeleteView):
queryset = ModuleBayTemplate.objects.all()
class ModuleBayTemplateBulkEditView(generic.BulkEditView):
queryset = ModuleBayTemplate.objects.all()
table = tables.ModuleBayTemplateTable
form = forms.ModuleBayTemplateBulkEditForm
class ModuleBayTemplateBulkRenameView(generic.BulkRenameView):
queryset = ModuleBayTemplate.objects.all()
class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleBayTemplate.objects.all()
table = tables.ModuleBayTemplateTable
#
# Device bay templates
#
@ -1388,6 +1430,13 @@ class DeviceRearPortsView(DeviceComponentsView):
template_name = 'dcim/device/rearports.html'
class DeviceModuleBaysView(DeviceComponentsView):
child_model = ModuleBay
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html'
class DeviceDeviceBaysView(DeviceComponentsView):
child_model = DeviceBay
table = tables.DeviceDeviceBayTable
@ -1978,6 +2027,61 @@ class RearPortBulkDeleteView(generic.BulkDeleteView):
table = tables.RearPortTable
#
# Module bays
#
class ModuleBayListView(generic.ObjectListView):
queryset = ModuleBay.objects.all()
filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable
action_buttons = ('import', 'export')
class ModuleBayView(generic.ObjectView):
queryset = ModuleBay.objects.all()
class ModuleBayCreateView(generic.ComponentCreateView):
queryset = ModuleBay.objects.all()
form = forms.ModuleBayCreateForm
model_form = forms.ModuleBayForm
class ModuleBayEditView(generic.ObjectEditView):
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayForm
template_name = 'dcim/device_component_edit.html'
class ModuleBayDeleteView(generic.ObjectDeleteView):
queryset = ModuleBay.objects.all()
class ModuleBayBulkImportView(generic.BulkImportView):
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayCSVForm
table = tables.ModuleBayTable
class ModuleBayBulkEditView(generic.BulkEditView):
queryset = ModuleBay.objects.all()
filterset = filtersets.ModuleBayFilterSet
table = tables.ModuleBayTable
form = forms.ModuleBayBulkEditForm
class ModuleBayBulkRenameView(generic.BulkRenameView):
queryset = ModuleBay.objects.all()
class ModuleBayBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleBay.objects.all()
filterset = filtersets.ModuleBayFilterSet
table = tables.ModuleBayTable
#
# Device bays
#
@ -2234,6 +2338,17 @@ class DeviceBulkAddRearPortView(generic.BulkComponentCreateView):
default_return_url = 'dcim:device_list'
class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'
form = forms.ModuleBayBulkCreateForm
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayForm
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'

View File

@ -161,6 +161,7 @@ DEVICES_MENU = Menu(
get_model_item('dcim', 'consoleserverport', 'Console Server Ports', actions=['import']),
get_model_item('dcim', 'powerport', 'Power Ports', actions=['import']),
get_model_item('dcim', 'poweroutlet', 'Power Outlets', actions=['import']),
get_model_item('dcim', 'modulebay', 'Module Bays', actions=['import']),
get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
),

View File

@ -69,6 +69,13 @@
</a>
</li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li>
<a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">
Module Bays
</a>
</li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li>
<a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">
@ -151,6 +158,14 @@
{% endif %}
{% endwith %}
{% with modulebay_count=object.modulebays.count %}
{% if modulebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'module-bays' %} active{% endif %}" href="{% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with devicebay_count=object.devicebays.count %}
{% if devicebay_count %}
<li role="presentation" class="nav-item">

View File

@ -0,0 +1,43 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% block content %}
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_modulebay %}
<button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
<button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if perms.dcim.delete_modulebay %}
<button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-outline-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
</button>
{% endif %}
</div>
{% if perms.dcim.add_modulebay %}
<div class="bulk-button-group">
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
</a>
</div>
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}

View File

@ -56,6 +56,13 @@
</button>
</li>
{% endif %}
{% if perms.dcim.add_modulebay %}
<li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Module Bays
</button>
</li>
{% endif %}
{% if perms.dcim.add_inventoryitem %}
<li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">

View File

@ -38,6 +38,9 @@
{% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
{% endif %}
{% if perms.dcim.add_modulebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays">Module Bays</a></li>
{% endif %}
{% if perms.dcim.add_devicebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
{% endif %}
@ -109,6 +112,14 @@
{% endif %}
{% endwith %}
{% with modulebay_count=object.modulebaytemplates.count %}
{% if modulebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'module-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with devicebay_count=object.devicebaytemplates.count %}
{% if devicebay_count %}
<li role="presentation" class="nav-item">

View File

@ -0,0 +1,69 @@
{% extends 'dcim/device_component.html' %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Module Bay</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Device</th>
<td>
<a href="{{ object.device.get_absolute_url }}">{{ object.device }}</a>
</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Label</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Installed Module</h5>
<div class="card-body">
{% if object.module %}
{% with module=object.module %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Module</th>
<td>
<a href="{{ module.get_absolute_url }}">{{ module }}</a>
</td>
</tr>
<tr>
<th scope="row">Module Type</th>
<td>{{ module.module_type }}</td>
</tr>
</table>
{% endwith %}
{% else %}
<div class="text-muted">None</div>
{% endif %}
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}