Add Module model

This commit is contained in:
jeremystretch 2021-12-17 16:12:03 -05:00
parent 5bd223a468
commit 7777922bef
30 changed files with 967 additions and 151 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ __all__ = (
'InventoryItem', 'InventoryItem',
'Location', 'Location',
'Manufacturer', 'Manufacturer',
'Module',
'ModuleBay', 'ModuleBay',
'ModuleBayTemplate', 'ModuleBayTemplate',
'ModuleType', 'ModuleType',

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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