Merge pull request #8105 from netbox-community/7844-modules

Closes #7844: Add support for device modules
This commit is contained in:
Jeremy Stretch 2021-12-20 10:55:50 -05:00 committed by GitHub
commit d69a314bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 3701 additions and 281 deletions

View File

@ -37,4 +37,5 @@ Once component templates have been created, every new device that you create as
{!models/dcim/interfacetemplate.md!} {!models/dcim/interfacetemplate.md!}
{!models/dcim/frontporttemplate.md!} {!models/dcim/frontporttemplate.md!}
{!models/dcim/rearporttemplate.md!} {!models/dcim/rearporttemplate.md!}
{!models/dcim/modulebaytemplate.md!}
{!models/dcim/devicebaytemplate.md!} {!models/dcim/devicebaytemplate.md!}

View File

@ -17,6 +17,7 @@ Device components represent discrete objects within a device which are used to t
{!models/dcim/interface.md!} {!models/dcim/interface.md!}
{!models/dcim/frontport.md!} {!models/dcim/frontport.md!}
{!models/dcim/rearport.md!} {!models/dcim/rearport.md!}
{!models/dcim/modulebay.md!}
{!models/dcim/devicebay.md!} {!models/dcim/devicebay.md!}
{!models/dcim/inventoryitem.md!} {!models/dcim/inventoryitem.md!}

View File

@ -0,0 +1,4 @@
# Modules
{!models/dcim/moduletype.md!}
{!models/dcim/module.md!}

View File

@ -5,4 +5,4 @@ Device bays represent a space or slot within a parent device in which a child de
Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices. Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices.
!!! note !!! note
Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device. Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, these should be modeled as modules installed within module bays.

View File

@ -1,3 +1,3 @@
## Device Bay Templates ## Device Bay Templates
A template for a device bay that will be created on all instantiations of the parent device type. A template for a device bay that will be created on all instantiations of the parent device type. Device bays hold child devices, such as blade servers.

View File

@ -0,0 +1,5 @@
# Modules
A module is a field-replaceable hardware component installed within a device which houses its own child components. The most common example is a chassis-based router or switch.
Similar to devices, modules are instantiated from module types, and any components associated with the module type are automatically instantiated on the new model. Each module must be installed within a module bay on a device, and each module bay may have only one module installed in it. A module may optionally be assigned a serial number and asset tag.

View File

@ -0,0 +1,3 @@
## Module Bays
Module bays represent a space or slot within a device in which a field-replaceable module may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device.

View File

@ -0,0 +1,3 @@
## Module Bay Templates
A template for a module bay that will be created on all instantiations of the parent device type. Module bays hold installed modules that do not have an independent management plane, such as line cards.

View File

@ -0,0 +1,23 @@
# Module Types
A module type represent a specific make and model of hardware component which is installable within a device and has its own child components. For example, consider a chassis-based switch or router with a number of field-replaceable line cards. Each line card has its own model number and includes a certain set of components such as interfaces. Each module type may have a manufacturer, model number, and part number assigned to it.
Similar to device types, each module type can have any of the following component templates associated with it:
* Interfaces
* Console ports
* Console server ports
* Power ports
* Power Outlets
* Front pass-through ports
* Rear pass-through ports
Note that device bays and module bays may _not_ be added to modules.
## Automatic Component Renaming
When adding component templates to a module type, the string `{module}` can be used to reference the `position` field of the module bay into which an instance of the module type is being installed.
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
Automatic renaming is supported for all modular component types (those listed above).

View File

@ -59,6 +59,7 @@ nav:
- Sites and Racks: 'core-functionality/sites-and-racks.md' - Sites and Racks: 'core-functionality/sites-and-racks.md'
- Devices and Cabling: 'core-functionality/devices.md' - Devices and Cabling: 'core-functionality/devices.md'
- Device Types: 'core-functionality/device-types.md' - Device Types: 'core-functionality/device-types.md'
- Modules: 'core-functionality/modules.md'
- Virtualization: 'core-functionality/virtualization.md' - Virtualization: 'core-functionality/virtualization.md'
- Service Mapping: 'core-functionality/services.md' - Service Mapping: 'core-functionality/services.md'
- Circuits: 'core-functionality/circuits.md' - Circuits: 'core-functionality/circuits.md'

View File

