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',
'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer',
'NestedModuleSerializer',
'NestedModuleTypeSerializer',
'NestedPlatformSerializer',
'NestedPowerFeedSerializer',
@ -260,6 +261,18 @@ class NestedDeviceSerializer(WritableNestedSerializer):
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):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
@ -325,11 +338,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
# module = NestedModuleSerializer(read_only=True)
module = NestedModuleSerializer(read_only=True)
class Meta:
model = models.DeviceBay
fields = ['id', 'url', 'display', 'name']
model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer):

View File

@ -517,6 +517,20 @@ class DeviceSerializer(PrimaryModelSerializer):
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):
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('device-bay-templates', views.DeviceBayTemplateViewSet)
# Devices
# Device/modules
router.register('device-roles', views.DeviceRoleViewSet)
router.register('platforms', views.PlatformViewSet)
router.register('devices', views.DeviceViewSet)
router.register('modules', views.ModuleViewSet)
# Device components
router.register('console-ports', views.ConsolePortViewSet)

View File

@ -377,7 +377,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
#
# Devices
# Devices/modules
#
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
@ -526,6 +526,14 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
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
#

View File

@ -43,6 +43,7 @@ __all__ = (
'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
'ModuleFilterSet',
'ModuleTypeFilterSet',
'PathEndpointFilterSet',
'PlatformFilterSet',
@ -924,6 +925,42 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
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):
q = django_filters.CharFilter(
method='search',

View File

@ -32,6 +32,7 @@ __all__ = (
'InventoryItemBulkEditForm',
'LocationBulkEditForm',
'ManufacturerBulkEditForm',
'ModuleBulkEditForm',
'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm',
'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):
pk = forms.ModelMultipleChoiceField(
queryset=Cable.objects.all(),

View File

@ -26,6 +26,7 @@ __all__ = (
'InventoryItemCSVForm',
'LocationCSVForm',
'ManufacturerCSVForm',
'ModuleCSVForm',
'ModuleBayCSVForm',
'PlatformCSVForm',
'PowerFeedCSVForm',
@ -400,6 +401,35 @@ class DeviceCSVForm(BaseDeviceCSVForm):
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):
parent = CSVModelChoiceField(
queryset=Device.objects.all(),

View File

@ -29,6 +29,8 @@ __all__ = (
'InventoryItemFilterForm',
'LocationFilterForm',
'ManufacturerFilterForm',
'ModuleFilterForm',
'ModuleFilterForm',
'ModuleBayFilterForm',
'ModuleTypeFilterForm',
'PlatformFilterForm',
@ -645,6 +647,37 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
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):
model = VirtualChassis
field_groups = [

View File

@ -39,6 +39,7 @@ __all__ = (
'InventoryItemForm',
'LocationForm',
'ManufacturerForm',
'ModuleForm',
'ModuleBayForm',
'ModuleBayTemplateForm',
'ModuleTypeForm',
@ -651,6 +652,46 @@ class DeviceForm(TenancyForm, CustomFieldModelForm):
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):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),

View File

@ -56,6 +56,9 @@ class DCIMQuery(graphene.ObjectType):
manufacturer = ObjectField(ManufacturerType)
manufacturer_list = ObjectListField(ManufacturerType)
module = ObjectField(ModuleType)
module_list = ObjectListField(ModuleType)
module_bay = ObjectField(ModuleBayType)
module_bay_list = ObjectListField(ModuleBayType)

View File

@ -27,6 +27,7 @@ __all__ = (
'InventoryItemType',
'LocationType',
'ManufacturerType',
'ModuleType',
'ModuleBayType',
'ModuleBayTemplateType',
'ModuleTypeType',
@ -257,6 +258,14 @@ class ManufacturerType(OrganizationalObjectType):
filterset_class = filtersets.ManufacturerFilterSet
class ModuleType(ComponentObjectType):
class Meta:
model = models.Module
fields = '__all__'
filterset_class = filtersets.ModuleFilterSet
class ModuleBayType(ComponentObjectType):
class Meta:

View File

@ -95,36 +95,110 @@ class Migration(migrations.Migration):
'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(
model_name='consoleporttemplate',
name='module_type',
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(
model_name='consoleserverporttemplate',
name='module_type',
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(
model_name='frontporttemplate',
name='module_type',
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(
model_name='interfacetemplate',
name='module_type',
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(
model_name='poweroutlettemplate',
name='module_type',
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(
model_name='powerporttemplate',
name='module_type',
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(
model_name='rearporttemplate',
name='module_type',
@ -140,7 +214,7 @@ class Migration(migrations.Migration):
),
migrations.AlterUniqueTogether(
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(
name='interfacetemplate',
@ -175,23 +249,4 @@ class Migration(migrations.Migration):
'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',
'Location',
'Manufacturer',
'Module',
'ModuleBay',
'ModuleBayTemplate',
'ModuleType',

View File

@ -142,12 +142,12 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
('module_type', 'name'),
)
def instantiate(self, device):
def instantiate(self, **kwargs):
return ConsolePort(
device=device,
name=self.name,
label=self.label,
type=self.type
type=self.type,
**kwargs
)
@ -169,12 +169,12 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
('module_type', 'name'),
)
def instantiate(self, device):
def instantiate(self, **kwargs):
return ConsoleServerPort(
device=device,
name=self.name,
label=self.label,
type=self.type
type=self.type,
**kwargs
)
@ -208,14 +208,14 @@ class PowerPortTemplate(ModularComponentTemplateModel):
('module_type', 'name'),
)
def instantiate(self, device):
def instantiate(self, **kwargs):
return PowerPort(
device=device,
name=self.name,
label=self.label,
type=self.type,
maximum_draw=self.maximum_draw,
allocated_draw=self.allocated_draw
allocated_draw=self.allocated_draw,
**kwargs
)
def clean(self):
@ -273,18 +273,18 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
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:
power_port = PowerPort.objects.get(device=device, name=self.power_port.name)
power_port = PowerPort.objects.get(name=self.power_port.name, **kwargs)
else:
power_port = None
return PowerOutlet(
device=device,
name=self.name,
label=self.label,
type=self.type,
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'),
)
def instantiate(self, device):
def instantiate(self, **kwargs):
return Interface(
device=device,
name=self.name,
label=self.label,
type=self.type,
mgmt_only=self.mgmt_only
mgmt_only=self.mgmt_only,
**kwargs
)
@ -381,19 +381,19 @@ class FrontPortTemplate(ModularComponentTemplateModel):
except RearPortTemplate.DoesNotExist:
pass
def instantiate(self, device):
def instantiate(self, **kwargs):
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:
rear_port = None
return FrontPort(
device=device,
name=self.name,
label=self.label,
type=self.type,
color=self.color,
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'),
)
def instantiate(self, device):
def instantiate(self, **kwargs):
return RearPort(
device=device,
name=self.name,
label=self.label,
type=self.type,
color=self.color,
positions=self.positions
positions=self.positions,
**kwargs
)

View File

@ -87,6 +87,19 @@ class ComponentModel(PrimaryModel):
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):
"""
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')
class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
"""
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')
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.
"""
@ -294,7 +307,7 @@ class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
#
@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.
"""
@ -387,7 +400,7 @@ class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
@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.
"""
@ -502,7 +515,7 @@ class BaseInterface(models.Model):
@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.
"""
@ -765,7 +778,7 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
#
@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.
"""
@ -819,7 +832,7 @@ class FrontPort(ComponentModel, LinkTermination):
@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.
"""

View File

@ -26,6 +26,7 @@ __all__ = (
'DeviceRole',
'DeviceType',
'Manufacturer',
'Module',
'ModuleType',
'Platform',
'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 is_new:
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(
[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(
[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(
[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(
[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(
[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(
[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(
[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(
[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
@ -1008,6 +1009,85 @@ class Device(PrimaryModel, ConfigContextModel):
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
#

View File

@ -6,7 +6,7 @@ from dcim.models import ConsolePort, Interface, PowerPort
from .cables import *
from .devices import *
from .devicetypes import *
from .moduletypes import *
from .modules import *
from .power import *
from .racks import *
from .sites import *

View File

@ -725,26 +725,31 @@ class ModuleBayTable(DeviceComponentTable):
'args': [Accessor('device_id')],
}
)
installed_module = tables.Column(
linkify=True,
verbose_name='Installed module'
)
tags = TagColumn(
url_name='dcim:modulebay_list'
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'device', 'label', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'description')
fields = ('pk', 'id', 'name', 'device', 'label', 'installed_module', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
class DeviceModuleBayTable(ModuleBayTable):
actions = ButtonsColumn(
model=ModuleBay,
buttons=('edit', 'delete')
model=DeviceBay,
buttons=('edit', 'delete'),
prepend_template=MODULEBAY_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'label', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'label', 'description', 'actions')
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
default_columns = ('pk', 'name', 'label', 'description', 'installed_module', 'actions')
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 %}
"""
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.models import *
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 wireless.models import WirelessLAN
@ -1105,6 +1105,67 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
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):
model = ConsolePort
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 tenancy.models import Tenant, TenantGroup
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 wireless.choices import WirelessChannelChoices, WirelessRoleChoices
@ -1648,6 +1648,79 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
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):
queryset = ConsolePort.objects.all()
filterset = ConsolePortFilterSet

View File

@ -1697,6 +1697,75 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
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):
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>/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>/changelog/', views.DeviceChangeLogView.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>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', 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>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
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
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
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 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.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic
@ -30,7 +30,7 @@ from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import (
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
InventoryItem, Manufacturer, Module, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
)
@ -1629,14 +1629,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
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):
queryset = Device.objects.all()
model_form = forms.DeviceForm
@ -1685,6 +1677,49 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
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
#

View File

@ -139,6 +139,7 @@ DEVICES_MENU = Menu(
label='Devices',
items=(
get_model_item('dcim', 'device', 'Devices'),
get_model_item('dcim', 'module', 'Modules'),
get_model_item('dcim', 'devicerole', 'Device Roles'),
get_model_item('dcim', 'platform', 'Platforms'),
get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),

View File

@ -102,6 +102,22 @@
</a>
</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 %}
{% if interface_count %}
<li role="presentation" class="nav-item">
@ -158,22 +174,6 @@
{% endif %}
{% endwith %}
{% with modulebay_count=object.modulebays.count %}
{% if modulebay_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'module-bays' %} active{% endif %}" href="{% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with devicebay_count=object.devicebays.count %}
{% if devicebay_count %}
<li role="presentation" class="nav-item">
<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 %}
{% if inventoryitem_count %}
<li role="presentation" class="nav-item">

View File

@ -56,6 +56,22 @@
</a>
</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 %}
{% if interface_count %}
<li role="presentation" class="nav-item">
@ -111,20 +127,4 @@
</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 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 %}

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