mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 11:42:52 -06:00
Add Module model
This commit is contained in:
parent
5bd223a468
commit
7777922bef
@ -22,6 +22,7 @@ __all__ = [
|
|||||||
'NestedManufacturerSerializer',
|
'NestedManufacturerSerializer',
|
||||||
'NestedModuleBaySerializer',
|
'NestedModuleBaySerializer',
|
||||||
'NestedModuleBayTemplateSerializer',
|
'NestedModuleBayTemplateSerializer',
|
||||||
|
'NestedModuleSerializer',
|
||||||
'NestedModuleTypeSerializer',
|
'NestedModuleTypeSerializer',
|
||||||
'NestedPlatformSerializer',
|
'NestedPlatformSerializer',
|
||||||
'NestedPowerFeedSerializer',
|
'NestedPowerFeedSerializer',
|
||||||
@ -260,6 +261,18 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedModuleSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
# TODO: Solve circular dependency
|
||||||
|
# module_bay = NestedModuleBaySerializer(read_only=True)
|
||||||
|
module_type = NestedModuleTypeSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Module
|
||||||
|
fields = ['id', 'url', 'display', 'device', 'module_bay', 'module_type']
|
||||||
|
|
||||||
|
|
||||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
@ -325,11 +338,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||||
# module = NestedModuleSerializer(read_only=True)
|
module = NestedModuleSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.DeviceBay
|
model = models.ModuleBay
|
||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'module', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||||
|
@ -517,6 +517,20 @@ class DeviceSerializer(PrimaryModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
|
device = NestedDeviceSerializer()
|
||||||
|
module_bay = NestedModuleBaySerializer()
|
||||||
|
module_type = NestedModuleTypeSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Module
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
||||||
|
'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
config_context = serializers.SerializerMethodField()
|
config_context = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
@ -32,10 +32,11 @@ router.register('rear-port-templates', views.RearPortTemplateViewSet)
|
|||||||
router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
|
router.register('module-bay-templates', views.ModuleBayTemplateViewSet)
|
||||||
router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
|
router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||||
|
|
||||||
# Devices
|
# Device/modules
|
||||||
router.register('device-roles', views.DeviceRoleViewSet)
|
router.register('device-roles', views.DeviceRoleViewSet)
|
||||||
router.register('platforms', views.PlatformViewSet)
|
router.register('platforms', views.PlatformViewSet)
|
||||||
router.register('devices', views.DeviceViewSet)
|
router.register('devices', views.DeviceViewSet)
|
||||||
|
router.register('modules', views.ModuleViewSet)
|
||||||
|
|
||||||
# Device components
|
# Device components
|
||||||
router.register('console-ports', views.ConsolePortViewSet)
|
router.register('console-ports', views.ConsolePortViewSet)
|
||||||
|
@ -377,7 +377,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices/modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||||
@ -526,6 +526,14 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
|||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = Module.objects.prefetch_related(
|
||||||
|
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
||||||
|
)
|
||||||
|
serializer_class = serializers.ModuleSerializer
|
||||||
|
filterset_class = filtersets.ModuleFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
@ -43,6 +43,7 @@ __all__ = (
|
|||||||
'ManufacturerFilterSet',
|
'ManufacturerFilterSet',
|
||||||
'ModuleBayFilterSet',
|
'ModuleBayFilterSet',
|
||||||
'ModuleBayTemplateFilterSet',
|
'ModuleBayTemplateFilterSet',
|
||||||
|
'ModuleFilterSet',
|
||||||
'ModuleTypeFilterSet',
|
'ModuleTypeFilterSet',
|
||||||
'PathEndpointFilterSet',
|
'PathEndpointFilterSet',
|
||||||
'PlatformFilterSet',
|
'PlatformFilterSet',
|
||||||
@ -924,6 +925,42 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
|
|||||||
return queryset.exclude(devicebays__isnull=value)
|
return queryset.exclude(devicebays__isnull=value)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleFilterSet(PrimaryModelFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='module_type__manufacturer',
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
label='Manufacturer (ID)',
|
||||||
|
)
|
||||||
|
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='module_type__manufacturer__slug',
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Manufacturer (slug)',
|
||||||
|
)
|
||||||
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Module
|
||||||
|
fields = ['id', 'serial', 'asset_tag']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(serial__icontains=value.strip()) |
|
||||||
|
Q(asset_tag__icontains=value.strip()) |
|
||||||
|
Q(comments__icontains=value)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -32,6 +32,7 @@ __all__ = (
|
|||||||
'InventoryItemBulkEditForm',
|
'InventoryItemBulkEditForm',
|
||||||
'LocationBulkEditForm',
|
'LocationBulkEditForm',
|
||||||
'ManufacturerBulkEditForm',
|
'ManufacturerBulkEditForm',
|
||||||
|
'ModuleBulkEditForm',
|
||||||
'ModuleBayBulkEditForm',
|
'ModuleBayBulkEditForm',
|
||||||
'ModuleBayTemplateBulkEditForm',
|
'ModuleBayTemplateBulkEditForm',
|
||||||
'ModuleTypeBulkEditForm',
|
'ModuleTypeBulkEditForm',
|
||||||
@ -473,6 +474,32 @@ class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Module.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
manufacturer = DynamicModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
module_type = DynamicModelChoiceField(
|
||||||
|
queryset=ModuleType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'manufacturer_id': '$manufacturer'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
serial = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False,
|
||||||
|
label='Serial Number'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['serial']
|
||||||
|
|
||||||
|
|
||||||
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Cable.objects.all(),
|
queryset=Cable.objects.all(),
|
||||||
|
@ -26,6 +26,7 @@ __all__ = (
|
|||||||
'InventoryItemCSVForm',
|
'InventoryItemCSVForm',
|
||||||
'LocationCSVForm',
|
'LocationCSVForm',
|
||||||
'ManufacturerCSVForm',
|
'ManufacturerCSVForm',
|
||||||
|
'ModuleCSVForm',
|
||||||
'ModuleBayCSVForm',
|
'ModuleBayCSVForm',
|
||||||
'PlatformCSVForm',
|
'PlatformCSVForm',
|
||||||
'PowerFeedCSVForm',
|
'PowerFeedCSVForm',
|
||||||
@ -400,6 +401,35 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
|||||||
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleCSVForm(CustomFieldModelCSVForm):
|
||||||
|
device = CSVModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name'
|
||||||
|
)
|
||||||
|
module_bay = CSVModelChoiceField(
|
||||||
|
queryset=ModuleBay.objects.all(),
|
||||||
|
to_field_name='name'
|
||||||
|
)
|
||||||
|
module_type = CSVModelChoiceField(
|
||||||
|
queryset=ModuleType.objects.all(),
|
||||||
|
to_field_name='model'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Module
|
||||||
|
fields = (
|
||||||
|
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Limit module_bay queryset by assigned device
|
||||||
|
params = {f"device__{self.fields['device'].to_field_name}": data.get('device')}
|
||||||
|
self.fields['module_bay'].queryset = self.fields['module_bay'].queryset.filter(**params)
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||||
parent = CSVModelChoiceField(
|
parent = CSVModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
|
@ -29,6 +29,8 @@ __all__ = (
|
|||||||
'InventoryItemFilterForm',
|
'InventoryItemFilterForm',
|
||||||
'LocationFilterForm',
|
'LocationFilterForm',
|
||||||
'ManufacturerFilterForm',
|
'ManufacturerFilterForm',
|
||||||
|
'ModuleFilterForm',
|
||||||
|
'ModuleFilterForm',
|
||||||
'ModuleBayFilterForm',
|
'ModuleBayFilterForm',
|
||||||
'ModuleTypeFilterForm',
|
'ModuleTypeFilterForm',
|
||||||
'PlatformFilterForm',
|
'PlatformFilterForm',
|
||||||
@ -645,6 +647,37 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||||
|
model = Module
|
||||||
|
field_groups = [
|
||||||
|
['q', 'tag'],
|
||||||
|
['manufacturer_id', 'module_type_id'],
|
||||||
|
['serial', 'asset_tag'],
|
||||||
|
]
|
||||||
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Manufacturer'),
|
||||||
|
fetch_trigger='open'
|
||||||
|
)
|
||||||
|
module_type_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ModuleType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'manufacturer_id': '$manufacturer_id'
|
||||||
|
},
|
||||||
|
label=_('Type'),
|
||||||
|
fetch_trigger='open'
|
||||||
|
)
|
||||||
|
serial = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
asset_tag = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
field_groups = [
|
field_groups = [
|
||||||
|
@ -39,6 +39,7 @@ __all__ = (
|
|||||||
'InventoryItemForm',
|
'InventoryItemForm',
|
||||||
'LocationForm',
|
'LocationForm',
|
||||||
'ManufacturerForm',
|
'ManufacturerForm',
|
||||||
|
'ModuleForm',
|
||||||
'ModuleBayForm',
|
'ModuleBayForm',
|
||||||
'ModuleBayTemplateForm',
|
'ModuleBayTemplateForm',
|
||||||
'ModuleTypeForm',
|
'ModuleTypeForm',
|
||||||
@ -651,6 +652,46 @@ class DeviceForm(TenancyForm, CustomFieldModelForm):
|
|||||||
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
self.fields['position'].widget.choices = [(position, f'U{position}')]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleForm(CustomFieldModelForm):
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'modulebays': '$module_bay'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
module_bay = DynamicModelChoiceField(
|
||||||
|
queryset=ModuleBay.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'device_id': '$device'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
manufacturer = DynamicModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'device_types': '$device_type'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
module_type = DynamicModelChoiceField(
|
||||||
|
queryset=ModuleType.objects.all(),
|
||||||
|
query_params={
|
||||||
|
'manufacturer_id': '$manufacturer'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comments = CommentField()
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Module
|
||||||
|
fields = [
|
||||||
|
'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'tags', 'comments',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CableForm(TenancyForm, CustomFieldModelForm):
|
class CableForm(TenancyForm, CustomFieldModelForm):
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
|
@ -56,6 +56,9 @@ class DCIMQuery(graphene.ObjectType):
|
|||||||
manufacturer = ObjectField(ManufacturerType)
|
manufacturer = ObjectField(ManufacturerType)
|
||||||
manufacturer_list = ObjectListField(ManufacturerType)
|
manufacturer_list = ObjectListField(ManufacturerType)
|
||||||
|
|
||||||
|
module = ObjectField(ModuleType)
|
||||||
|
module_list = ObjectListField(ModuleType)
|
||||||
|
|
||||||
module_bay = ObjectField(ModuleBayType)
|
module_bay = ObjectField(ModuleBayType)
|
||||||
module_bay_list = ObjectListField(ModuleBayType)
|
module_bay_list = ObjectListField(ModuleBayType)
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'InventoryItemType',
|
'InventoryItemType',
|
||||||
'LocationType',
|
'LocationType',
|
||||||
'ManufacturerType',
|
'ManufacturerType',
|
||||||
|
'ModuleType',
|
||||||
'ModuleBayType',
|
'ModuleBayType',
|
||||||
'ModuleBayTemplateType',
|
'ModuleBayTemplateType',
|
||||||
'ModuleTypeType',
|
'ModuleTypeType',
|
||||||
@ -257,6 +258,14 @@ class ManufacturerType(OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.ManufacturerFilterSet
|
filterset_class = filtersets.ManufacturerFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleType(ComponentObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Module
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.ModuleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayType(ComponentObjectType):
|
class ModuleBayType(ComponentObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -95,36 +95,110 @@ class Migration(migrations.Migration):
|
|||||||
'unique_together': {('manufacturer', 'model')},
|
'unique_together': {('manufacturer', 'model')},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Module',
|
||||||
|
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)),
|
||||||
|
('local_context_data', models.JSONField(blank=True, null=True)),
|
||||||
|
('serial', models.CharField(blank=True, max_length=50)),
|
||||||
|
('asset_tag', models.CharField(blank=True, max_length=50, null=True, unique=True)),
|
||||||
|
('comments', models.TextField(blank=True)),
|
||||||
|
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='dcim.device')),
|
||||||
|
('module_bay', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='installed_module', to='dcim.modulebay')),
|
||||||
|
('module_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.moduletype')),
|
||||||
|
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('module_bay',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleports', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='consoleporttemplate',
|
model_name='consoleporttemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverports', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='consoleserverporttemplate',
|
model_name='consoleserverporttemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='frontporttemplate',
|
model_name='frontporttemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='interfacetemplate',
|
model_name='interfacetemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlets', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='poweroutlettemplate',
|
model_name='poweroutlettemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerports', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='powerporttemplate',
|
model_name='powerporttemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.moduletype'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.moduletype'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.module'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='rearporttemplate',
|
model_name='rearporttemplate',
|
||||||
name='module_type',
|
name='module_type',
|
||||||
@ -140,7 +214,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='frontporttemplate',
|
name='frontporttemplate',
|
||||||
unique_together={('device_type', 'name'), ('module_type', 'name'), ('rear_port', 'rear_port_position')},
|
unique_together={('device_type', 'name'), ('rear_port', 'rear_port_position'), ('module_type', 'name')},
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='interfacetemplate',
|
name='interfacetemplate',
|
||||||
@ -175,23 +249,4 @@ class Migration(migrations.Migration):
|
|||||||
'unique_together': {('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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'InventoryItem',
|
'InventoryItem',
|
||||||
'Location',
|
'Location',
|
||||||
'Manufacturer',
|
'Manufacturer',
|
||||||
|
'Module',
|
||||||
'ModuleBay',
|
'ModuleBay',
|
||||||
'ModuleBayTemplate',
|
'ModuleBayTemplate',
|
||||||
'ModuleType',
|
'ModuleType',
|
||||||
|
@ -142,12 +142,12 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
|
|||||||
('module_type', 'name'),
|
('module_type', 'name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return ConsolePort(
|
return ConsolePort(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type
|
type=self.type,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -169,12 +169,12 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
|
|||||||
('module_type', 'name'),
|
('module_type', 'name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return ConsoleServerPort(
|
return ConsoleServerPort(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type
|
type=self.type,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -208,14 +208,14 @@ class PowerPortTemplate(ModularComponentTemplateModel):
|
|||||||
('module_type', 'name'),
|
('module_type', 'name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return PowerPort(
|
return PowerPort(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type,
|
type=self.type,
|
||||||
maximum_draw=self.maximum_draw,
|
maximum_draw=self.maximum_draw,
|
||||||
allocated_draw=self.allocated_draw
|
allocated_draw=self.allocated_draw,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -273,18 +273,18 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
|
|||||||
f"Parent power port ({self.power_port}) must belong to the same module type"
|
f"Parent power port ({self.power_port}) must belong to the same module type"
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
if self.power_port:
|
if self.power_port:
|
||||||
power_port = PowerPort.objects.get(device=device, name=self.power_port.name)
|
power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
|
||||||
else:
|
else:
|
||||||
power_port = None
|
power_port = None
|
||||||
return PowerOutlet(
|
return PowerOutlet(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type,
|
type=self.type,
|
||||||
power_port=power_port,
|
power_port=power_port,
|
||||||
feed_leg=self.feed_leg
|
feed_leg=self.feed_leg,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -316,13 +316,13 @@ class InterfaceTemplate(ModularComponentTemplateModel):
|
|||||||
('module_type', 'name'),
|
('module_type', 'name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return Interface(
|
return Interface(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type,
|
type=self.type,
|
||||||
mgmt_only=self.mgmt_only
|
mgmt_only=self.mgmt_only,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -381,19 +381,19 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
|||||||
except RearPortTemplate.DoesNotExist:
|
except RearPortTemplate.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
if self.rear_port:
|
if self.rear_port:
|
||||||
rear_port = RearPort.objects.get(device=device, name=self.rear_port.name)
|
rear_port = RearPort.objects.get(name=self.rear_port.name, **kwargs)
|
||||||
else:
|
else:
|
||||||
rear_port = None
|
rear_port = None
|
||||||
return FrontPort(
|
return FrontPort(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
rear_port=rear_port,
|
rear_port=rear_port,
|
||||||
rear_port_position=self.rear_port_position
|
rear_port_position=self.rear_port_position,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -424,14 +424,14 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
('module_type', 'name'),
|
('module_type', 'name'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return RearPort(
|
return RearPort(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
type=self.type,
|
type=self.type,
|
||||||
color=self.color,
|
color=self.color,
|
||||||
positions=self.positions
|
positions=self.positions,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,6 +87,19 @@ class ComponentModel(PrimaryModel):
|
|||||||
return self.device
|
return self.device
|
||||||
|
|
||||||
|
|
||||||
|
class ModularComponentModel(ComponentModel):
|
||||||
|
module = models.ForeignKey(
|
||||||
|
to='dcim.Module',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='%(class)ss',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class LinkTermination(models.Model):
|
class LinkTermination(models.Model):
|
||||||
"""
|
"""
|
||||||
An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples
|
An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples
|
||||||
@ -234,7 +247,7 @@ class PathEndpoint(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
|
class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -262,7 +275,7 @@ class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
|
class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
@ -294,7 +307,7 @@ class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
|
class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -387,7 +400,7 @@ class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class PowerOutlet(ComponentModel, LinkTermination, PathEndpoint):
|
class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
@ -502,7 +515,7 @@ class BaseInterface(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
"""
|
"""
|
||||||
@ -765,7 +778,7 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class FrontPort(ComponentModel, LinkTermination):
|
class FrontPort(ModularComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
"""
|
"""
|
||||||
@ -819,7 +832,7 @@ class FrontPort(ComponentModel, LinkTermination):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class RearPort(ComponentModel, LinkTermination):
|
class RearPort(ModularComponentModel, LinkTermination):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
"""
|
"""
|
||||||
|
@ -26,6 +26,7 @@ __all__ = (
|
|||||||
'DeviceRole',
|
'DeviceRole',
|
||||||
'DeviceType',
|
'DeviceType',
|
||||||
'Manufacturer',
|
'Manufacturer',
|
||||||
|
'Module',
|
||||||
'ModuleType',
|
'ModuleType',
|
||||||
'Platform',
|
'Platform',
|
||||||
'VirtualChassis',
|
'VirtualChassis',
|
||||||
@ -906,31 +907,31 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||||
if is_new:
|
if is_new:
|
||||||
ConsolePort.objects.bulk_create(
|
ConsolePort.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.consoleporttemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.consoleporttemplates.all()]
|
||||||
)
|
)
|
||||||
ConsoleServerPort.objects.bulk_create(
|
ConsoleServerPort.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.consoleserverporttemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.consoleserverporttemplates.all()]
|
||||||
)
|
)
|
||||||
PowerPort.objects.bulk_create(
|
PowerPort.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.powerporttemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.powerporttemplates.all()]
|
||||||
)
|
)
|
||||||
PowerOutlet.objects.bulk_create(
|
PowerOutlet.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.poweroutlettemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.poweroutlettemplates.all()]
|
||||||
)
|
)
|
||||||
Interface.objects.bulk_create(
|
Interface.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.interfacetemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.interfacetemplates.all()]
|
||||||
)
|
)
|
||||||
RearPort.objects.bulk_create(
|
RearPort.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.rearporttemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.rearporttemplates.all()]
|
||||||
)
|
)
|
||||||
FrontPort.objects.bulk_create(
|
FrontPort.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.frontporttemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.frontporttemplates.all()]
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(
|
ModuleBay.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.modulebaytemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.modulebaytemplates.all()]
|
||||||
)
|
)
|
||||||
DeviceBay.objects.bulk_create(
|
DeviceBay.objects.bulk_create(
|
||||||
[x.instantiate(self) for x in self.device_type.devicebaytemplates.all()]
|
[x.instantiate(device=self) for x in self.device_type.devicebaytemplates.all()]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update Site and Rack assignment for any child Devices
|
# Update Site and Rack assignment for any child Devices
|
||||||
@ -1008,6 +1009,85 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
return DeviceStatusChoices.colors.get(self.status, 'secondary')
|
return DeviceStatusChoices.colors.get(self.status, 'secondary')
|
||||||
|
|
||||||
|
|
||||||
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
|
class Module(PrimaryModel, ConfigContextModel):
|
||||||
|
"""
|
||||||
|
A Module represents a field-installable component within a Device which may itself hold multiple device components
|
||||||
|
(for example, a line card within a chassis switch). Modules are instantiated from ModuleTypes.
|
||||||
|
"""
|
||||||
|
device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='modules'
|
||||||
|
)
|
||||||
|
module_bay = models.OneToOneField(
|
||||||
|
to='dcim.ModuleBay',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='installed_module'
|
||||||
|
)
|
||||||
|
module_type = models.ForeignKey(
|
||||||
|
to='dcim.ModuleType',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='instances'
|
||||||
|
)
|
||||||
|
serial = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Serial number'
|
||||||
|
)
|
||||||
|
asset_tag = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
verbose_name='Asset tag',
|
||||||
|
help_text='A unique tag used to identify this device'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
clone_fields = ('device', 'module_type')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('module_bay',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.module_type)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:module', args=[self.pk])
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
is_new = not bool(self.pk)
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# If this is a new Module, instantiate all its related components per the ModuleType definition
|
||||||
|
if is_new:
|
||||||
|
ConsolePort.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleporttemplates.all()]
|
||||||
|
)
|
||||||
|
ConsoleServerPort.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.consoleserverporttemplates.all()]
|
||||||
|
)
|
||||||
|
PowerPort.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.powerporttemplates.all()]
|
||||||
|
)
|
||||||
|
PowerOutlet.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.poweroutlettemplates.all()]
|
||||||
|
)
|
||||||
|
Interface.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.interfacetemplates.all()]
|
||||||
|
)
|
||||||
|
RearPort.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.rearporttemplates.all()]
|
||||||
|
)
|
||||||
|
FrontPort.objects.bulk_create(
|
||||||
|
[x.instantiate(device=self.device, module=self) for x in self.module_type.frontporttemplates.all()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
@ -6,7 +6,7 @@ from dcim.models import ConsolePort, Interface, PowerPort
|
|||||||
from .cables import *
|
from .cables import *
|
||||||
from .devices import *
|
from .devices import *
|
||||||
from .devicetypes import *
|
from .devicetypes import *
|
||||||
from .moduletypes import *
|
from .modules import *
|
||||||
from .power import *
|
from .power import *
|
||||||
from .racks import *
|
from .racks import *
|
||||||
from .sites import *
|
from .sites import *
|
||||||
|
@ -725,26 +725,31 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
installed_module = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name='Installed module'
|
||||||
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='dcim:modulebay_list'
|
url_name='dcim:modulebay_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = ('pk', 'id', 'name', 'device', 'label', 'description', 'tags')
|
fields = ('pk', 'id', 'name', 'device', 'label', 'installed_module', 'description', 'tags')
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
|
||||||
|
|
||||||
|
|
||||||
class DeviceModuleBayTable(ModuleBayTable):
|
class DeviceModuleBayTable(ModuleBayTable):
|
||||||
actions = ButtonsColumn(
|
actions = ButtonsColumn(
|
||||||
model=ModuleBay,
|
model=DeviceBay,
|
||||||
buttons=('edit', 'delete')
|
buttons=('edit', 'delete'),
|
||||||
|
prepend_template=MODULEBAY_BUTTONS
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(DeviceComponentTable.Meta):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = ('pk', 'id', 'name', 'label', 'description', 'tags', 'actions')
|
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
|
||||||
default_columns = ('pk', 'name', 'label', 'description', 'actions')
|
default_columns = ('pk', 'name', 'label', 'description', 'installed_module', 'actions')
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTable(DeviceComponentTable):
|
class InventoryItemTable(DeviceComponentTable):
|
||||||
|
61
netbox/dcim/tables/modules.py
Normal file
61
netbox/dcim/tables/modules.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from dcim.models import Module, ModuleType
|
||||||
|
from utilities.tables import BaseTable, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ModuleTable',
|
||||||
|
'ModuleTypeTable',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTypeTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
model = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name='Module Type'
|
||||||
|
)
|
||||||
|
instance_count = LinkedCountColumn(
|
||||||
|
viewname='dcim:module_list',
|
||||||
|
url_params={'module_type_id': 'pk'},
|
||||||
|
verbose_name='Instances'
|
||||||
|
)
|
||||||
|
comments = MarkdownColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:moduletype_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = ModuleType
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'model', 'manufacturer', 'part_number',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
device = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
module_bay = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
module_type = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
comments = MarkdownColumn()
|
||||||
|
tags = TagColumn(
|
||||||
|
url_name='dcim:module_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Module
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag',
|
||||||
|
)
|
@ -1,34 +0,0 @@
|
|||||||
import django_tables2 as tables
|
|
||||||
|
|
||||||
from dcim.models import ModuleType
|
|
||||||
from utilities.tables import BaseTable, MarkdownColumn, TagColumn, ToggleColumn
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'ModuleTypeTable',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeTable(BaseTable):
|
|
||||||
pk = ToggleColumn()
|
|
||||||
model = tables.Column(
|
|
||||||
linkify=True,
|
|
||||||
verbose_name='Device Type'
|
|
||||||
)
|
|
||||||
# instance_count = LinkedCountColumn(
|
|
||||||
# viewname='dcim:module_list',
|
|
||||||
# url_params={'module_type_id': 'pk'},
|
|
||||||
# verbose_name='Instances'
|
|
||||||
# )
|
|
||||||
comments = MarkdownColumn()
|
|
||||||
tags = TagColumn(
|
|
||||||
url_name='dcim:moduletype_list'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
|
||||||
model = ModuleType
|
|
||||||
fields = (
|
|
||||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
|
||||||
)
|
|
||||||
default_columns = (
|
|
||||||
'pk', 'model', 'manufacturer', 'part_number',
|
|
||||||
)
|
|
@ -321,3 +321,17 @@ DEVICEBAY_BUTTONS = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MODULEBAY_BUTTONS = """
|
||||||
|
{% if perms.dcim.add_module %}
|
||||||
|
{% if record.installed_module %}
|
||||||
|
<a href="{% url 'dcim:module_delete' pk=record.installed_module.pk %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||||
|
<i class="mdi mdi-minus-thick" aria-hidden="true" title="Remove module"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
|
||||||
|
<i class="mdi mdi-plus-thick" aria-hidden="true" title="Install module"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
@ -7,7 +7,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, RIR, VLAN
|
from ipam.models import ASN, RIR, VLAN
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
|
||||||
@ -1105,6 +1105,67 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
model = Module
|
||||||
|
brief_fields = ['device', 'display', 'id', 'module_bay', 'module_type', 'url']
|
||||||
|
bulk_update_data = {
|
||||||
|
'serial': '1234ABCD',
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
|
||||||
|
device = create_test_device('Test Device 1')
|
||||||
|
|
||||||
|
module_types = (
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 1'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 2'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 3'),
|
||||||
|
)
|
||||||
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
|
module_bays = (
|
||||||
|
ModuleBay(device=device, name='Module Bay 1'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 2'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 3'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 4'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 5'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 6'),
|
||||||
|
)
|
||||||
|
ModuleBay.objects.bulk_create(module_bays)
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
Module(device=device, module_bay=module_bays[0], module_type=module_types[0]),
|
||||||
|
Module(device=device, module_bay=module_bays[1], module_type=module_types[1]),
|
||||||
|
Module(device=device, module_bay=module_bays[2], module_type=module_types[2]),
|
||||||
|
)
|
||||||
|
Module.objects.bulk_create(modules)
|
||||||
|
|
||||||
|
cls.create_data = [
|
||||||
|
{
|
||||||
|
'device': device.pk,
|
||||||
|
'module_bay': module_bays[3].pk,
|
||||||
|
'module_type': module_types[0].pk,
|
||||||
|
'serial': 'ABC123',
|
||||||
|
'asset_tag': 'Foo1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device': device.pk,
|
||||||
|
'module_bay': module_bays[4].pk,
|
||||||
|
'module_type': module_types[1].pk,
|
||||||
|
'serial': 'DEF456',
|
||||||
|
'asset_tag': 'Foo2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device': device.pk,
|
||||||
|
'module_bay': module_bays[5].pk,
|
||||||
|
'module_type': module_types[2].pk,
|
||||||
|
'serial': 'GHI789',
|
||||||
|
'asset_tag': 'Foo3',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
|
||||||
|
@ -7,7 +7,7 @@ from dcim.models import *
|
|||||||
from ipam.models import ASN, IPAddress, RIR
|
from ipam.models import ASN, IPAddress, RIR
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
@ -1648,6 +1648,79 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
queryset = Module.objects.all()
|
||||||
|
filterset = ModuleFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturers = (
|
||||||
|
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||||
|
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||||
|
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
|
||||||
|
)
|
||||||
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('Test Device 1'),
|
||||||
|
create_test_device('Test Device 2'),
|
||||||
|
create_test_device('Test Device 3'),
|
||||||
|
)
|
||||||
|
|
||||||
|
module_types = (
|
||||||
|
ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
|
||||||
|
ModuleType(manufacturer=manufacturers[1], model='Module Type 2'),
|
||||||
|
ModuleType(manufacturer=manufacturers[2], model='Module Type 3'),
|
||||||
|
)
|
||||||
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
|
module_bays = (
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 1'),
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 2'),
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 3'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 1'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 3'),
|
||||||
|
ModuleBay(device=devices[2], name='Module Bay 1'),
|
||||||
|
ModuleBay(device=devices[2], name='Module Bay 2'),
|
||||||
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
|
)
|
||||||
|
ModuleBay.objects.bulk_create(module_bays)
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0], serial='A', asset_tag='A'),
|
||||||
|
Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1], serial='B', asset_tag='B'),
|
||||||
|
Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2], serial='C', asset_tag='C'),
|
||||||
|
Module(device=devices[1], module_bay=module_bays[3], module_type=module_types[0], serial='D', asset_tag='D'),
|
||||||
|
Module(device=devices[1], module_bay=module_bays[4], module_type=module_types[1], serial='E', asset_tag='E'),
|
||||||
|
Module(device=devices[1], module_bay=module_bays[5], module_type=module_types[2], serial='F', asset_tag='F'),
|
||||||
|
Module(device=devices[2], module_bay=module_bays[6], module_type=module_types[0], serial='G', asset_tag='G'),
|
||||||
|
Module(device=devices[2], module_bay=module_bays[7], module_type=module_types[1], serial='H', asset_tag='H'),
|
||||||
|
Module(device=devices[2], module_bay=module_bays[8], module_type=module_types[2], serial='I', asset_tag='I'),
|
||||||
|
)
|
||||||
|
Module.objects.bulk_create(modules)
|
||||||
|
|
||||||
|
def test_manufacturer(self):
|
||||||
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
|
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
|
def test_device(self):
|
||||||
|
device_types = Device.objects.all()[:2]
|
||||||
|
params = {'device_id': [device_types[0].pk, device_types[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
|
def test_serial(self):
|
||||||
|
params = {'asset_tag': ['A', 'B']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_asset_tag(self):
|
||||||
|
params = {'asset_tag': ['A', 'B']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ConsolePort.objects.all()
|
queryset = ConsolePort.objects.all()
|
||||||
filterset = ConsolePortFilterSet
|
filterset = ConsolePortFilterSet
|
||||||
|
@ -1697,6 +1697,75 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTestCase(
|
||||||
|
# Module does not support bulk renaming (no name field) or
|
||||||
|
# bulk creation (need to specify module bays)
|
||||||
|
ViewTestCases.GetObjectViewTestCase,
|
||||||
|
ViewTestCases.GetObjectChangelogViewTestCase,
|
||||||
|
ViewTestCases.EditObjectViewTestCase,
|
||||||
|
ViewTestCases.DeleteObjectViewTestCase,
|
||||||
|
ViewTestCases.ListObjectsViewTestCase,
|
||||||
|
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||||
|
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||||
|
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
||||||
|
):
|
||||||
|
model = Module
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
|
||||||
|
devices = (
|
||||||
|
create_test_device('Device 1'),
|
||||||
|
create_test_device('Device 2'),
|
||||||
|
)
|
||||||
|
|
||||||
|
module_types = (
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 1'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 2'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 3'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 4'),
|
||||||
|
)
|
||||||
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
|
module_bays = (
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 1'),
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 2'),
|
||||||
|
ModuleBay(device=devices[0], name='Module Bay 3'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 1'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
|
ModuleBay(device=devices[1], name='Module Bay 3'),
|
||||||
|
)
|
||||||
|
ModuleBay.objects.bulk_create(module_bays)
|
||||||
|
|
||||||
|
modules = (
|
||||||
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0]),
|
||||||
|
Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1]),
|
||||||
|
Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2]),
|
||||||
|
)
|
||||||
|
Module.objects.bulk_create(modules)
|
||||||
|
|
||||||
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
cls.form_data = {
|
||||||
|
'device': devices[1].pk,
|
||||||
|
'module_bay': module_bays[3].pk,
|
||||||
|
'module_type': module_types[0].pk,
|
||||||
|
'serial': 'A',
|
||||||
|
'tags': [t.pk for t in tags],
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.bulk_edit_data = {
|
||||||
|
'module_type': module_types[3].pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"device,module_bay,module_type,serial,asset_tag",
|
||||||
|
"Device 2,Module Bay 1,Module Type 1,A,A",
|
||||||
|
"Device 2,Module Bay 2,Module Type 2,B,B",
|
||||||
|
"Device 2,Module Bay 3,Module Type 3,C,C",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
|
|
||||||
|
@ -254,12 +254,24 @@ urlpatterns = [
|
|||||||
path('devices/<int:pk>/device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'),
|
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>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||||
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||||
path('devices/<int:pk>/changelog/', views.DeviceChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
path('devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||||
path('devices/<int:pk>/journal/', views.DeviceJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
|
path('devices/<int:pk>/journal/', ObjectJournalView.as_view(), name='device_journal', kwargs={'model': Device}),
|
||||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||||
|
|
||||||
|
# Modules
|
||||||
|
path('modules/', views.ModuleListView.as_view(), name='module_list'),
|
||||||
|
path('modules/add/', views.ModuleEditView.as_view(), name='module_add'),
|
||||||
|
path('modules/import/', views.ModuleBulkImportView.as_view(), name='module_import'),
|
||||||
|
path('modules/edit/', views.ModuleBulkEditView.as_view(), name='module_bulk_edit'),
|
||||||
|
path('modules/delete/', views.ModuleBulkDeleteView.as_view(), name='module_bulk_delete'),
|
||||||
|
path('modules/<int:pk>/', views.ModuleView.as_view(), name='module'),
|
||||||
|
path('modules/<int:pk>/edit/', views.ModuleEditView.as_view(), name='module_edit'),
|
||||||
|
path('modules/<int:pk>/delete/', views.ModuleDeleteView.as_view(), name='module_delete'),
|
||||||
|
path('modules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='module_changelog', kwargs={'model': Module}),
|
||||||
|
path('modules/<int:pk>/journal/', ObjectJournalView.as_view(), name='module_journal', kwargs={'model': Module}),
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||||
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||||
|
@ -13,7 +13,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN
|
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN
|
||||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
@ -30,7 +30,7 @@ from .constants import NONCONNECTABLE_IFACE_TYPES
|
|||||||
from .models import (
|
from .models import (
|
||||||
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||||
InventoryItem, Manufacturer, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
|
InventoryItem, Manufacturer, Module, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
|
||||||
PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
|
PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
|
||||||
RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
|
RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
|
||||||
)
|
)
|
||||||
@ -1629,14 +1629,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
|
|||||||
base_template = 'dcim/device/base.html'
|
base_template = 'dcim/device/base.html'
|
||||||
|
|
||||||
|
|
||||||
class DeviceChangeLogView(ObjectChangeLogView):
|
|
||||||
base_template = 'dcim/device/base.html'
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceJournalView(ObjectJournalView):
|
|
||||||
base_template = 'dcim/device/base.html'
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceEditView(generic.ObjectEditView):
|
class DeviceEditView(generic.ObjectEditView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
model_form = forms.DeviceForm
|
model_form = forms.DeviceForm
|
||||||
@ -1685,6 +1677,49 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Devices
|
||||||
|
#
|
||||||
|
|
||||||
|
class ModuleListView(generic.ObjectListView):
|
||||||
|
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||||
|
filterset = filtersets.ModuleFilterSet
|
||||||
|
filterset_form = forms.ModuleFilterForm
|
||||||
|
table = tables.ModuleTable
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleView(generic.ObjectView):
|
||||||
|
queryset = Module.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleEditView(generic.ObjectEditView):
|
||||||
|
queryset = Module.objects.all()
|
||||||
|
model_form = forms.ModuleForm
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = Module.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = Module.objects.all()
|
||||||
|
model_form = forms.ModuleCSVForm
|
||||||
|
table = tables.ModuleTable
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||||
|
filterset = filtersets.ModuleFilterSet
|
||||||
|
table = tables.ModuleTable
|
||||||
|
form = forms.ModuleBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = Module.objects.prefetch_related('device', 'module_type__manufacturer')
|
||||||
|
filterset = filtersets.ModuleFilterSet
|
||||||
|
table = tables.ModuleTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
@ -139,6 +139,7 @@ DEVICES_MENU = Menu(
|
|||||||
label='Devices',
|
label='Devices',
|
||||||
items=(
|
items=(
|
||||||
get_model_item('dcim', 'device', 'Devices'),
|
get_model_item('dcim', 'device', 'Devices'),
|
||||||
|
get_model_item('dcim', 'module', 'Modules'),
|
||||||
get_model_item('dcim', 'devicerole', 'Device Roles'),
|
get_model_item('dcim', 'devicerole', 'Device Roles'),
|
||||||
get_model_item('dcim', 'platform', 'Platforms'),
|
get_model_item('dcim', 'platform', 'Platforms'),
|
||||||
get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),
|
get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),
|
||||||
|
@ -102,6 +102,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% with devicebay_count=object.devicebays.count %}
|
||||||
|
{% if devicebay_count %}
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'device-bays' %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
|
||||||
|
</li>
|
||||||
|
{% 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 interface_count=object.interfaces_count %}
|
{% with interface_count=object.interfaces_count %}
|
||||||
{% if interface_count %}
|
{% if interface_count %}
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
@ -158,22 +174,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% 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">
|
|
||||||
<a class="nav-link {% if active_tab == 'device-bays' %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% with inventoryitem_count=object.inventoryitems.count %}
|
{% with inventoryitem_count=object.inventoryitems.count %}
|
||||||
{% if inventoryitem_count %}
|
{% if inventoryitem_count %}
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
|
@ -56,6 +56,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% with devicebay_count=object.devicebaytemplates.count %}
|
||||||
|
{% if devicebay_count %}
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'device-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
|
||||||
|
</li>
|
||||||
|
{% 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 interface_count=object.interfacetemplates.count %}
|
{% with interface_count=object.interfacetemplates.count %}
|
||||||
{% if interface_count %}
|
{% if interface_count %}
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
@ -111,20 +127,4 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% 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">
|
|
||||||
<a class="nav-link {% if active_tab == 'device-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
154
netbox/templates/dcim/module.html
Normal file
154
netbox/templates/dcim/module.html
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
{% load tz %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{{ block.super }}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'dcim:module_list' %}?module_type_id={{ object.module_type.pk }}">{{ object.module_type }}</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Module</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">Device Type</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ object.device.device_type.get_absolute_url }}">{{ object.device.device_type }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Module Type</th>
|
||||||
|
<td>
|
||||||
|
<a href="{{ object.module_type.get_absolute_url }}">{{ object.module_type }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Serial Number</th>
|
||||||
|
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Asset Tag</th>
|
||||||
|
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Components</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Interfaces</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.interfaces.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:interface_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Console Ports</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.consoleports.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:consoleport_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Console Server Ports</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.consoleserverports.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:consoleserverport_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Power Ports</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.powerports.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:powerport_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Power Outlets</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.poweroutlets.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:poweroutlet_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Front Ports</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.frontports.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:frontport_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Rear Ports</th>
|
||||||
|
<td>
|
||||||
|
{% with component_count=object.rearports.count %}
|
||||||
|
{% if component_count %}
|
||||||
|
<a href="{% url 'dcim:rearport_list' %}?module={{ object.pk }}">{{ component_count }}</a>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</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 %}
|
Loading…
Reference in New Issue
Block a user