@ -4,6 +4,7 @@ from dcim import models
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [ __all__ = [
'ComponentNestedModuleSerializer',
'NestedCableSerializer', 'NestedCableSerializer',
'NestedConsolePortSerializer', 'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer', 'NestedConsolePortTemplateSerializer',
@ -20,6 +21,10 @@ __all__ = [
'NestedInterfaceTemplateSerializer', 'NestedInterfaceTemplateSerializer',
'NestedInventoryItemSerializer', 'NestedInventoryItemSerializer',
'NestedManufacturerSerializer', 'NestedManufacturerSerializer',
'NestedModuleBaySerializer',
'NestedModuleBayTemplateSerializer',
'NestedModuleSerializer',
'NestedModuleTypeSerializer',
'NestedPlatformSerializer', 'NestedPlatformSerializer',
'NestedPowerFeedSerializer', 'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer', 'NestedPowerOutletSerializer',
@ -117,7 +122,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
# #
# Device types # Device/module types
# #
class NestedManufacturerSerializer(WritableNestedSerializer): class NestedManufacturerSerializer(WritableNestedSerializer):
@ -139,6 +144,20 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count'] fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
fields = ['id', 'url', 'display', 'manufacturer', 'model']
#
# Component templates
#
class NestedConsolePortTemplateSerializer(WritableNestedSerializer): class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
@ -195,6 +214,14 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
class Meta:
model = models.ModuleBayTemplate
fields = ['id', 'url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer): class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
@ -235,6 +262,37 @@ class NestedDeviceSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'name']
class ComponentNestedModuleSerializer(WritableNestedSerializer):
"""
Used by device component serializers.
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'device', 'module_bay']
class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True)
module_bay = ModuleNestedModuleBaySerializer(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)
@ -298,6 +356,15 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
module = NestedModuleSerializer(read_only=True)
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)

View File

@ -261,7 +261,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# #
# Device types # Device/module types
# #
class ManufacturerSerializer(PrimaryModelSerializer): class ManufacturerSerializer(PrimaryModelSerializer):
@ -294,6 +294,23 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
] ]
class ModuleTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = ModuleType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
#
# Component templates
#
class ConsolePortTemplateSerializer(ValidatedModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
@ -409,6 +426,18 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
] ]
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
device_type = NestedDeviceTypeSerializer()
class Meta:
model = ModuleBayTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
'last_updated',
]
class DeviceBayTemplateSerializer(ValidatedModelSerializer): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
@ -491,6 +520,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()
@ -518,6 +561,10 @@ class DeviceNAPALMSerializer(serializers.Serializer):
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True, allow_blank=True,
@ -533,8 +580,8 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -542,6 +589,10 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True, allow_blank=True,
@ -557,8 +608,8 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -566,6 +617,10 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
allow_blank=True, allow_blank=True,
@ -587,15 +642,20 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField( type = ChoiceField(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
allow_blank=True, allow_blank=True,
@ -606,15 +666,20 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True) parent = NestedInterfaceSerializer(required=False, allow_null=True)
bridge = NestedInterfaceSerializer(required=False, allow_null=True) bridge = NestedInterfaceSerializer(required=False, allow_null=True)
@ -643,12 +708,12 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint', 'connected_endpoint_type', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'count_fhrp_groups', '_occupied', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
] ]
def validate(self, data): def validate(self, data):
@ -668,13 +733,17 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'positions', 'description', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
] ]
@ -694,6 +763,10 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer() rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
@ -701,9 +774,22 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags',
'created', 'last_updated', '_occupied', 'custom_fields', 'created', 'last_updated', '_occupied',
]
class ModuleBaySerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields',
'created', 'last_updated',
] ]

View File

@ -16,9 +16,10 @@ router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)
# Device types # Device/module types
router.register('manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register('device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
router.register('module-types', views.ModuleTypeViewSet)
# Device type components # Device type components
router.register('console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)
@ -28,12 +29,14 @@ router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register('interface-templates', views.InterfaceTemplateViewSet) router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register('front-port-templates', views.FrontPortTemplateViewSet) router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register('rear-port-templates', views.RearPortTemplateViewSet) router.register('rear-port-templates', views.RearPortTemplateViewSet)
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)
@ -43,6 +46,7 @@ router.register('power-outlets', views.PowerOutletViewSet)
router.register('interfaces', views.InterfaceViewSet) router.register('interfaces', views.InterfaceViewSet)
router.register('front-ports', views.FrontPortViewSet) router.register('front-ports', views.FrontPortViewSet)
router.register('rear-ports', views.RearPortViewSet) router.register('rear-ports', views.RearPortViewSet)
router.register('module-bays', views.ModuleBayViewSet)
router.register('device-bays', views.DeviceBayViewSet) router.register('device-bays', views.DeviceBayViewSet)
router.register('inventory-items', views.InventoryItemViewSet) router.register('inventory-items', views.InventoryItemViewSet)

View File

@ -271,7 +271,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
# #
# Device types # Device/module types
# #
class DeviceTypeViewSet(CustomFieldModelViewSet): class DeviceTypeViewSet(CustomFieldModelViewSet):
@ -283,6 +283,15 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
brief_prefetch_fields = ['manufacturer'] brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(CustomFieldModelViewSet):
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
# module_count=count_related(Module, 'module_type')
)
serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet
brief_prefetch_fields = ['manufacturer']
# #
# Device type components # Device type components
# #
@ -329,6 +338,12 @@ class RearPortTemplateViewSet(ModelViewSet):
filterset_class = filtersets.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(ModelViewSet):
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet): class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
@ -362,7 +377,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
# #
# Devices # Devices/modules
# #
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
@ -511,12 +526,22 @@ 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
# #
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = ConsolePort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@ -524,7 +549,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related( queryset = ConsoleServerPort.objects.prefetch_related(
'device', '_path__destination', 'cable', '_link_peer', 'tags' 'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
) )
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
@ -532,14 +557,18 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
class PowerPortViewSet(PathEndpointMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = PowerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_link_peer', 'tags') queryset = PowerOutlet.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@ -547,8 +576,8 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
class InterfaceViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer', 'wireless_lans', 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags' 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
@ -556,28 +585,39 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
)
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, ModelViewSet): class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
)
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class ModuleBayViewSet(ModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags')
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet): class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet): class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']

View File

@ -41,6 +41,10 @@ __all__ = (
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'LocationFilterSet', 'LocationFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'ModuleBayFilterSet',
'ModuleBayTemplateFilterSet',
'ModuleFilterSet',
'ModuleTypeFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
'PowerConnectionFilterSet', 'PowerConnectionFilterSet',
@ -447,6 +451,10 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter( device_bays = django_filters.BooleanFilter(
method='_device_bays', method='_device_bays',
label='Has device bays', label='Has device bays',
@ -490,10 +498,90 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
rearporttemplates__isnull=value rearporttemplates__isnull=value
) )
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebaytemplates__isnull=value)
def _device_bays(self, queryset, name, value): def _device_bays(self, queryset, name, value):
return queryset.exclude(devicebaytemplates__isnull=value) return queryset.exclude(devicebaytemplates__isnull=value)
class ModuleTypeFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
)
console_server_ports = django_filters.BooleanFilter(
method='_console_server_ports',
label='Has console server ports',
)
power_ports = django_filters.BooleanFilter(
method='_power_ports',
label='Has power ports',
)
power_outlets = django_filters.BooleanFilter(
method='_power_outlets',
label='Has power outlets',
)
interfaces = django_filters.BooleanFilter(
method='_interfaces',
label='Has interfaces',
)
pass_through_ports = django_filters.BooleanFilter(
method='_pass_through_ports',
label='Has pass-through ports',
)
tag = TagFilter()
class Meta:
model = ModuleType
fields = ['id', 'model', 'part_number']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(manufacturer__name__icontains=value) |
Q(model__icontains=value) |
Q(part_number__icontains=value) |
Q(comments__icontains=value)
)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleporttemplates__isnull=value)
def _console_server_ports(self, queryset, name, value):
return queryset.exclude(consoleserverporttemplates__isnull=value)
def _power_ports(self, queryset, name, value):
return queryset.exclude(powerporttemplates__isnull=value)
def _power_outlets(self, queryset, name, value):
return queryset.exclude(poweroutlettemplates__isnull=value)
def _interfaces(self, queryset, name, value):
return queryset.exclude(interfacetemplates__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
frontporttemplates__isnull=value,
rearporttemplates__isnull=value
)
class DeviceTypeComponentFilterSet(django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
@ -511,28 +599,36 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
return queryset.filter(name__icontains=value) return queryset.filter(name__icontains=value)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
moduletype_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleType.objects.all(),
field_name='module_type_id',
label='Module type (ID)',
)
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter( feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
@ -543,7 +639,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompone
fields = ['id', 'name', 'type', 'feed_leg'] fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
null_value=None null_value=None
@ -554,7 +650,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'mgmt_only'] fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -565,7 +661,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
fields = ['id', 'name', 'type', 'color'] fields = ['id', 'name', 'type', 'color']
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
@ -576,6 +672,13 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentF
fields = ['id', 'name', 'type', 'color', 'positions'] fields = ['id', 'name', 'type', 'color', 'positions']
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
model = ModuleBayTemplate
fields = ['id', 'name']
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
@ -760,6 +863,10 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
method='_pass_through_ports', method='_pass_through_ports',
label='Has pass-through ports', label='Has pass-through ports',
) )
module_bays = django_filters.BooleanFilter(
method='_module_bays',
label='Has module bays',
)
device_bays = django_filters.BooleanFilter( device_bays = django_filters.BooleanFilter(
method='_device_bays', method='_device_bays',
label='Has device bays', label='Has device bays',
@ -811,10 +918,49 @@ class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContex
rearports__isnull=value rearports__isnull=value
) )
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebays__isnull=value)
def _device_bays(self, queryset, name, value): def _device_bays(self, queryset, name, value):
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',
@ -1104,6 +1250,13 @@ class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTe
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description'] fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
class ModuleBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta:
model = ModuleBay
fields = ['id', 'name', 'label', 'description']
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:

View File

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

View File

@ -7,7 +7,6 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
from ipam.models import VLAN, ASN from ipam.models import VLAN, ASN
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
@ -33,6 +32,10 @@ __all__ = (
'InventoryItemBulkEditForm', 'InventoryItemBulkEditForm',
'LocationBulkEditForm', 'LocationBulkEditForm',
'ManufacturerBulkEditForm', 'ManufacturerBulkEditForm',
'ModuleBulkEditForm',
'ModuleBayBulkEditForm',
'ModuleBayTemplateBulkEditForm',
'ModuleTypeBulkEditForm',
'PlatformBulkEditForm', 'PlatformBulkEditForm',
'PowerFeedBulkEditForm', 'PowerFeedBulkEditForm',
'PowerOutletBulkEditForm', 'PowerOutletBulkEditForm',
@ -326,6 +329,9 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
part_number = forms.CharField(
required=False
)
u_height = forms.IntegerField( u_height = forms.IntegerField(
min_value=1, min_value=1,
required=False required=False
@ -342,7 +348,24 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
) )
class Meta: class Meta:
nullable_fields = ['airflow'] nullable_fields = ['part_number', 'airflow']
class ModuleTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleType.objects.all(),
widget=forms.MultipleHiddenInput()
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
)
part_number = forms.CharField(
required=False
)
class Meta:
nullable_fields = ['part_number']
class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
@ -451,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(),
@ -823,6 +872,23 @@ class RearPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('description',) nullable_fields = ('description',)
class ModuleBayTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBayTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
max_length=64,
required=False
)
description = forms.CharField(
required=False
)
class Meta:
nullable_fields = ('label', 'position', 'description')
class DeviceBayTemplateBulkEditForm(BulkEditForm): class DeviceBayTemplateBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=DeviceBayTemplate.objects.all(), queryset=DeviceBayTemplate.objects.all(),
@ -1076,6 +1142,20 @@ class RearPortBulkEditForm(
nullable_fields = ['label', 'description'] nullable_fields = ['label', 'description']
class ModuleBayBulkEditForm(
form_from_model(DeviceBay, ['label', 'description']),
AddRemoveTagsForm,
CustomFieldModelBulkEditForm
):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBay.objects.all(),
widget=forms.MultipleHiddenInput()
)
class Meta:
nullable_fields = ['label', 'position', 'description']
class DeviceBayBulkEditForm( class DeviceBayBulkEditForm(
form_from_model(DeviceBay, ['label', 'description']), form_from_model(DeviceBay, ['label', 'description']),
AddRemoveTagsForm, AddRemoveTagsForm,

View File

@ -26,6 +26,8 @@ __all__ = (
'InventoryItemCSVForm', 'InventoryItemCSVForm',
'LocationCSVForm', 'LocationCSVForm',
'ManufacturerCSVForm', 'ManufacturerCSVForm',
'ModuleCSVForm',
'ModuleBayCSVForm',
'PlatformCSVForm', 'PlatformCSVForm',
'PowerFeedCSVForm', 'PowerFeedCSVForm',
'PowerOutletCSVForm', 'PowerOutletCSVForm',
@ -399,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(),
@ -678,6 +709,17 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
} }
class ModuleBayCSVForm(CustomFieldModelCSVForm):
device = CSVModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleBay
fields = ('device', 'name', 'label', 'position', 'description')
class DeviceBayCSVForm(CustomFieldModelCSVForm): class DeviceBayCSVForm(CustomFieldModelCSVForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),

View File

@ -29,6 +29,10 @@ __all__ = (
'InventoryItemFilterForm', 'InventoryItemFilterForm',
'LocationFilterForm', 'LocationFilterForm',
'ManufacturerFilterForm', 'ManufacturerFilterForm',
'ModuleFilterForm',
'ModuleFilterForm',
'ModuleBayFilterForm',
'ModuleTypeFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
'PowerConnectionFilterForm', 'PowerConnectionFilterForm',
'PowerFeedFilterForm', 'PowerFeedFilterForm',
@ -336,7 +340,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
model = DeviceType model = DeviceType
field_groups = [ field_groups = [
['q', 'tag'], ['q', 'tag'],
['manufacturer_id', 'subdevice_role', 'airflow'], ['manufacturer_id', 'part_number', 'subdevice_role', 'airflow'],
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
] ]
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
@ -345,6 +349,9 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
label=_('Manufacturer'), label=_('Manufacturer'),
fetch_trigger='open' fetch_trigger='open'
) )
part_number = forms.CharField(
required=False
)
subdevice_role = forms.MultipleChoiceField( subdevice_role = forms.MultipleChoiceField(
choices=add_blank_choice(SubdeviceRoleChoices), choices=add_blank_choice(SubdeviceRoleChoices),
required=False, required=False,
@ -400,6 +407,67 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ModuleTypeFilterForm(CustomFieldModelFilterForm):
model = ModuleType
field_groups = [
['q', 'tag'],
['manufacturer_id', 'part_number'],
['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'],
]
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
)
part_number = forms.CharField(
required=False
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
widget=StaticSelect(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
class DeviceRoleFilterForm(CustomFieldModelFilterForm): class DeviceRoleFilterForm(CustomFieldModelFilterForm):
model = DeviceRole model = DeviceRole
tag = TagFilterField(model) tag = TagFilterField(model)
@ -579,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 = [
@ -970,6 +1069,19 @@ class RearPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
field_groups = [
['q', 'tag'],
['name', 'label', 'position'],
['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
]
tag = TagFilterField(model)
position = forms.CharField(
required=False
)
class DeviceBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay model = DeviceBay
field_groups = [ field_groups = [

View File

@ -39,6 +39,10 @@ __all__ = (
'InventoryItemForm', 'InventoryItemForm',
'LocationForm', 'LocationForm',
'ManufacturerForm', 'ManufacturerForm',
'ModuleForm',
'ModuleBayForm',
'ModuleBayTemplateForm',
'ModuleTypeForm',
'PlatformForm', 'PlatformForm',
'PopulateDeviceBayForm', 'PopulateDeviceBayForm',
'PowerFeedForm', 'PowerFeedForm',
@ -412,6 +416,23 @@ class DeviceTypeForm(CustomFieldModelForm):
} }
class ModuleTypeForm(CustomFieldModelForm):
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all()
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ModuleType
fields = [
'manufacturer', 'model', 'part_number', 'comments', 'tags',
]
class DeviceRoleForm(CustomFieldModelForm): class DeviceRoleForm(CustomFieldModelForm):
slug = SlugField() slug = SlugField()
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
@ -631,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(),
@ -890,10 +951,11 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
} }
@ -901,10 +963,11 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
} }
@ -912,10 +975,11 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
} }
@ -923,19 +987,21 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType # Limit power_port choices to current DeviceType/ModuleType
if hasattr(self.instance, 'device_type'): if self.instance.pk:
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter( self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=self.instance.device_type device_type=self.instance.device_type,
module_type=self.instance.module_type
) )
@ -943,10 +1009,11 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
@ -955,20 +1022,23 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'rear_port': StaticSelect(), 'rear_port': StaticSelect(),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit rear_port choices to current DeviceType # Limit rear_port choices to current DeviceType/ModuleType
if hasattr(self.instance, 'device_type'): if self.instance.pk:
self.fields['rear_port'].queryset = RearPortTemplate.objects.filter( self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
device_type=self.instance.device_type device_type=self.instance.device_type,
module_type=self.instance.module_type
) )
@ -976,14 +1046,26 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'color', 'positions', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(), 'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
]
widgets = {
'device_type': forms.HiddenInput(),
}
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@ -1222,6 +1304,22 @@ class RearPortForm(CustomFieldModelForm):
} }
class ModuleBayForm(CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ModuleBay
fields = [
'device', 'name', 'label', 'position', 'description', 'tags',
]
widgets = {
'device': forms.HiddenInput(),
}
class DeviceBayForm(CustomFieldModelForm): class DeviceBayForm(CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),

View File

@ -25,6 +25,8 @@ __all__ = (
'InterfaceCreateForm', 'InterfaceCreateForm',
'InterfaceTemplateCreateForm', 'InterfaceTemplateCreateForm',
'InventoryItemCreateForm', 'InventoryItemCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm', 'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm', 'PowerOutletTemplateCreateForm',
'PowerPortCreateForm', 'PowerPortCreateForm',
@ -150,11 +152,13 @@ class ComponentTemplateCreateForm(ComponentForm):
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
initial_params={ initial_params={
'device_types': 'device_type' 'device_types': 'device_type',
'module_types': 'module_type',
} }
) )
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
required=False,
query_params={ query_params={
'manufacturer_id': '$manufacturer' 'manufacturer_id': '$manufacturer'
} }
@ -164,23 +168,37 @@ class ComponentTemplateCreateForm(ComponentForm):
) )
class ConsolePortTemplateCreateForm(ComponentTemplateCreateForm): class ModularComponentTemplateCreateForm(ComponentTemplateCreateForm):
module_type = DynamicModelChoiceField(
queryset=ModuleType.objects.all(),
required=False,
query_params={
'manufacturer_id': '$manufacturer'
}
)
class ConsolePortTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices), choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect() widget=StaticSelect()
) )
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
)
class ConsoleServerPortTemplateCreateForm(ComponentTemplateCreateForm): class ConsoleServerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(ConsolePortTypeChoices), choices=add_blank_choice(ConsolePortTypeChoices),
widget=StaticSelect() widget=StaticSelect()
) )
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'description') field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'description',
)
class PowerPortTemplateCreateForm(ComponentTemplateCreateForm): class PowerPortTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(PowerPortTypeChoices), choices=add_blank_choice(PowerPortTypeChoices),
required=False required=False
@ -196,19 +214,23 @@ class PowerPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
field_order = ( field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'maximum_draw',
'description', 'allocated_draw', 'description',
) )
class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm): class PowerOutletTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices), choices=add_blank_choice(PowerOutletTypeChoices),
required=False required=False
) )
power_port = forms.ModelChoiceField( power_port = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(), queryset=PowerPortTemplate.objects.all(),
required=False required=False,
query_params={
'devicetype_id': '$device_type',
'moduletype_id': '$module_type',
}
) )
feed_leg = forms.ChoiceField( feed_leg = forms.ChoiceField(
choices=add_blank_choice(PowerOutletFeedLegChoices), choices=add_blank_choice(PowerOutletFeedLegChoices),
@ -216,23 +238,12 @@ class PowerOutletTemplateCreateForm(ComponentTemplateCreateForm):
widget=StaticSelect() widget=StaticSelect()
) )
field_order = ( field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg', 'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'power_port', 'feed_leg',
'description', 'description',
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType class InterfaceTemplateCreateForm(ModularComponentTemplateCreateForm):
device_type = DeviceType.objects.get(
pk=self.initial.get('device_type') or self.data.get('device_type')
)
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
device_type=device_type
)
class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
widget=StaticSelect() widget=StaticSelect()
@ -241,10 +252,13 @@ class InterfaceTemplateCreateForm(ComponentTemplateCreateForm):
required=False, required=False,
label='Management only' label='Management only'
) )
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only', 'description') field_order = (
'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'mgmt_only',
'description',
)
class FrontPortTemplateCreateForm(ComponentTemplateCreateForm): class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=PortTypeChoices, choices=PortTypeChoices,
widget=StaticSelect() widget=StaticSelect()
@ -258,7 +272,8 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text='Select one rear port assignment for each front port being created.', help_text='Select one rear port assignment for each front port being created.',
) )
field_order = ( field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set', 'description', 'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'rear_port_set',
'description',
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -308,7 +323,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm):
} }
class RearPortTemplateCreateForm(ComponentTemplateCreateForm): class RearPortTemplateCreateForm(ModularComponentTemplateCreateForm):
type = forms.ChoiceField( type = forms.ChoiceField(
choices=PortTypeChoices, choices=PortTypeChoices,
widget=StaticSelect(), widget=StaticSelect(),
@ -323,10 +338,16 @@ class RearPortTemplateCreateForm(ComponentTemplateCreateForm):
help_text='The number of front ports which may be mapped to each rear port' help_text='The number of front ports which may be mapped to each rear port'
) )
field_order = ( field_order = (
'manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions', 'description', 'manufacturer', 'device_type', 'module_type', 'name_pattern', 'label_pattern', 'type', 'color', 'positions',
'description',
) )
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
# TODO: Support patterned position assignment
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm): class DeviceBayTemplateCreateForm(ComponentTemplateCreateForm):
field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description') field_order = ('manufacturer', 'device_type', 'name_pattern', 'label_pattern', 'description')
@ -619,6 +640,11 @@ class RearPortCreateForm(ComponentCreateForm):
) )
class ModuleBayCreateForm(ComponentCreateForm):
model = ModuleBay
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')
class DeviceBayCreateForm(ComponentCreateForm): class DeviceBayCreateForm(ComponentCreateForm):
model = DeviceBay model = DeviceBay
field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags') field_order = ('device', 'name_pattern', 'label_pattern', 'description', 'tags')

View File

@ -11,6 +11,8 @@ __all__ = (
'DeviceTypeImportForm', 'DeviceTypeImportForm',
'FrontPortTemplateImportForm', 'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm', 'InterfaceTemplateImportForm',
'ModuleBayTemplateImportForm',
'ModuleTypeImportForm',
'PowerOutletTemplateImportForm', 'PowerOutletTemplateImportForm',
'PowerPortTemplateImportForm', 'PowerPortTemplateImportForm',
'RearPortTemplateImportForm', 'RearPortTemplateImportForm',
@ -31,29 +33,38 @@ class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
] ]
class ModuleTypeImportForm(BootstrapMixin, forms.ModelForm):
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name'
)
class Meta:
model = ModuleType
fields = ['manufacturer', 'model', 'part_number', 'comments']
# #
# Component template import forms # Component template import forms
# #
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
def __init__(self, device_type, data=None, *args, **kwargs):
# Must pass the parent DeviceType on form initialization
data.update({
'device_type': device_type.pk,
})
super().__init__(data, *args, **kwargs)
def clean_device_type(self): def clean_device_type(self):
data = self.cleaned_data['device_type']
# Limit fields referencing other components to the parent DeviceType # Limit fields referencing other components to the parent DeviceType
for field_name, field in self.fields.items(): if data := self.cleaned_data['device_type']:
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type': for field_name, field in self.fields.items():
field.queryset = field.queryset.filter(device_type=data) if isinstance(field, forms.ModelChoiceField) and field_name not in ['device_type', 'module_type']:
field.queryset = field.queryset.filter(device_type=data)
return data
def clean_module_type(self):
# Limit fields referencing other components to the parent ModuleType
if data := self.cleaned_data['module_type']:
for field_name, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field_name not in ['device_type', 'module_type']:
field.queryset = field.queryset.filter(module_type=data)
return data return data
@ -63,7 +74,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
@ -72,7 +83,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
@ -81,7 +92,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
] ]
@ -95,7 +106,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
] ]
@ -107,7 +118,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description',
] ]
@ -123,7 +134,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description', 'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
] ]
@ -135,7 +146,16 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'device_type', 'name', 'type', 'positions', 'label', 'description', 'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
]
class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
] ]

View File

@ -56,6 +56,18 @@ 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_list = ObjectListField(ModuleBayType)
module_bay_template = ObjectField(ModuleBayTemplateType)
module_bay_template_list = ObjectListField(ModuleBayTemplateType)
module_type = ObjectField(ModuleTypeType)
module_type_list = ObjectListField(ModuleTypeType)
platform = ObjectField(PlatformType) platform = ObjectField(PlatformType)
platform_list = ObjectListField(PlatformType) platform_list = ObjectListField(PlatformType)

View File

@ -27,6 +27,10 @@ __all__ = (
'InventoryItemType', 'InventoryItemType',
'LocationType', 'LocationType',
'ManufacturerType', 'ManufacturerType',
'ModuleType',
'ModuleBayType',
'ModuleBayTemplateType',
'ModuleTypeType',
'PlatformType', 'PlatformType',
'PowerFeedType', 'PowerFeedType',
'PowerOutletType', 'PowerOutletType',
@ -254,6 +258,38 @@ 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 Meta:
model = models.ModuleBay
fields = '__all__'
filterset_class = filtersets.ModuleBayFilterSet
class ModuleBayTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ModuleBayTemplate
fields = '__all__'
filterset_class = filtersets.ModuleBayTemplateFilterSet
class ModuleTypeType(PrimaryObjectType):
class Meta:
model = models.ModuleType
fields = '__all__'
filterset_class = filtersets.ModuleTypeFilterSet
class PlatformType(OrganizationalObjectType): class PlatformType(OrganizationalObjectType):
class Meta: class Meta:

View File

@ -0,0 +1,254 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.fields
import utilities.ordering
class Migration(migrations.Migration):
dependencies = [
('extras', '0066_customfield_name_validation'),
('dcim', '0144_site_remove_deprecated_fields'),
]
operations = [
migrations.AlterModelOptions(
name='consoleporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='consoleserverporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='frontporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='interfacetemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='poweroutlettemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='powerporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterModelOptions(
name='rearporttemplate',
options={'ordering': ('device_type', 'module_type', '_name')},
),
migrations.AlterField(
model_name='consoleporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='frontporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='frontporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='interfacetemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfacetemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlettemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='powerporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='powerporttemplates', to='dcim.devicetype'),
),
migrations.AlterField(
model_name='rearporttemplate',
name='device_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.devicetype'),
),
migrations.CreateModel(
name='ModuleType',
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)),
('model', models.CharField(max_length=100)),
('part_number', models.CharField(blank=True, max_length=50)),
('comments', models.TextField(blank=True)),
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='module_types', to='dcim.manufacturer')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('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)),
('position', models.CharField(blank=True, max_length=30)),
('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',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rearporttemplates', to='dcim.moduletype'),
),
migrations.AlterUniqueTogether(
name='consoleporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='consoleserverporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='frontporttemplate',
unique_together={('device_type', 'name'), ('rear_port', 'rear_port_position'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='interfacetemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='poweroutlettemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='powerporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.AlterUniqueTogether(
name='rearporttemplate',
unique_together={('device_type', 'name'), ('module_type', 'name')},
),
migrations.CreateModel(
name='ModuleBayTemplate',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('_name', utilities.fields.NaturalOrderingField('name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize)),
('label', models.CharField(blank=True, max_length=64)),
('position', models.CharField(blank=True, max_length=30)),
('description', models.CharField(blank=True, max_length=200)),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modulebaytemplates', to='dcim.devicetype')),
],
options={
'ordering': ('device_type', '_name'),
'unique_together': {('device_type', 'name')},
},
),
]

View File

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

View File

@ -9,7 +9,7 @@ from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, ModuleBay, PowerOutlet, PowerPort, RearPort,
) )
@ -19,6 +19,7 @@ __all__ = (
'DeviceBayTemplate', 'DeviceBayTemplate',
'FrontPortTemplate', 'FrontPortTemplate',
'InterfaceTemplate', 'InterfaceTemplate',
'ModuleBayTemplate',
'PowerOutletTemplate', 'PowerOutletTemplate',
'PowerPortTemplate', 'PowerPortTemplate',
'RearPortTemplate', 'RearPortTemplate',
@ -63,7 +64,7 @@ class ComponentTemplateModel(ChangeLoggedModel):
""" """
raise NotImplementedError() raise NotImplementedError()
def to_objectchange(self, action): def to_objectchange(self, action, related_object=None):
# Annotate the parent DeviceType # Annotate the parent DeviceType
try: try:
device_type = self.device_type device_type = self.device_type
@ -73,8 +74,63 @@ class ComponentTemplateModel(ChangeLoggedModel):
return super().to_objectchange(action, related_object=device_type) return super().to_objectchange(action, related_object=device_type)
class ModularComponentTemplateModel(ComponentTemplateModel):
"""
A ComponentTemplateModel which supports optional assignment to a ModuleType.
"""
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='%(class)ss',
blank=True,
null=True
)
module_type = models.ForeignKey(
to='dcim.ModuleType',
on_delete=models.CASCADE,
related_name='%(class)ss',
blank=True,
null=True
)
class Meta:
abstract = True
def to_objectchange(self, action, related_object=None):
# Annotate the parent DeviceType or ModuleType
try:
if getattr(self, 'device_type'):
return super().to_objectchange(action, related_object=self.device_type)
except ObjectDoesNotExist:
pass
try:
if getattr(self, 'module_type'):
return super().to_objectchange(action, related_object=self.module_type)
except ObjectDoesNotExist:
pass
return super().to_objectchange(action)
def clean(self):
super().clean()
# A component template must belong to a DeviceType *or* to a ModuleType
if self.device_type and self.module_type:
raise ValidationError(
"A component template cannot be associated with both a device type and a module type."
)
if not self.device_type and not self.module_type:
raise ValidationError(
"A component template must be associated with either a device type or a module type."
)
def resolve_name(self, module):
if module:
return self.name.replace('{module}', module.module_bay.position)
return self.name
@extras_features('webhooks') @extras_features('webhooks')
class ConsolePortTemplate(ComponentTemplateModel): class ConsolePortTemplate(ModularComponentTemplateModel):
""" """
A template for a ConsolePort to be created for a new Device. A template for a ConsolePort to be created for a new Device.
""" """
@ -85,20 +141,23 @@ class ConsolePortTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ('device_type', 'name') unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device): def instantiate(self, **kwargs):
return ConsolePort( return ConsolePort(
device=device, name=self.resolve_name(kwargs.get('module')),
name=self.name,
label=self.label, label=self.label,
type=self.type type=self.type,
**kwargs
) )
@extras_features('webhooks') @extras_features('webhooks')
class ConsoleServerPortTemplate(ComponentTemplateModel): class ConsoleServerPortTemplate(ModularComponentTemplateModel):
""" """
A template for a ConsoleServerPort to be created for a new Device. A template for a ConsoleServerPort to be created for a new Device.
""" """
@ -109,20 +168,23 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ('device_type', 'name') unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device): def instantiate(self, **kwargs):
return ConsoleServerPort( return ConsoleServerPort(
device=device, name=self.resolve_name(kwargs.get('module')),
name=self.name,
label=self.label, label=self.label,
type=self.type type=self.type,
**kwargs
) )
@extras_features('webhooks') @extras_features('webhooks')
class PowerPortTemplate(ComponentTemplateModel): class PowerPortTemplate(ModularComponentTemplateModel):
""" """
A template for a PowerPort to be created for a new Device. A template for a PowerPort to be created for a new Device.
""" """
@ -145,17 +207,20 @@ class PowerPortTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ('device_type', 'name') unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device): def instantiate(self, **kwargs):
return PowerPort( return PowerPort(
device=device, name=self.resolve_name(kwargs.get('module')),
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):
@ -169,7 +234,7 @@ class PowerPortTemplate(ComponentTemplateModel):
@extras_features('webhooks') @extras_features('webhooks')
class PowerOutletTemplate(ComponentTemplateModel): class PowerOutletTemplate(ModularComponentTemplateModel):
""" """
A template for a PowerOutlet to be created for a new Device. A template for a PowerOutlet to be created for a new Device.
""" """
@ -193,35 +258,43 @@ class PowerOutletTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ('device_type', 'name') unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def clean(self): def clean(self):
super().clean() super().clean()
# Validate power port assignment # Validate power port assignment
if self.power_port and self.power_port.device_type != self.device_type:
raise ValidationError(
"Parent power port ({}) must belong to the same device type".format(self.power_port)
)
def instantiate(self, device):
if self.power_port: if self.power_port:
power_port = PowerPort.objects.get(device=device, name=self.power_port.name) if self.device_type and self.power_port.device_type != self.device_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same device type"
)
if self.module_type and self.power_port.module_type != self.module_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same module type"
)
def instantiate(self, **kwargs):
if self.power_port:
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.resolve_name(kwargs.get('module')),
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
) )
@extras_features('webhooks') @extras_features('webhooks')
class InterfaceTemplate(ComponentTemplateModel): class InterfaceTemplate(ModularComponentTemplateModel):
""" """
A template for a physical data interface on a new Device. A template for a physical data interface on a new Device.
""" """
@ -242,21 +315,24 @@ class InterfaceTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ('device_type', 'name') unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, device): def instantiate(self, **kwargs):
return Interface( return Interface(
device=device, name=self.resolve_name(kwargs.get('module')),
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
) )
@extras_features('webhooks') @extras_features('webhooks')
class FrontPortTemplate(ComponentTemplateModel): class FrontPortTemplate(ModularComponentTemplateModel):
""" """
Template for a pass-through port on the front of a new Device. Template for a pass-through port on the front of a new Device.
""" """
@ -281,9 +357,10 @@ class FrontPortTemplate(ComponentTemplateModel):
) )
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', 'module_type', '_name')
unique_together = ( unique_together = (
('device_type', 'name'), ('device_type', 'name'),
('module_type', 'name'),
('rear_port', 'rear_port_position'), ('rear_port', 'rear_port_position'),
) )
@ -309,24 +386,24 @@ class FrontPortTemplate(ComponentTemplateModel):
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.resolve_name(kwargs.get('module')),
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
) )
@extras_features('webhooks') @extras_features('webhooks')
class RearPortTemplate(ComponentTemplateModel): class RearPortTemplate(ModularComponentTemplateModel):
""" """
Template for a pass-through port on the rear of a new Device. Template for a pass-through port on the rear of a new Device.
""" """
@ -345,18 +422,45 @@ class RearPortTemplate(ComponentTemplateModel):
] ]
) )
class Meta:
ordering = ('device_type', 'module_type', '_name')
unique_together = (
('device_type', 'name'),
('module_type', 'name'),
)
def instantiate(self, **kwargs):
return RearPort(
name=self.resolve_name(kwargs.get('module')),
label=self.label,
type=self.type,
color=self.color,
positions=self.positions,
**kwargs
)
@extras_features('webhooks')
class ModuleBayTemplate(ComponentTemplateModel):
"""
A template for a ModuleBay to be created for a new parent Device.
"""
position = models.CharField(
max_length=30,
blank=True,
help_text='Identifier to reference when renaming installed components'
)
class Meta: class Meta:
ordering = ('device_type', '_name') ordering = ('device_type', '_name')
unique_together = ('device_type', 'name') unique_together = ('device_type', 'name')
def instantiate(self, device): def instantiate(self, device):
return RearPort( return ModuleBay(
device=device, device=device,
name=self.name, name=self.name,
label=self.label, label=self.label,
type=self.type, position=self.position
color=self.color,
positions=self.positions
) )

View File

@ -30,6 +30,7 @@ __all__ = (
'FrontPort', 'FrontPort',
'Interface', 'Interface',
'InventoryItem', 'InventoryItem',
'ModuleBay',
'PathEndpoint', 'PathEndpoint',
'PowerOutlet', 'PowerOutlet',
'PowerPort', 'PowerPort',
@ -86,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
@ -229,11 +243,11 @@ class PathEndpoint(models.Model):
# #
# Console ports # Console components
# #
@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.
""" """
@ -260,12 +274,8 @@ class ConsolePort(ComponentModel, LinkTermination, PathEndpoint):
return reverse('dcim:consoleport', kwargs={'pk': self.pk}) return reverse('dcim:consoleport', kwargs={'pk': self.pk})
#
# Console server ports
#
@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.
""" """
@ -293,11 +303,11 @@ class ConsoleServerPort(ComponentModel, LinkTermination, PathEndpoint):
# #
# Power ports # Power components
# #
@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.
""" """
@ -389,12 +399,8 @@ class PowerPort(ComponentModel, LinkTermination, PathEndpoint):
} }
#
# Power outlets
#
@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.
""" """
@ -509,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.
""" """
@ -772,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.
""" """
@ -826,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.
""" """
@ -866,9 +872,30 @@ class RearPort(ComponentModel, LinkTermination):
# #
# Device bays # Bays
# #
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ModuleBay(ComponentModel):
"""
An empty space within a Device which can house a child device
"""
position = models.CharField(
max_length=30,
blank=True,
help_text='Identifier to reference when renaming installed components'
)
clone_fields = ['device']
class Meta:
ordering = ('device', '_name')
unique_together = ('device', 'name')
def get_absolute_url(self):
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceBay(ComponentModel): class DeviceBay(ComponentModel):
""" """

View File

@ -26,6 +26,8 @@ __all__ = (
'DeviceRole', 'DeviceRole',
'DeviceType', 'DeviceType',
'Manufacturer', 'Manufacturer',
'Module',
'ModuleType',
'Platform', 'Platform',
'VirtualChassis', 'VirtualChassis',
) )
@ -253,6 +255,15 @@ class DeviceType(PrimaryModel):
} }
for c in self.rearporttemplates.all() for c in self.rearporttemplates.all()
] ]
if self.modulebaytemplates.exists():
data['module-bays'] = [
{
'name': c.name,
'label': c.label,
'description': c.description,
}
for c in self.modulebaytemplates.all()
]
if self.devicebaytemplates.exists(): if self.devicebaytemplates.exists():
data['device-bays'] = [ data['device-bays'] = [
{ {
@ -342,6 +353,136 @@ class DeviceType(PrimaryModel):
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ModuleType(PrimaryModel):
"""
A ModuleType represents a hardware element that can be installed within a device and which houses additional
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
DeviceType, each ModuleType can have console, power, interface, and pass-through port templates assigned to it. It
cannot, however house device bays or module bays.
"""
manufacturer = models.ForeignKey(
to='dcim.Manufacturer',
on_delete=models.PROTECT,
related_name='module_types'
)
model = models.CharField(
max_length=100
)
part_number = models.CharField(
max_length=50,
blank=True,
help_text='Discrete part number (optional)'
)
comments = models.TextField(
blank=True
)
clone_fields = ('manufacturer',)
class Meta:
ordering = ('manufacturer', 'model')
unique_together = (
('manufacturer', 'model'),
)
def __str__(self):
return self.model
def get_absolute_url(self):
return reverse('dcim:moduletype', args=[self.pk])
def to_yaml(self):
data = OrderedDict((
('manufacturer', self.manufacturer.name),
('model', self.model),
('part_number', self.part_number),
('comments', self.comments),
))
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
{
'name': c.name,
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
}
for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
{
'name': c.name,
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
}
for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
{
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
}
for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
{
'name': c.name,
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
}
for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
{
'name': c.name,
'type': c.type,
'positions': c.positions,
'label': c.label,
'description': c.description,
}
for c in self.rearporttemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)
# #
# Devices # Devices
# #
@ -766,28 +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(
[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
@ -865,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,6 +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 .modules import *
from .power import * from .power import *
from .racks import * from .racks import *
from .sites import * from .sites import *

View File

@ -2,8 +2,8 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, ModuleBay,
PowerOutlet, PowerPort, RearPort, VirtualChassis, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
) )
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
@ -25,6 +25,7 @@ __all__ = (
'DeviceImportTable', 'DeviceImportTable',
'DeviceInterfaceTable', 'DeviceInterfaceTable',
'DeviceInventoryItemTable', 'DeviceInventoryItemTable',
'DeviceModuleBayTable',
'DevicePowerPortTable', 'DevicePowerPortTable',
'DevicePowerOutletTable', 'DevicePowerOutletTable',
'DeviceRearPortTable', 'DeviceRearPortTable',
@ -33,6 +34,7 @@ __all__ = (
'FrontPortTable', 'FrontPortTable',
'InterfaceTable', 'InterfaceTable',
'InventoryItemTable', 'InventoryItemTable',
'ModuleBayTable',
'PlatformTable', 'PlatformTable',
'PowerOutletTable', 'PowerOutletTable',
'PowerPortTable', 'PowerPortTable',
@ -255,6 +257,19 @@ class DeviceComponentTable(BaseTable):
order_by = ('device', 'name') order_by = ('device', 'name')
class ModularDeviceComponentTable(DeviceComponentTable):
module_bay = tables.Column(
accessor=Accessor('module__module_bay'),
linkify={
'viewname': 'dcim:device_modulebays',
'args': [Accessor('device_id')],
}
)
module = tables.Column(
linkify=True
)
class CableTerminationTable(BaseTable): class CableTerminationTable(BaseTable):
cable = tables.Column( cable = tables.Column(
linkify=True linkify=True
@ -282,7 +297,7 @@ class PathEndpointTable(CableTerminationTable):
) )
class ConsolePortTable(DeviceComponentTable, PathEndpointTable): class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_consoleports', 'viewname': 'dcim:device_consoleports',
@ -296,8 +311,8 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'link_peer', 'connection', 'tags', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@ -317,8 +332,8 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsolePort model = ConsolePort
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'link_peer', 'connection', 'tags', 'actions' 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = { row_attrs = {
@ -326,7 +341,7 @@ class DeviceConsolePortTable(ConsolePortTable):
} }
class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable): class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_consoleserverports', 'viewname': 'dcim:device_consoleserverports',
@ -340,8 +355,8 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'speed', 'description',
'cable_color', 'link_peer', 'connection', 'tags', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'speed', 'description')
@ -362,8 +377,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'speed', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'link_peer', 'connection', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions')
row_attrs = { row_attrs = {
@ -371,7 +386,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
} }
class PowerPortTable(DeviceComponentTable, PathEndpointTable): class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_powerports', 'viewname': 'dcim:device_powerports',
@ -385,8 +400,8 @@ class PowerPortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'mark_connected', 'maximum_draw', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'mark_connected',
'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'maximum_draw', 'allocated_draw', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
@ -407,8 +422,8 @@ class DevicePowerPortTable(PowerPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerPort model = PowerPort
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
@ -419,7 +434,7 @@ class DevicePowerPortTable(PowerPortTable):
} }
class PowerOutletTable(DeviceComponentTable, PathEndpointTable): class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_poweroutlets', 'viewname': 'dcim:device_poweroutlets',
@ -436,8 +451,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'description', 'power_port', 'feed_leg', 'mark_connected', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
@ -457,8 +472,8 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions', 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
@ -489,7 +504,7 @@ class BaseInterfaceTable(BaseTable):
) )
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable): class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_interfaces', 'viewname': 'dcim:device_interfaces',
@ -512,10 +527,10 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans',
'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
) )
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@ -547,10 +562,11 @@ class DeviceInterfaceTable(InterfaceTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = Interface model = Interface
fields = ( fields = (
'pk', 'id', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
'tagged_vlans', 'actions',
) )
order_by = ('name',) order_by = ('name',)
default_columns = ( default_columns = (
@ -564,7 +580,7 @@ class DeviceInterfaceTable(InterfaceTable):
} }
class FrontPortTable(DeviceComponentTable, CableTerminationTable): class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_frontports', 'viewname': 'dcim:device_frontports',
@ -585,8 +601,8 @@ class FrontPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'rear_port',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description', 'pk', 'name', 'device', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
@ -609,8 +625,8 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = FrontPort model = FrontPort
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
'cable_color', 'link_peer', 'tags', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer', 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
@ -621,7 +637,7 @@ class DeviceFrontPortTable(FrontPortTable):
} }
class RearPortTable(DeviceComponentTable, CableTerminationTable): class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={
'viewname': 'dcim:device_rearports', 'viewname': 'dcim:device_rearports',
@ -636,8 +652,8 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ( fields = (
'pk', 'id', 'name', 'device', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'color', 'positions', 'description',
'cable_color', 'link_peer', 'tags', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'tags',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'description')
@ -658,8 +674,8 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = RearPort model = RearPort
fields = ( fields = (
'pk', 'id', 'name', 'label', 'type', 'positions', 'description', 'mark_connected', 'cable', 'cable_color', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
'link_peer', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions', 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions',
@ -716,6 +732,40 @@ class DeviceDeviceBayTable(DeviceBayTable):
) )
class ModuleBayTable(DeviceComponentTable):
device = tables.Column(
linkify={
'viewname': 'dcim:device_modulebays',
'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', 'position', 'installed_module', 'description', 'tags')
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'description')
class DeviceModuleBayTable(ModuleBayTable):
actions = ButtonsColumn(
model=DeviceBay,
buttons=('edit', 'delete'),
prepend_template=MODULEBAY_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
model = ModuleBay
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
default_columns = ('pk', 'name', 'label', 'description', 'installed_module', 'actions')
class InventoryItemTable(DeviceComponentTable): class InventoryItemTable(DeviceComponentTable):
device = tables.Column( device = tables.Column(
linkify={ linkify={

View File

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

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

@ -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
@ -470,6 +470,45 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
] ]
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
model = ModuleType
brief_fields = ['display', 'id', 'manufacturer', 'model', 'url']
bulk_update_data = {
'part_number': 'ABC123',
}
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
)
Manufacturer.objects.bulk_create(manufacturers)
module_types = (
ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
ModuleType(manufacturer=manufacturers[0], model='Module Type 2'),
ModuleType(manufacturer=manufacturers[0], model='Module Type 3'),
)
ModuleType.objects.bulk_create(module_types)
cls.create_data = [
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 4',
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 5',
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Module Type 6',
},
]
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase): class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsolePortTemplate model = ConsolePortTemplate
brief_fields = ['display', 'id', 'name', 'url'] brief_fields = ['display', 'id', 'name', 'url']
@ -778,6 +817,46 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
] ]
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer,
model='Device Type 1',
slug='device-type-1',
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
)
module_bay_templates = (
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
)
ModuleBayTemplate.objects.bulk_create(module_bay_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 4',
},
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 5',
},
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 6',
},
]
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate model = DeviceBayTemplate
brief_fields = ['display', 'id', 'name', 'url'] brief_fields = ['display', 'id', 'name', 'url']
@ -1026,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']
@ -1369,6 +1509,45 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
] ]
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay
brief_fields = ['display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devicerole = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
device = Device.objects.create(device_type=device_type, device_role=devicerole, name='Device 1', site=site)
device_bays = (
ModuleBay(device=device, name='Device Bay 1'),
ModuleBay(device=device, name='Device Bay 2'),
ModuleBay(device=device, name='Device Bay 3'),
)
ModuleBay.objects.bulk_create(device_bays)
cls.create_data = [
{
'device': device.pk,
'name': 'Device Bay 4',
},
{
'device': device.pk,
'name': 'Device Bay 5',
},
{
'device': device.pk,
'name': 'Device Bay 6',
},
]
class DeviceBayTest(APIViewTestCases.APIViewTestCase): class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay model = DeviceBay
brief_fields = ['device', 'display', 'id', 'name', 'url'] brief_fields = ['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
@ -678,6 +678,10 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
)) ))
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
))
DeviceBayTemplate.objects.bulk_create(( DeviceBayTemplate.objects.bulk_create((
DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'), DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
@ -762,6 +766,116 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_bays': 'false'} params = {'device_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_module_bays(self):
params = {'module_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleType.objects.all()
filterset = ModuleTypeFilterSet
@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)
module_types = (
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1'),
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2'),
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3'),
)
ModuleType.objects.bulk_create(module_types)
# Add component templates for filtering
ConsolePortTemplate.objects.bulk_create((
ConsolePortTemplate(module_type=module_types[0], name='Console Port 1'),
ConsolePortTemplate(module_type=module_types[1], name='Console Port 2'),
))
ConsoleServerPortTemplate.objects.bulk_create((
ConsoleServerPortTemplate(module_type=module_types[0], name='Console Server Port 1'),
ConsoleServerPortTemplate(module_type=module_types[1], name='Console Server Port 2'),
))
PowerPortTemplate.objects.bulk_create((
PowerPortTemplate(module_type=module_types[0], name='Power Port 1'),
PowerPortTemplate(module_type=module_types[1], name='Power Port 2'),
))
PowerOutletTemplate.objects.bulk_create((
PowerOutletTemplate(module_type=module_types[0], name='Power Outlet 1'),
PowerOutletTemplate(module_type=module_types[1], name='Power Outlet 2'),
))
InterfaceTemplate.objects.bulk_create((
InterfaceTemplate(module_type=module_types[0], name='Interface 1'),
InterfaceTemplate(module_type=module_types[1], name='Interface 2'),
))
rear_ports = (
RearPortTemplate(module_type=module_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C),
RearPortTemplate(module_type=module_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C),
)
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create((
FrontPortTemplate(module_type=module_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
def test_model(self):
params = {'model': ['Model 1', 'Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_part_number(self):
params = {'part_number': ['Part Number 1', 'Part Number 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
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(), 2)
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_console_ports(self):
params = {'console_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'console_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_console_server_ports(self):
params = {'console_server_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'console_server_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_power_ports(self):
params = {'power_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'power_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_power_outlets(self):
params = {'power_outlets': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'power_outlets': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_interfaces(self):
params = {'interfaces': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'interfaces': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_pass_through_ports(self):
params = {'pass_through_ports': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'pass_through_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all() queryset = ConsolePortTemplate.objects.all()
@ -1036,6 +1150,38 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleBayTemplate.objects.all()
filterset = ModuleBayTemplateFilterSet
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_types = (
DeviceType(manufacturer=manufacturer, model='Model 1', slug='model-1'),
DeviceType(manufacturer=manufacturer, model='Model 2', slug='model-2'),
DeviceType(manufacturer=manufacturer, model='Model 3', slug='model-3'),
)
DeviceType.objects.bulk_create(device_types)
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3'),
))
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicetype_id(self):
device_types = DeviceType.objects.all()[:2]
params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceBayTemplate.objects.all() queryset = DeviceBayTemplate.objects.all()
filterset = DeviceBayTemplateFilterSet filterset = DeviceBayTemplateFilterSet
@ -1280,6 +1426,10 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
)) ))
ModuleBay.objects.bulk_create((
ModuleBay(device=devices[0], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
))
DeviceBay.objects.bulk_create(( DeviceBay.objects.bulk_create((
DeviceBay(device=devices[0], name='Device Bay 1'), DeviceBay(device=devices[0], name='Device Bay 1'),
DeviceBay(device=devices[1], name='Device Bay 2'), DeviceBay(device=devices[1], name='Device Bay 2'),
@ -1465,6 +1615,12 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'pass_through_ports': 'false'} params = {'pass_through_ports': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_module_bays(self):
params = {'module_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'module_bays': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_device_bays(self): def test_device_bays(self):
params = {'device_bays': 'true'} params = {'device_bays': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -1492,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
@ -2508,6 +2737,109 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ModuleBay.objects.all()
filterset = ModuleBayFilterSet
@classmethod
def setUpTestData(cls):
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for group in groups:
group.save()
sites = Site.objects.bulk_create((
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0]),
Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1]),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2]),
Site(name='Site X', slug='site-x'),
))
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()
devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
)
Device.objects.bulk_create(devices)
module_bays = (
ModuleBay(device=devices[0], name='Module Bay 1', label='A', description='First'),
ModuleBay(device=devices[1], name='Module Bay 2', label='B', description='Second'),
ModuleBay(device=devices[2], name='Module Bay 3', label='C', description='Third'),
)
ModuleBay.objects.bulk_create(module_bays)
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_label(self):
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
filterset = DeviceBayFilterSet filterset = DeviceBayFilterSet

View File

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

View File

@ -554,6 +554,19 @@ class DeviceTypeTestCase(
url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk}) url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200) self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_devicetype_modulebays(self):
devicetype = DeviceType.objects.first()
module_bays = (
ModuleBayTemplate(device_type=devicetype, name='Module Bay 1'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay 2'),
ModuleBayTemplate(device_type=devicetype, name='Module Bay 3'),
)
ModuleBayTemplate.objects.bulk_create(module_bays)
url = reverse('dcim:devicetype_modulebays', kwargs={'pk': devicetype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_devicetype_devicebays(self): def test_devicetype_devicebays(self):
devicetype = DeviceType.objects.first() devicetype = DeviceType.objects.first()
@ -578,7 +591,7 @@ model: TEST-1000
slug: test-1000 slug: test-1000
u_height: 2 u_height: 2
subdevice_role: parent subdevice_role: parent
comments: test comment comments: Test comment
console-ports: console-ports:
- name: Console Port 1 - name: Console Port 1
type: de-9 type: de-9
@ -638,6 +651,10 @@ front-ports:
- name: Front Port 3 - name: Front Port 3
type: 8p8c type: 8p8c
rear_port: Rear Port 3 rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
- name: Module Bay 2
- name: Module Bay 3
device-bays: device-bays:
- name: Device Bay 1 - name: Device Bay 1
- name: Device Bay 2 - name: Device Bay 2
@ -658,6 +675,7 @@ device-bays:
'dcim.add_interfacetemplate', 'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate', 'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate', 'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate', 'dcim.add_devicebaytemplate',
) )
@ -668,49 +686,53 @@ device-bays:
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True) response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200) self.assertHttpStatus(response, 200)
dt = DeviceType.objects.get(model='TEST-1000') device_type = DeviceType.objects.get(model='TEST-1000')
self.assertEqual(dt.comments, 'test comment') self.assertEqual(device_type.comments, 'Test comment')
# Verify all of the components were created # Verify all of the components were created
self.assertEqual(dt.consoleporttemplates.count(), 3) self.assertEqual(device_type.consoleporttemplates.count(), 3)
cp1 = ConsolePortTemplate.objects.first() cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1') self.assertEqual(cp1.name, 'Console Port 1')
self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9) self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
self.assertEqual(dt.consoleserverporttemplates.count(), 3) self.assertEqual(device_type.consoleserverporttemplates.count(), 3)
csp1 = ConsoleServerPortTemplate.objects.first() csp1 = ConsoleServerPortTemplate.objects.first()
self.assertEqual(csp1.name, 'Console Server Port 1') self.assertEqual(csp1.name, 'Console Server Port 1')
self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45) self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
self.assertEqual(dt.powerporttemplates.count(), 3) self.assertEqual(device_type.powerporttemplates.count(), 3)
pp1 = PowerPortTemplate.objects.first() pp1 = PowerPortTemplate.objects.first()
self.assertEqual(pp1.name, 'Power Port 1') self.assertEqual(pp1.name, 'Power Port 1')
self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14) self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
self.assertEqual(dt.poweroutlettemplates.count(), 3) self.assertEqual(device_type.poweroutlettemplates.count(), 3)
po1 = PowerOutletTemplate.objects.first() po1 = PowerOutletTemplate.objects.first()
self.assertEqual(po1.name, 'Power Outlet 1') self.assertEqual(po1.name, 'Power Outlet 1')
self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13) self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
self.assertEqual(po1.power_port, pp1) self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A) self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(dt.interfacetemplates.count(), 3) self.assertEqual(device_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first() iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1') self.assertEqual(iface1.name, 'Interface 1')
self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED) self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
self.assertTrue(iface1.mgmt_only) self.assertTrue(iface1.mgmt_only)
self.assertEqual(dt.rearporttemplates.count(), 3) self.assertEqual(device_type.rearporttemplates.count(), 3)
rp1 = RearPortTemplate.objects.first() rp1 = RearPortTemplate.objects.first()
self.assertEqual(rp1.name, 'Rear Port 1') self.assertEqual(rp1.name, 'Rear Port 1')
self.assertEqual(dt.frontporttemplates.count(), 3) self.assertEqual(device_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first() fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1') self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1) self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1) self.assertEqual(fp1.rear_port_position, 1)
self.assertEqual(dt.devicebaytemplates.count(), 3) self.assertEqual(device_type.modulebaytemplates.count(), 3)
db1 = ModuleBayTemplate.objects.first()
self.assertEqual(db1.name, 'Module Bay 1')
self.assertEqual(device_type.devicebaytemplates.count(), 3)
db1 = DeviceBayTemplate.objects.first() db1 = DeviceBayTemplate.objects.first()
self.assertEqual(db1.name, 'Device Bay 1') self.assertEqual(db1.name, 'Device Bay 1')
@ -719,7 +741,7 @@ device-bays:
self.add_permissions('dcim.view_devicetype') self.add_permissions('dcim.view_devicetype')
# Test default YAML export # Test default YAML export
response = self.client.get('{}?export'.format(url)) response = self.client.get(f'{url}?export')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
self.assertEqual(len(data), 3) self.assertEqual(len(data), 3)
@ -732,6 +754,300 @@ device-bays:
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8') self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
# TODO: Change base class to PrimaryObjectViewTestCase
# Blocked by absence of bulk import view for ModuleTypes
class ModuleTypeTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.GetObjectChangelogViewTestCase,
ViewTestCases.CreateObjectViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
model = ModuleType
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2')
)
Manufacturer.objects.bulk_create(manufacturers)
ModuleType.objects.bulk_create([
ModuleType(model='Module Type 1', manufacturer=manufacturers[0]),
ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'manufacturer': manufacturers[1].pk,
'model': 'Device Type X',
'part_number': '123ABC',
'comments': 'Some comments',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'manufacturer': manufacturers[1].pk,
'part_number': '456DEF',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_consoleports(self):
moduletype = ModuleType.objects.first()
console_ports = (
ConsolePortTemplate(module_type=moduletype, name='Console Port 1'),
ConsolePortTemplate(module_type=moduletype, name='Console Port 2'),
ConsolePortTemplate(module_type=moduletype, name='Console Port 3'),
)
ConsolePortTemplate.objects.bulk_create(console_ports)
url = reverse('dcim:moduletype_consoleports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_consoleserverports(self):
moduletype = ModuleType.objects.first()
console_server_ports = (
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 1'),
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 2'),
ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 3'),
)
ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)
url = reverse('dcim:moduletype_consoleserverports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_powerports(self):
moduletype = ModuleType.objects.first()
power_ports = (
PowerPortTemplate(module_type=moduletype, name='Power Port 1'),
PowerPortTemplate(module_type=moduletype, name='Power Port 2'),
PowerPortTemplate(module_type=moduletype, name='Power Port 3'),
)
PowerPortTemplate.objects.bulk_create(power_ports)
url = reverse('dcim:moduletype_powerports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_poweroutlets(self):
moduletype = ModuleType.objects.first()
power_outlets = (
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 1'),
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 2'),
PowerOutletTemplate(module_type=moduletype, name='Power Outlet 3'),
)
PowerOutletTemplate.objects.bulk_create(power_outlets)
url = reverse('dcim:moduletype_poweroutlets', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_interfaces(self):
moduletype = ModuleType.objects.first()
interfaces = (
InterfaceTemplate(module_type=moduletype, name='Interface 1'),
InterfaceTemplate(module_type=moduletype, name='Interface 2'),
InterfaceTemplate(module_type=moduletype, name='Interface 3'),
)
InterfaceTemplate.objects.bulk_create(interfaces)
url = reverse('dcim:moduletype_interfaces', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_rearports(self):
moduletype = ModuleType.objects.first()
rear_ports = (
RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
)
RearPortTemplate.objects.bulk_create(rear_ports)
url = reverse('dcim:moduletype_rearports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_moduletype_frontports(self):
moduletype = ModuleType.objects.first()
rear_ports = (
RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
)
RearPortTemplate.objects.bulk_create(rear_ports)
front_ports = (
FrontPortTemplate(module_type=moduletype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1),
FrontPortTemplate(module_type=moduletype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1),
FrontPortTemplate(module_type=moduletype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1),
)
FrontPortTemplate.objects.bulk_create(front_ports)
url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_objects(self):
"""
Custom import test for YAML-based imports (versus CSV)
"""
IMPORT_DATA = """
manufacturer: Generic
model: TEST-1000
comments: Test comment
console-ports:
- name: Console Port 1
type: de-9
- name: Console Port 2
type: de-9
- name: Console Port 3
type: de-9
console-server-ports:
- name: Console Server Port 1
type: rj-45
- name: Console Server Port 2
type: rj-45
- name: Console Server Port 3
type: rj-45
power-ports:
- name: Power Port 1
type: iec-60320-c14
- name: Power Port 2
type: iec-60320-c14
- name: Power Port 3
type: iec-60320-c14
power-outlets:
- name: Power Outlet 1
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
- name: Power Outlet 2
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
- name: Power Outlet 3
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
interfaces:
- name: Interface 1
type: 1000base-t
mgmt_only: true
- name: Interface 2
type: 1000base-t
- name: Interface 3
type: 1000base-t
rear-ports:
- name: Rear Port 1
type: 8p8c
- name: Rear Port 2
type: 8p8c
- name: Rear Port 3
type: 8p8c
front-ports:
- name: Front Port 1
type: 8p8c
rear_port: Rear Port 1
- name: Front Port 2
type: 8p8c
rear_port: Rear Port 2
- name: Front Port 3
type: 8p8c
rear_port: Rear Port 3
"""
# Create the manufacturer
Manufacturer(name='Generic', slug='generic').save()
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_moduletype',
'dcim.add_moduletype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
)
form_data = {
'data': IMPORT_DATA,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:moduletype_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
module_type = ModuleType.objects.get(model='TEST-1000')
self.assertEqual(module_type.comments, 'Test comment')
# Verify all the components were created
self.assertEqual(module_type.consoleporttemplates.count(), 3)
cp1 = ConsolePortTemplate.objects.first()
self.assertEqual(cp1.name, 'Console Port 1')
self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)
self.assertEqual(module_type.consoleserverporttemplates.count(), 3)
csp1 = ConsoleServerPortTemplate.objects.first()
self.assertEqual(csp1.name, 'Console Server Port 1')
self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)
self.assertEqual(module_type.powerporttemplates.count(), 3)
pp1 = PowerPortTemplate.objects.first()
self.assertEqual(pp1.name, 'Power Port 1')
self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)
self.assertEqual(module_type.poweroutlettemplates.count(), 3)
po1 = PowerOutletTemplate.objects.first()
self.assertEqual(po1.name, 'Power Outlet 1')
self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(module_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
self.assertTrue(iface1.mgmt_only)
self.assertEqual(module_type.rearporttemplates.count(), 3)
rp1 = RearPortTemplate.objects.first()
self.assertEqual(rp1.name, 'Rear Port 1')
self.assertEqual(module_type.frontporttemplates.count(), 3)
fp1 = FrontPortTemplate.objects.first()
self.assertEqual(fp1.name, 'Front Port 1')
self.assertEqual(fp1.rear_port, rp1)
self.assertEqual(fp1.rear_port_position, 1)
def test_export_objects(self):
url = reverse('dcim:moduletype_list')
self.add_permissions('dcim.view_moduletype')
# Test default YAML export
response = self.client.get(f'{url}?export')
self.assertEqual(response.status_code, 200)
data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
self.assertEqual(len(data), 3)
self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
self.assertEqual(data[0]['model'], 'Module Type 1')
# Test table-based export
response = self.client.get(f'{url}?export=table')
self.assertHttpStatus(response, 200)
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
# #
# DeviceType components # DeviceType components
# #
@ -1011,6 +1327,39 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
} }
class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ModuleBayTemplate
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
)
DeviceType.objects.bulk_create(devicetypes)
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'),
))
cls.form_data = {
'device_type': devicetypes[1].pk,
'name': 'Module Bay Template X',
}
cls.bulk_create_data = {
'device_type': devicetypes[1].pk,
'name_pattern': 'Module Bay Template [4-6]',
}
cls.bulk_edit_data = {
'description': 'Foo bar',
}
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = DeviceBayTemplate model = DeviceBayTemplate
@ -1307,6 +1656,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('dcim:device_frontports', kwargs={'pk': device.pk}) url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200) self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_device_modulebays(self):
device = Device.objects.first()
device_bays = (
ModuleBay(device=device, name='Module Bay 1'),
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
)
ModuleBay.objects.bulk_create(device_bays)
url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_device_devicebays(self): def test_device_devicebays(self):
device = Device.objects.first() device = Device.objects.first()
@ -1335,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
@ -1807,6 +2238,47 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
self.assertHttpStatus(response, 200) self.assertHttpStatus(response, 200)
class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ModuleBay
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
ModuleBay.objects.bulk_create([
ModuleBay(device=device, name='Module Bay 1'),
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': device.pk,
'name': 'Module Bay X',
'description': 'A device bay',
'tags': [t.pk for t in tags],
}
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Module Bay [4-6]',
'description': 'A module bay',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'description': 'New description',
}
cls.csv_data = (
"device,name",
"Device 1,Module Bay 4",
"Device 1,Module Bay 5",
"Device 1,Module Bay 6",
)
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay model = DeviceBay

View File

@ -113,12 +113,32 @@ urlpatterns = [
path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'), path('device-types/<int:pk>/interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'),
path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'), path('device-types/<int:pk>/front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'),
path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'), path('device-types/<int:pk>/rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'),
path('device-types/<int:pk>/module-bays/', views.DeviceTypeModuleBaysView.as_view(), name='devicetype_modulebays'),
path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'), path('device-types/<int:pk>/device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'),
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}), path('device-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='devicetype_journal', kwargs={'model': DeviceType}),
# Module types
path('module-types/', views.ModuleTypeListView.as_view(), name='moduletype_list'),
path('module-types/add/', views.ModuleTypeEditView.as_view(), name='moduletype_add'),
path('module-types/import/', views.ModuleTypeImportView.as_view(), name='moduletype_import'),
path('module-types/edit/', views.ModuleTypeBulkEditView.as_view(), name='moduletype_bulk_edit'),
path('module-types/delete/', views.ModuleTypeBulkDeleteView.as_view(), name='moduletype_bulk_delete'),
path('module-types/<int:pk>/', views.ModuleTypeView.as_view(), name='moduletype'),
path('module-types/<int:pk>/console-ports/', views.ModuleTypeConsolePortsView.as_view(), name='moduletype_consoleports'),
path('module-types/<int:pk>/console-server-ports/', views.ModuleTypeConsoleServerPortsView.as_view(), name='moduletype_consoleserverports'),
path('module-types/<int:pk>/power-ports/', views.ModuleTypePowerPortsView.as_view(), name='moduletype_powerports'),
path('module-types/<int:pk>/power-outlets/', views.ModuleTypePowerOutletsView.as_view(), name='moduletype_poweroutlets'),
path('module-types/<int:pk>/interfaces/', views.ModuleTypeInterfacesView.as_view(), name='moduletype_interfaces'),
path('module-types/<int:pk>/front-ports/', views.ModuleTypeFrontPortsView.as_view(), name='moduletype_frontports'),
path('module-types/<int:pk>/rear-ports/', views.ModuleTypeRearPortsView.as_view(), name='moduletype_rearports'),
path('module-types/<int:pk>/edit/', views.ModuleTypeEditView.as_view(), name='moduletype_edit'),
path('module-types/<int:pk>/delete/', views.ModuleTypeDeleteView.as_view(), name='moduletype_delete'),
path('module-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='moduletype_changelog', kwargs={'model': ModuleType}),
path('module-types/<int:pk>/journal/', ObjectJournalView.as_view(), name='moduletype_journal', kwargs={'model': ModuleType}),
# Console port templates # Console port templates
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'), path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'), path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
@ -183,6 +203,14 @@ urlpatterns = [
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'), path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
# Device bay templates
path('module-bay-templates/add/', views.ModuleBayTemplateCreateView.as_view(), name='modulebaytemplate_add'),
path('module-bay-templates/edit/', views.ModuleBayTemplateBulkEditView.as_view(), name='modulebaytemplate_bulk_edit'),
path('module-bay-templates/rename/', views.ModuleBayTemplateBulkRenameView.as_view(), name='modulebaytemplate_bulk_rename'),
path('module-bay-templates/delete/', views.ModuleBayTemplateBulkDeleteView.as_view(), name='modulebaytemplate_bulk_delete'),
path('module-bay-templates/<int:pk>/edit/', views.ModuleBayTemplateEditView.as_view(), name='modulebaytemplate_edit'),
path('module-bay-templates/<int:pk>/delete/', views.ModuleBayTemplateDeleteView.as_view(), name='modulebaytemplate_delete'),
# Device roles # Device roles
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'), path('device-roles/add/', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
@ -222,15 +250,28 @@ urlpatterns = [
path('devices/<int:pk>/interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'), path('devices/<int:pk>/interfaces/', views.DeviceInterfacesView.as_view(), name='device_interfaces'),
path('devices/<int:pk>/front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'), path('devices/<int:pk>/front-ports/', views.DeviceFrontPortsView.as_view(), name='device_frontports'),
path('devices/<int:pk>/rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'), path('devices/<int:pk>/rear-ports/', views.DeviceRearPortsView.as_view(), name='device_rearports'),
path('devices/<int:pk>/module-bays/', views.DeviceModuleBaysView.as_view(), name='device_modulebays'),
path('devices/<int:pk>/device-bays/', views.DeviceDeviceBaysView.as_view(), name='device_devicebays'), path('devices/<int:pk>/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'),
@ -343,6 +384,19 @@ urlpatterns = [
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Module bays
path('module-bays/', views.ModuleBayListView.as_view(), name='modulebay_list'),
path('module-bays/add/', views.ModuleBayCreateView.as_view(), name='modulebay_add'),
path('module-bays/import/', views.ModuleBayBulkImportView.as_view(), name='modulebay_import'),
path('module-bays/edit/', views.ModuleBayBulkEditView.as_view(), name='modulebay_bulk_edit'),
path('module-bays/rename/', views.ModuleBayBulkRenameView.as_view(), name='modulebay_bulk_rename'),
path('module-bays/delete/', views.ModuleBayBulkDeleteView.as_view(), name='modulebay_bulk_delete'),
path('module-bays/<int:pk>/', views.ModuleBayView.as_view(), name='modulebay'),
path('module-bays/<int:pk>/edit/', views.ModuleBayEditView.as_view(), name='modulebay_edit'),
path('module-bays/<int:pk>/delete/', views.ModuleBayDeleteView.as_view(), name='modulebay_delete'),
path('module-bays/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='modulebay_changelog', kwargs={'model': ModuleBay}),
path('devices/module-bays/add/', views.DeviceBulkAddModuleBayView.as_view(), name='device_bulk_add_modulebay'),
# Device bays # Device bays
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_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,9 +30,9 @@ 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, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, InventoryItem, Manufacturer, Module, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
SiteGroup, VirtualChassis, RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
) )
@ -56,6 +56,14 @@ class DeviceTypeComponentsView(DeviceComponentsView):
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent) return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
class ModuleTypeComponentsView(DeviceComponentsView):
queryset = ModuleType.objects.all()
template_name = 'dcim/moduletype/component_templates.html'
def get_children(self, request, parent):
return self.child_model.objects.restrict(request.user, 'view').filter(module_type=parent)
class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
""" """
An extendable view for disconnection console/power/interface components in bulk. An extendable view for disconnection console/power/interface components in bulk.
@ -836,6 +844,12 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView):
filterset = filtersets.RearPortTemplateFilterSet filterset = filtersets.RearPortTemplateFilterSet
class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
child_model = ModuleBayTemplate
table = tables.ModuleBayTemplateTable
filterset = filtersets.ModuleBayTemplateFilterSet
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
child_model = DeviceBayTemplate child_model = DeviceBayTemplate
table = tables.DeviceBayTemplateTable table = tables.DeviceBayTemplateTable
@ -861,6 +875,7 @@ class DeviceTypeImportView(generic.ObjectImportView):
'dcim.add_interfacetemplate', 'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate', 'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate', 'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate', 'dcim.add_devicebaytemplate',
] ]
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
@ -873,9 +888,14 @@ class DeviceTypeImportView(generic.ObjectImportView):
('interfaces', forms.InterfaceTemplateImportForm), ('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm), ('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm), ('front-ports', forms.FrontPortTemplateImportForm),
('module-bays', forms.ModuleBayTemplateImportForm),
('device-bays', forms.DeviceBayTemplateImportForm), ('device-bays', forms.DeviceBayTemplateImportForm),
)) ))
def prep_related_object_data(self, parent, data):
data.update({'device_type': parent})
return data
class DeviceTypeBulkEditView(generic.BulkEditView): class DeviceTypeBulkEditView(generic.BulkEditView):
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
@ -894,6 +914,127 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceTypeTable table = tables.DeviceTypeTable
#
# Module types
#
class ModuleTypeListView(generic.ObjectListView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
filterset_form = forms.ModuleTypeFilterForm
table = tables.ModuleTypeTable
class ModuleTypeView(generic.ObjectView):
queryset = ModuleType.objects.prefetch_related('manufacturer')
def get_extra_context(self, request, instance):
# instance_count = Module.objects.restrict(request.user).filter(device_type=instance).count()
return {
# 'instance_count': instance_count,
'active_tab': 'moduletype',
}
class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet
class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet
class ModuleTypePowerPortsView(ModuleTypeComponentsView):
child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet
class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet
class ModuleTypeInterfacesView(ModuleTypeComponentsView):
child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet
class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet
class ModuleTypeRearPortsView(ModuleTypeComponentsView):
child_model = RearPortTemplate
table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet
class ModuleTypeEditView(generic.ObjectEditView):
queryset = ModuleType.objects.all()
model_form = forms.ModuleTypeForm
class ModuleTypeDeleteView(generic.ObjectDeleteView):
queryset = ModuleType.objects.all()
class ModuleTypeImportView(generic.ObjectImportView):
additional_permissions = [
'dcim.add_moduletype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
]
queryset = ModuleType.objects.all()
model_form = forms.ModuleTypeImportForm
related_object_forms = OrderedDict((
('console-ports', forms.ConsolePortTemplateImportForm),
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
('power-ports', forms.PowerPortTemplateImportForm),
('power-outlets', forms.PowerOutletTemplateImportForm),
('interfaces', forms.InterfaceTemplateImportForm),
('rear-ports', forms.RearPortTemplateImportForm),
('front-ports', forms.FrontPortTemplateImportForm),
))
def prep_related_object_data(self, parent, data):
data.update({'module_type': parent})
return data
class ModuleTypeBulkEditView(generic.BulkEditView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
form = forms.ModuleTypeBulkEditForm
class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleType.objects.prefetch_related('manufacturer').annotate(
# instance_count=count_related(Module, 'module_type')
)
filterset = filtersets.ModuleTypeFilterSet
table = tables.ModuleTypeTable
# #
# Console port templates # Console port templates
# #
@ -1132,6 +1273,40 @@ class RearPortTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.RearPortTemplateTable table = tables.RearPortTemplateTable
#
# Module bay templates
#
class ModuleBayTemplateCreateView(generic.ComponentCreateView):
queryset = ModuleBayTemplate.objects.all()
form = forms.ModuleBayTemplateCreateForm
model_form = forms.ModuleBayTemplateForm
class ModuleBayTemplateEditView(generic.ObjectEditView):
queryset = ModuleBayTemplate.objects.all()
model_form = forms.ModuleBayTemplateForm
class ModuleBayTemplateDeleteView(generic.ObjectDeleteView):
queryset = ModuleBayTemplate.objects.all()
class ModuleBayTemplateBulkEditView(generic.BulkEditView):
queryset = ModuleBayTemplate.objects.all()
table = tables.ModuleBayTemplateTable
form = forms.ModuleBayTemplateBulkEditForm
class ModuleBayTemplateBulkRenameView(generic.BulkRenameView):
queryset = ModuleBayTemplate.objects.all()
class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleBayTemplate.objects.all()
table = tables.ModuleBayTemplateTable
# #
# Device bay templates # Device bay templates
# #
@ -1388,6 +1563,13 @@ class DeviceRearPortsView(DeviceComponentsView):
template_name = 'dcim/device/rearports.html' template_name = 'dcim/device/rearports.html'
class DeviceModuleBaysView(DeviceComponentsView):
child_model = ModuleBay
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html'
class DeviceDeviceBaysView(DeviceComponentsView): class DeviceDeviceBaysView(DeviceComponentsView):
child_model = DeviceBay child_model = DeviceBay
table = tables.DeviceDeviceBayTable table = tables.DeviceDeviceBayTable
@ -1447,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
@ -1503,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
# #
@ -1978,6 +2195,61 @@ class RearPortBulkDeleteView(generic.BulkDeleteView):
table = tables.RearPortTable table = tables.RearPortTable
#
# Module bays
#
class ModuleBayListView(generic.ObjectListView):
queryset = ModuleBay.objects.select_related('installed_module__module_type')
filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable
action_buttons = ('import', 'export')
class ModuleBayView(generic.ObjectView):
queryset = ModuleBay.objects.all()
class ModuleBayCreateView(generic.ComponentCreateView):
queryset = ModuleBay.objects.all()
form = forms.ModuleBayCreateForm
model_form = forms.ModuleBayForm
class ModuleBayEditView(generic.ObjectEditView):
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayForm
template_name = 'dcim/device_component_edit.html'
class ModuleBayDeleteView(generic.ObjectDeleteView):
queryset = ModuleBay.objects.all()
class ModuleBayBulkImportView(generic.BulkImportView):
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayCSVForm
table = tables.ModuleBayTable
class ModuleBayBulkEditView(generic.BulkEditView):
queryset = ModuleBay.objects.all()
filterset = filtersets.ModuleBayFilterSet
table = tables.ModuleBayTable
form = forms.ModuleBayBulkEditForm
class ModuleBayBulkRenameView(generic.BulkRenameView):
queryset = ModuleBay.objects.all()
class ModuleBayBulkDeleteView(generic.BulkDeleteView):
queryset = ModuleBay.objects.all()
filterset = filtersets.ModuleBayFilterSet
table = tables.ModuleBayTable
# #
# Device bays # Device bays
# #
@ -2234,6 +2506,17 @@ class DeviceBulkAddRearPortView(generic.BulkComponentCreateView):
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'
form = forms.ModuleBayBulkCreateForm
queryset = ModuleBay.objects.all()
model_form = forms.ModuleBayForm
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'

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'),
@ -148,6 +149,7 @@ DEVICES_MENU = Menu(
label='Device Types', label='Device Types',
items=( items=(
get_model_item('dcim', 'devicetype', 'Device Types'), get_model_item('dcim', 'devicetype', 'Device Types'),
get_model_item('dcim', 'moduletype', 'Module Types'),
get_model_item('dcim', 'manufacturer', 'Manufacturers'), get_model_item('dcim', 'manufacturer', 'Manufacturers'),
), ),
), ),
@ -161,6 +163,7 @@ DEVICES_MENU = Menu(
get_model_item('dcim', 'consoleserverport', 'Console Server Ports', actions=['import']), get_model_item('dcim', 'consoleserverport', 'Console Server Ports', actions=['import']),
get_model_item('dcim', 'powerport', 'Power Ports', actions=['import']), get_model_item('dcim', 'powerport', 'Power Ports', actions=['import']),
get_model_item('dcim', 'poweroutlet', 'Power Outlets', actions=['import']), get_model_item('dcim', 'poweroutlet', 'Power Outlets', actions=['import']),
get_model_item('dcim', 'modulebay', 'Module Bays', actions=['import']),
get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']), get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']), get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
), ),

View File

@ -319,6 +319,13 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add') return get_permission_for_model(self.queryset.model, 'add')
def prep_related_object_data(self, parent, data):
"""
Hook to modify the data for related objects before it's passed to the related object form (for example, to
assign a parent object).
"""
return data
def _create_object(self, model_form): def _create_object(self, model_form):
# Save the primary object # Save the primary object
@ -333,8 +340,8 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
related_obj_pks = [] related_obj_pks = []
for i, rel_obj_data in enumerate(model_form.data.get(field_name, list())): for i, rel_obj_data in enumerate(model_form.data.get(field_name, list())):
rel_obj_data = self.prep_related_object_data(obj, rel_obj_data)
f = related_object_form(obj, rel_obj_data) f = related_object_form(rel_obj_data)
for subfield_name, field in f.fields.items(): for subfield_name, field in f.fields.items():
if subfield_name not in rel_obj_data and hasattr(field, 'initial'): if subfield_name not in rel_obj_data and hasattr(field, 'initial'):

View File

@ -69,6 +69,13 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %}
<li>
<a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">
Module Bays
</a>
</li>
{% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<li> <li>
<a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"> <a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">
@ -95,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">
@ -151,14 +174,6 @@
{% endif %} {% endif %}
{% endwith %} {% 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

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

View File

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

View File

@ -38,6 +38,9 @@
{% if perms.dcim.add_rearporttemplate %} {% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_modulebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays">Module Bays</a></li>
{% endif %}
{% if perms.dcim.add_devicebaytemplate %} {% if perms.dcim.add_devicebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li> <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
{% endif %} {% endif %}
@ -53,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">
@ -108,12 +127,4 @@
</li> </li>
{% endif %} {% endif %}
{% endwith %} {% 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 %}

View File

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

View File

@ -0,0 +1,48 @@
{% extends 'dcim/moduletype/base.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Module Type</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>Manufacturer</td>
<td><a href="{{ object.manufacturer.get_absolute_url }}">{{ object.manufacturer }}</a></td>
</tr>
<tr>
<td>Model Name</td>
<td>{{ object.model }}</td>
</tr>
<tr>
<td>Part Number</td>
<td>{{ object.part_number|placeholder }}</td>
</tr>
{% comment %}
<tr>
<td>Instances</td>
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
</tr>
{% endcomment %}
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,108 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>
{% endblock %}
{% block extra_controls %}
{% if perms.dcim.change_devicetype %}
<div class="dropdown">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
{% endif %}
{% if perms.dcim.add_powerporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
{% endif %}
{% if perms.dcim.add_interfacetemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
{% endif %}
{% if perms.dcim.add_frontporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
{% endif %}
{% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block tab_items %}
<li role="presentation" class="nav-item">
<a href="{% url 'dcim:moduletype' pk=object.pk %}" class="nav-link{% if active_tab == 'moduletype' %} active{% endif %}">
Module Type
</a>
</li>
{% with interface_count=object.interfacetemplates.count %}
{% if interface_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'interface-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with frontport_count=object.frontporttemplates.count %}
{% if frontport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'front-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with rearport_count=object.rearporttemplates.count %}
{% if rearport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'rear-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleport_count=object.consoleporttemplates.count %}
{% if consoleport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with consoleserverport_count=object.consoleserverporttemplates.count %}
{% if consoleserverport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'console-server-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with powerport_count=object.powerporttemplates.count %}
{% if powerport_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-port-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
</li>
{% endif %}
{% endwith %}
{% with poweroutlet_count=object.poweroutlettemplates.count %}
{% if poweroutlet_count %}
<li role="presentation" class="nav-item">
<a class="nav-link {% if active_tab == 'power-outlet-templates' %} active{% endif %}" href="{% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
</li>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'dcim/moduletype/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% block content %}
{% if perms.dcim.change_moduletype %}
<form method="post">
{% csrf_token %}
<div class="card">
<h5 class="card-header">{{ title }}</h5>
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
<div class="card-footer noprint">
{% if table.rows %}
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> Rename
</button>
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</button>
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
<div class="float-end">
<a href="{% url table.Meta.model|viewname:"add" %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add {{ title }}
</a>
</div>
<div class="clearfix"></div>
</div>
</div>
</form>
{% else %}
<div class="card">
<h5 class="card-header">{{ title }}</h5>
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
{% endif %}
{% endblock content %}