diff --git a/docs/models/dcim/devicebay.md b/docs/models/dcim/devicebay.md
index 5bbb125f8..9b19aee1e 100644
--- a/docs/models/dcim/devicebay.md
+++ b/docs/models/dcim/devicebay.md
@@ -23,3 +23,8 @@ The device bay's name. Must be unique to the parent device.
### Label
An alternative physical label identifying the device bay.
+
+### Enabled
+
+Whether this device bay is enabled. Disabled device bays are not available for installation.
+
diff --git a/docs/models/dcim/modulebay.md b/docs/models/dcim/modulebay.md
index 494012a7b..d42828b83 100644
--- a/docs/models/dcim/modulebay.md
+++ b/docs/models/dcim/modulebay.md
@@ -1,6 +1,6 @@
# Module Bays
-Module bays represent a space or slot within a device in which a field-replaceable [module](./module.md) 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.
+Module bays represent a space or slot within a device in which a field-replaceable [module](./module.md) 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.
!!! note
If you need to model child devices rather than modules, use a [device bay](./devicebay.md) instead.
@@ -29,3 +29,8 @@ An alternative physical label identifying the module bay.
### Position
The numeric position in which this module bay is situated. For example, this would be the number assigned to a slot within a chassis-based switch.
+
+### Enabled
+
+Whether this module bay is enabled. Disabled module bays are not available for installation.
+
diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py
index 09a850ad5..60e624a71 100644
--- a/netbox/dcim/api/serializers_/device_components.py
+++ b/netbox/dcim/api/serializers_/device_components.py
@@ -423,27 +423,29 @@ class ModuleBaySerializer(OwnerMixin, NetBoxModelSerializer):
required=False,
allow_null=True
)
+ _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = ModuleBay
fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
- 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'position', 'enabled',
+ 'description', 'installed_module', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
- brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
+ brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'enabled', 'description', '_occupied')
class DeviceBaySerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
+ _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta:
model = DeviceBay
fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
- 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'enabled', 'description',
+ 'installed_device', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
- brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'enabled', 'description', '_occupied',)
class InventoryItemSerializer(OwnerMixin, NetBoxModelSerializer):
diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py
index 147bc1701..b9b9c95ee 100644
--- a/netbox/dcim/api/serializers_/devicetype_components.py
+++ b/netbox/dcim/api/serializers_/devicetype_components.py
@@ -317,10 +317,10 @@ class ModuleBayTemplateSerializer(ComponentTemplateSerializer):
class Meta:
model = ModuleBayTemplate
fields = [
- 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'position', 'description',
+ 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description',
'created', 'last_updated',
]
- brief_fields = ('id', 'url', 'display', 'name', 'description')
+ brief_fields = ('id', 'url', 'display', 'name', 'enabled', 'description')
class DeviceBayTemplateSerializer(ComponentTemplateSerializer):
@@ -331,10 +331,10 @@ class DeviceBayTemplateSerializer(ComponentTemplateSerializer):
class Meta:
model = DeviceBayTemplate
fields = [
- 'id', 'url', 'display', 'device_type', 'name', 'label', 'description',
+ 'id', 'url', 'display', 'device_type', 'name', 'label', 'enabled', 'description',
'created', 'last_updated'
]
- brief_fields = ('id', 'url', 'display', 'name', 'description')
+ brief_fields = ('id', 'url', 'display', 'name', 'enabled', 'description')
class InventoryItemTemplateSerializer(ComponentTemplateSerializer):
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index e405129b8..08275554b 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -1032,7 +1032,7 @@ class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
class Meta:
model = ModuleBayTemplate
- fields = ('id', 'name', 'label', 'position', 'description')
+ fields = ('id', 'name', 'label', 'position', 'enabled', 'description')
@register_filterset
@@ -1040,7 +1040,7 @@ class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponent
class Meta:
model = DeviceBayTemplate
- fields = ('id', 'name', 'label', 'description')
+ fields = ('id', 'name', 'label', 'enabled', 'description')
@register_filterset
@@ -2397,7 +2397,7 @@ class ModuleBayFilterSet(ModularDeviceComponentFilterSet):
class Meta:
model = ModuleBay
- fields = ('id', 'name', 'label', 'position', 'description')
+ fields = ('id', 'name', 'label', 'position', 'enabled', 'description')
@register_filterset
@@ -2417,7 +2417,7 @@ class DeviceBayFilterSet(DeviceComponentFilterSet):
class Meta:
model = DeviceBay
- fields = ('id', 'name', 'label', 'description')
+ fields = ('id', 'name', 'label', 'enabled', 'description')
@register_filterset
diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py
index bfa492111..a13b2bf38 100644
--- a/netbox/dcim/forms/bulk_create.py
+++ b/netbox/dcim/forms/bulk_create.py
@@ -108,10 +108,13 @@ class RearPortBulkCreateForm(
field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags')
-class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
+class ModuleBayBulkCreateForm(
+ form_from_model(ModuleBay, ['enabled']),
+ DeviceBulkAddComponentForm
+):
model = ModuleBay
- field_order = ('name', 'label', 'position', 'description', 'tags')
- replication_fields = ('name', 'label', 'position')
+ field_order = ('name', 'label', 'position', 'enabled', 'description', 'tags')
+ replication_fields = ('name', 'label', 'position', 'enabled')
position = ExpandableNameField(
label=_('Position'),
required=False,
@@ -119,9 +122,12 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
)
-class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
+class DeviceBayBulkCreateForm(
+ form_from_model(DeviceBay, ['enabled']),
+ DeviceBulkAddComponentForm
+):
model = DeviceBay
- field_order = ('name', 'label', 'description', 'tags')
+ field_order = ('name', 'label', 'enabled', 'description', 'tags')
class InventoryItemBulkCreateForm(
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index aaea26eef..6dfa4884e 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -1245,6 +1245,11 @@ class ModuleBayTemplateBulkEditForm(ComponentTemplateBulkEditForm):
label=_('Description'),
required=False
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=BulkEditNullBooleanSelect,
+ )
nullable_fields = ('label', 'position', 'description')
@@ -1263,6 +1268,11 @@ class DeviceBayTemplateBulkEditForm(ComponentTemplateBulkEditForm):
label=_('Description'),
required=False
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=BulkEditNullBooleanSelect,
+ )
nullable_fields = ('label', 'description')
@@ -1687,23 +1697,23 @@ class RearPortBulkEditForm(
class ModuleBayBulkEditForm(
- form_from_model(ModuleBay, ['label', 'position', 'description']),
+ form_from_model(ModuleBay, ['label', 'position', 'enabled', 'description']),
NetBoxModelBulkEditForm
):
model = ModuleBay
fieldsets = (
- FieldSet('label', 'position', 'description'),
+ FieldSet('label', 'position', 'enabled', 'description'),
)
nullable_fields = ('label', 'position', 'description')
class DeviceBayBulkEditForm(
- form_from_model(DeviceBay, ['label', 'description']),
+ form_from_model(DeviceBay, ['label', 'enabled', 'description']),
NetBoxModelBulkEditForm
):
model = DeviceBay
fieldsets = (
- FieldSet('label', 'description'),
+ FieldSet('label', 'enabled', 'description'),
)
nullable_fields = ('label', 'description')
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index acdb0f638..ec60c394a 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -1154,7 +1154,13 @@ class ModuleBayImportForm(OwnerCSVMixin, NetBoxModelImportForm):
class Meta:
model = ModuleBay
- fields = ('device', 'name', 'label', 'position', 'description', 'owner', 'tags')
+ fields = ('device', 'name', 'label', 'position', 'enabled', 'description', 'owner', 'tags')
+
+ def clean_enabled(self):
+ # Make sure enabled is True when it's not included in the uploaded data
+ if 'enabled' not in self.data:
+ return True
+ return self.cleaned_data['enabled']
class DeviceBayImportForm(OwnerCSVMixin, NetBoxModelImportForm):
@@ -1176,7 +1182,7 @@ class DeviceBayImportForm(OwnerCSVMixin, NetBoxModelImportForm):
class Meta:
model = DeviceBay
- fields = ('device', 'name', 'label', 'installed_device', 'description', 'owner', 'tags')
+ fields = ('device', 'name', 'label', 'enabled', 'installed_device', 'description', 'owner', 'tags')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1204,6 +1210,12 @@ class DeviceBayImportForm(OwnerCSVMixin, NetBoxModelImportForm):
else:
self.fields['installed_device'].queryset = Device.objects.none()
+ def clean_enabled(self):
+ # Make sure enabled is True when it's not included in the uploaded data
+ if 'enabled' not in self.data:
+ return True
+ return self.cleaned_data['enabled']
+
class InventoryItemImportForm(OwnerCSVMixin, NetBoxModelImportForm):
device = CSVModelChoiceField(
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index b1a54842f..1584a09cc 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -1870,7 +1870,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('name', 'label', 'position', name=_('Attributes')),
+ FieldSet('name', 'label', 'position', 'enabled', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet(
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1878,31 +1878,41 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
- tag = TagFilterField(model)
position = forms.CharField(
label=_('Position'),
required=False
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
+ )
+ tag = TagFilterField(model)
class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
model = ModuleBayTemplate
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('name', 'label', 'position', name=_('Attributes')),
+ FieldSet('name', 'label', 'position', 'enabled', name=_('Attributes')),
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
)
position = forms.CharField(
label=_('Position'),
required=False,
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
+ )
class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('name', 'label', name=_('Attributes')),
+ FieldSet('name', 'label', 'enabled', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet(
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1910,6 +1920,11 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
+ )
tag = TagFilterField(model)
@@ -1917,9 +1932,14 @@ class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
model = DeviceBayTemplate
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('name', 'label', name=_('Attributes')),
+ FieldSet('name', 'label', 'enabled', name=_('Attributes')),
FieldSet('device_type_id', name=_('Device')),
)
+ enabled = forms.NullBooleanField(
+ label=_('Enabled'),
+ required=False,
+ widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
+ )
class InventoryItemFilterForm(DeviceComponentFilterForm):
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index ca06b620b..dd00418b5 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -777,7 +777,7 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
'device_id': '$device',
},
context={
- 'disabled': 'installed_module',
+ 'disabled': '_occupied',
},
)
module_type = DynamicModelChoiceField(
@@ -1233,26 +1233,26 @@ class ModuleBayTemplateForm(ModularComponentTemplateForm):
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
- 'name', 'label', 'position', 'description',
+ 'name', 'label', 'position', 'enabled', 'description',
),
)
class Meta:
model = ModuleBayTemplate
fields = [
- 'device_type', 'module_type', 'name', 'label', 'position', 'description',
+ 'device_type', 'module_type', 'name', 'label', 'position', 'enabled', 'description',
]
class DeviceBayTemplateForm(ComponentTemplateForm):
fieldsets = (
- FieldSet('device_type', 'name', 'label', 'description'),
+ FieldSet('device_type', 'name', 'label', 'enabled', 'description'),
)
class Meta:
model = DeviceBayTemplate
fields = [
- 'device_type', 'name', 'label', 'description',
+ 'device_type', 'name', 'label', 'enabled', 'description',
]
@@ -1698,25 +1698,25 @@ class RearPortForm(ModularDeviceComponentForm):
class ModuleBayForm(ModularDeviceComponentForm):
fieldsets = (
- FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
+ FieldSet('device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'tags',),
)
class Meta:
model = ModuleBay
fields = [
- 'device', 'module', 'name', 'label', 'position', 'description', 'owner', 'tags',
+ 'device', 'module', 'name', 'label', 'position', 'enabled', 'description', 'owner', 'tags',
]
class DeviceBayForm(DeviceComponentForm):
fieldsets = (
- FieldSet('device', 'name', 'label', 'description', 'tags',),
+ FieldSet('device', 'name', 'label', 'enabled', 'description', 'tags',),
)
class Meta:
model = DeviceBay
fields = [
- 'device', 'name', 'label', 'description', 'owner', 'tags',
+ 'device', 'name', 'label', 'enabled', 'description', 'owner', 'tags',
]
diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py
index 85a7dfdf5..121110886 100644
--- a/netbox/dcim/graphql/filters.py
+++ b/netbox/dcim/graphql/filters.py
@@ -318,6 +318,7 @@ class DeviceFilter(
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
+ enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -326,7 +327,7 @@ class DeviceBayFilter(ComponentModelFilterMixin, NetBoxModelFilter):
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedModelFilter):
- pass
+ enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
@@ -742,11 +743,13 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
)
parent_id: ID | None = strawberry_django.filter_field()
position: StrFilterLookup[str] | None = strawberry_django.filter_field()
+ enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
position: StrFilterLookup[str] | None = strawberry_django.filter_field()
+ enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
diff --git a/netbox/dcim/migrations/0229_devicebay_modulebay_enabled.py b/netbox/dcim/migrations/0229_devicebay_modulebay_enabled.py
new file mode 100644
index 000000000..16083708a
--- /dev/null
+++ b/netbox/dcim/migrations/0229_devicebay_modulebay_enabled.py
@@ -0,0 +1,30 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('dcim', '0228_cable_bundle'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='devicebay',
+ name='enabled',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='devicebaytemplate',
+ name='enabled',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='enabled',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='modulebaytemplate',
+ name='enabled',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index 0d54331d8..3394ca4ab 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -722,6 +722,10 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
blank=True,
help_text=_('Identifier to reference when renaming installed components')
)
+ enabled = models.BooleanField(
+ verbose_name=_('enabled'),
+ default=True,
+ )
component_model = ModuleBay
@@ -734,6 +738,7 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
position=self.position,
+ enabled=self.enabled,
**kwargs
)
instantiate.do_not_call_in_templates = True
@@ -743,6 +748,7 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
'name': self.name,
'label': self.label,
'position': self.position,
+ 'enabled': self.enabled,
'description': self.description,
}
@@ -751,6 +757,11 @@ class DeviceBayTemplate(ComponentTemplateModel):
"""
A template for a DeviceBay to be created for a new parent Device.
"""
+ enabled = models.BooleanField(
+ verbose_name=_('enabled'),
+ default=True,
+ )
+
component_model = DeviceBay
class Meta(ComponentTemplateModel.Meta):
@@ -761,7 +772,8 @@ class DeviceBayTemplate(ComponentTemplateModel):
return self.component_model(
device=device,
name=self.name,
- label=self.label
+ label=self.label,
+ enabled=self.enabled,
)
instantiate.do_not_call_in_templates = True
@@ -777,6 +789,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
return {
'name': self.name,
'label': self.label,
+ 'enabled': self.enabled,
'description': self.description,
}
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 09dc02ae9..cfed988aa 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1257,10 +1257,14 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
blank=True,
help_text=_('Identifier to reference when renaming installed components')
)
+ enabled = models.BooleanField(
+ verbose_name=_('enabled'),
+ default=True,
+ )
objects = TreeManager()
- clone_fields = ('device',)
+ clone_fields = ('device', 'enabled')
class Meta(ModularComponentModel.Meta):
# Empty tuple triggers Django migration detection for MPTT indexes
@@ -1299,6 +1303,13 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
self.parent = None
super().save(*args, **kwargs)
+ @property
+ def _occupied(self):
+ """
+ Indicates whether the module bay is occupied by a module.
+ """
+ return bool(not self.enabled or hasattr(self, 'installed_module'))
+
class DeviceBay(ComponentModel, TrackingModelMixin):
"""
@@ -1311,8 +1322,12 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
blank=True,
null=True
)
+ enabled = models.BooleanField(
+ verbose_name=_('enabled'),
+ default=True,
+ )
- clone_fields = ('device',)
+ clone_fields = ('device', 'enabled')
class Meta(ComponentModel.Meta):
verbose_name = _('device bay')
@@ -1327,6 +1342,16 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
device_type=self.device.device_type
))
+ # Prevent installing a device into a disabled bay
+ if self.installed_device and not self.enabled:
+ current_installed_device_id = (
+ DeviceBay.objects.filter(pk=self.pk).values_list('installed_device_id', flat=True).first()
+ )
+ if self.pk is None or current_installed_device_id != self.installed_device_id:
+ raise ValidationError({
+ 'installed_device': _("Cannot install a device in a disabled device bay.")
+ })
+
# Cannot install a device into itself, obviously
if self.installed_device and getattr(self, 'device', None) == self.installed_device:
raise ValidationError(_("Cannot install a device into itself."))
@@ -1341,6 +1366,13 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
).format(bay=current_bay)
})
+ @property
+ def _occupied(self):
+ """
+ Indicates whether the device bay is occupied by a child device.
+ """
+ return bool(not self.enabled or self.installed_device_id)
+
#
# Inventory items
diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py
index 89e914366..1a4645df3 100644
--- a/netbox/dcim/models/modules.py
+++ b/netbox/dcim/models/modules.py
@@ -258,6 +258,14 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
)
)
+ # Prevent module from being installed in a disabled bay
+ if hasattr(self, 'module_bay') and self.module_bay and not self.module_bay.enabled:
+ current_module_bay_id = Module.objects.filter(pk=self.pk).values_list('module_bay_id', flat=True).first()
+ if self.pk is None or current_module_bay_id != self.module_bay_id:
+ raise ValidationError({
+ 'module_bay': _("Cannot install a module in a disabled module bay.")
+ })
+
# Check for recursion
module = self
module_bays = []
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 7fbe61e70..e89df45f9 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -888,6 +888,9 @@ class DeviceBayTable(DeviceComponentTable):
'args': [Accessor('device_id')],
}
)
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
status = tables.TemplateColumn(
verbose_name=_('Status'),
template_code=DEVICEBAY_STATUS,
@@ -925,12 +928,12 @@ class DeviceBayTable(DeviceComponentTable):
class Meta(DeviceComponentTable.Meta):
model = models.DeviceBay
fields = (
- 'pk', 'id', 'name', 'device', 'label', 'status', 'description', 'installed_device', 'installed_role',
- 'installed_device_type', 'installed_description', 'installed_serial', 'installed_asset_tag', 'tags',
- 'created', 'last_updated',
+ 'pk', 'id', 'name', 'device', 'label', 'enabled', 'status', 'description', 'installed_device',
+ 'installed_role', 'installed_device_type', 'installed_description', 'installed_serial',
+ 'installed_asset_tag', 'tags', 'created', 'last_updated',
)
- default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
+ default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'status', 'installed_device', 'description')
class DeviceDeviceBayTable(DeviceBayTable):
@@ -940,6 +943,9 @@ class DeviceDeviceBayTable(DeviceBayTable):
'"> {{ value }}',
attrs={'td': {'class': 'text-nowrap'}}
)
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
actions = columns.ActionsColumn(
extra_buttons=DEVICEBAY_BUTTONS
)
@@ -947,9 +953,9 @@ class DeviceDeviceBayTable(DeviceBayTable):
class Meta(DeviceComponentTable.Meta):
model = models.DeviceBay
fields = (
- 'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
+ 'pk', 'id', 'name', 'label', 'enabled', 'status', 'installed_device', 'description', 'tags', 'actions',
)
- default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
+ default_columns = ('pk', 'name', 'label', 'enabled', 'status', 'installed_device', 'description')
class ModuleBayTable(ModularDeviceComponentTable):
@@ -960,6 +966,9 @@ class ModuleBayTable(ModularDeviceComponentTable):
'args': [Accessor('device_id')],
}
)
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
parent = tables.Column(
linkify=True,
verbose_name=_('Parent'),
@@ -988,11 +997,11 @@ class ModuleBayTable(ModularDeviceComponentTable):
class Meta(ModularDeviceComponentTable.Meta):
model = models.ModuleBay
fields = (
- 'pk', 'id', 'name', 'device', 'parent', 'label', 'position', 'installed_module', 'module_status',
+ 'pk', 'id', 'name', 'device', 'enabled', 'parent', 'label', 'position', 'installed_module', 'module_status',
'module_serial', 'module_asset_tag', 'description', 'tags',
)
default_columns = (
- 'pk', 'name', 'device', 'parent', 'label', 'installed_module', 'module_status', 'description',
+ 'pk', 'name', 'device', 'enabled', 'parent', 'label', 'installed_module', 'module_status', 'description',
)
def render_parent_bay(self, value):
@@ -1007,6 +1016,9 @@ class DeviceModuleBayTable(ModuleBayTable):
verbose_name=_('Name'),
linkify=True,
)
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
actions = columns.ActionsColumn(
extra_buttons=MODULEBAY_BUTTONS
)
@@ -1014,10 +1026,10 @@ class DeviceModuleBayTable(ModuleBayTable):
class Meta(ModuleBayTable.Meta):
model = models.ModuleBay
fields = (
- 'pk', 'id', 'parent', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
- 'module_asset_tag', 'description', 'tags', 'actions',
+ 'pk', 'id', 'parent', 'name', 'label', 'enabled', 'position', 'installed_module', 'module_status',
+ 'module_serial', 'module_asset_tag', 'description', 'tags', 'actions',
)
- default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
+ default_columns = ('pk', 'name', 'label', 'enabled', 'installed_module', 'module_status', 'description')
class InventoryItemTable(DeviceComponentTable):
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 652ff9d45..f952c2d4a 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -289,24 +289,30 @@ class RearPortTemplateTable(ComponentTemplateTable):
class ModuleBayTemplateTable(ComponentTemplateTable):
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
actions = columns.ActionsColumn(
actions=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
model = models.ModuleBayTemplate
- fields = ('pk', 'name', 'label', 'position', 'description', 'actions')
+ fields = ('pk', 'name', 'label', 'position', 'enabled', 'description', 'actions')
empty_text = "None"
class DeviceBayTemplateTable(ComponentTemplateTable):
+ enabled = columns.BooleanColumn(
+ verbose_name=_('Enabled'),
+ )
actions = columns.ActionsColumn(
actions=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
model = models.DeviceBayTemplate
- fields = ('pk', 'name', 'label', 'description', 'actions')
+ fields = ('pk', 'name', 'label', 'enabled', 'description', 'actions')
empty_text = "None"
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 3675a18cc..2b786aec5 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -565,7 +565,7 @@ DEVICEBAY_BUTTONS = """
- {% else %}
+ {% elif record.enabled %}
@@ -579,7 +579,7 @@ MODULEBAY_BUTTONS = """
- {% else %}
+ {% elif record.enabled %}
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 326678bfa..bbdcc3f36 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -1226,7 +1226,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
- brief_fields = ['description', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1243,9 +1243,9 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
)
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(device_type=devicetype, name='Module Bay Template 1', enabled=True),
+ ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2', enabled=False),
+ ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3', enabled=True),
)
ModuleBayTemplate.objects.bulk_create(module_bay_templates)
@@ -1253,6 +1253,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
{
'device_type': devicetype.pk,
'name': 'Module Bay Template 4',
+ 'enabled': False,
},
{
'device_type': devicetype.pk,
@@ -1267,7 +1268,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate
- brief_fields = ['description', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1284,9 +1285,9 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
)
device_bay_templates = (
- DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
- DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
- DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1', enabled=True),
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2', enabled=False),
+ DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3', enabled=True),
)
DeviceBayTemplate.objects.bulk_create(device_bay_templates)
@@ -1294,6 +1295,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
{
'device_type': devicetype.pk,
'name': 'Device Bay Template 4',
+ 'enabled': False,
},
{
'device_type': devicetype.pk,
@@ -2594,7 +2596,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay
- brief_fields = ['description', 'display', 'id', 'installed_module', 'name', 'url']
+ brief_fields = ['_occupied', 'description', 'display', 'enabled', 'id', 'installed_module', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -2610,9 +2612,9 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
device = Device.objects.create(device_type=device_type, role=role, name='Device 1', site=site)
module_bays = (
- ModuleBay(device=device, name='Device Bay 1'),
- ModuleBay(device=device, name='Device Bay 2'),
- ModuleBay(device=device, name='Device Bay 3'),
+ ModuleBay(device=device, name='Device Bay 1', enabled=True),
+ ModuleBay(device=device, name='Device Bay 2', enabled=False),
+ ModuleBay(device=device, name='Device Bay 3', enabled=True),
)
for module_bay in module_bays:
module_bay.save()
@@ -2621,6 +2623,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
{
'device': device.pk,
'name': 'Device Bay 4',
+ 'enabled': False,
},
{
'device': device.pk,
@@ -2635,7 +2638,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay
- brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'description', 'device', 'display', 'enabled', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -2672,9 +2675,9 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
Device.objects.bulk_create(devices)
device_bays = (
- DeviceBay(device=devices[0], name='Device Bay 1'),
- DeviceBay(device=devices[0], name='Device Bay 2'),
- DeviceBay(device=devices[0], name='Device Bay 3'),
+ DeviceBay(device=devices[0], name='Device Bay 1', enabled=True),
+ DeviceBay(device=devices[0], name='Device Bay 2', enabled=False),
+ DeviceBay(device=devices[0], name='Device Bay 3', enabled=True),
)
DeviceBay.objects.bulk_create(device_bays)
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 8afb4d05f..d91e24d79 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -2247,13 +2247,21 @@ class ModuleBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
ModuleBayTemplate.objects.bulk_create(
(
ModuleBayTemplate(
- device_type=device_types[0], name='Module Bay 1', description='foobar1'
+ device_type=device_types[0], name='Module Bay 1', enabled=True, description='foobar1'
),
ModuleBayTemplate(
- device_type=device_types[1], name='Module Bay 2', description='foobar2', module_type=module_types[0]
+ device_type=device_types[1],
+ name='Module Bay 2',
+ enabled=False,
+ description='foobar2',
+ module_type=module_types[0],
),
ModuleBayTemplate(
- device_type=device_types[2], name='Module Bay 3', description='foobar3', module_type=module_types[1]
+ device_type=device_types[2],
+ name='Module Bay 3',
+ enabled=True,
+ description='foobar3',
+ module_type=module_types[1],
),
)
)
@@ -2262,6 +2270,12 @@ class ModuleBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_enabled(self):
+ params = {'enabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'enabled': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_module_type(self):
module_types = ModuleType.objects.all()[:2]
params = {'module_type_id': [module_types[0].pk, module_types[1].pk]}
@@ -2284,16 +2298,30 @@ class DeviceBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
)
DeviceType.objects.bulk_create(device_types)
- DeviceBayTemplate.objects.bulk_create((
- DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1', description='foobar1'),
- DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2', description='foobar2'),
- DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3', description='foobar3'),
- ))
+ DeviceBayTemplate.objects.bulk_create(
+ (
+ DeviceBayTemplate(
+ device_type=device_types[0], name='Device Bay 1', enabled=True, description='foobar1'
+ ),
+ DeviceBayTemplate(
+ device_type=device_types[1], name='Device Bay 2', enabled=False, description='foobar2'
+ ),
+ DeviceBayTemplate(
+ device_type=device_types[2], name='Device Bay 3', enabled=True, description='foobar3'
+ ),
+ )
+ )
def test_name(self):
params = {'name': ['Device Bay 1', 'Device Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_enabled(self):
+ params = {'enabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'enabled': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
class InventoryItemTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = InventoryItemTemplate.objects.all()
@@ -5778,11 +5806,11 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
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(device=devices[2], name='Module Bay 4', label='D', description='Fourth'),
- ModuleBay(device=devices[2], name='Module Bay 5', label='E', description='Fifth'),
+ ModuleBay(device=devices[0], name='Module Bay 1', label='A', enabled=True, description='First'),
+ ModuleBay(device=devices[1], name='Module Bay 2', label='B', enabled=False, description='Second'),
+ ModuleBay(device=devices[2], name='Module Bay 3', label='C', enabled=True, description='Third'),
+ ModuleBay(device=devices[2], name='Module Bay 4', label='D', enabled=False, description='Fourth'),
+ ModuleBay(device=devices[2], name='Module Bay 5', label='E', enabled=True, description='Fifth'),
)
for module_bay in module_bays:
module_bay.save()
@@ -5806,6 +5834,12 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_enabled(self):
+ params = {'enabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'enabled': False}
+ 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)
@@ -5965,6 +5999,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
device=devices[0],
name='Device Bay 1',
label='A',
+ enabled=True,
description='First',
_site=devices[0].site,
_location=devices[0].location,
@@ -5974,6 +6009,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
device=devices[1],
name='Device Bay 2',
label='B',
+ enabled=False,
description='Second',
_site=devices[1].site,
_location=devices[1].location,
@@ -5983,6 +6019,7 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
device=devices[2],
name='Device Bay 3',
label='C',
+ enabled=True,
description='Third',
_site=devices[2].site,
_location=devices[2].location,
@@ -5999,6 +6036,12 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'label': ['A', 'B']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_enabled(self):
+ params = {'enabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'enabled': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_description(self):
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index c3c3acf99..3509a097d 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -712,6 +712,112 @@ class DeviceTestCase(TestCase):
).full_clean()
+class DeviceBayTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ site = Site.objects.create(name='Test Site 1', slug='test-site-1')
+ manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
+
+ # Parent device type must support device bays (is_parent_device=True)
+ parent_device_type = DeviceType.objects.create(
+ manufacturer=manufacturer,
+ model='Parent Device Type',
+ slug='parent-device-type',
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
+ )
+ # Child device type for installation
+ child_device_type = DeviceType.objects.create(
+ manufacturer=manufacturer,
+ model='Child Device Type',
+ slug='child-device-type',
+ u_height=0,
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
+ )
+ device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
+
+ cls.parent_device = Device.objects.create(
+ name='Parent Device',
+ device_type=parent_device_type,
+ role=device_role,
+ site=site
+ )
+ cls.child_device = Device.objects.create(
+ name='Child Device',
+ device_type=child_device_type,
+ role=device_role,
+ site=site
+ )
+ cls.child_device_2 = Device.objects.create(
+ name='Child Device 2',
+ device_type=child_device_type,
+ role=device_role,
+ site=site
+ )
+
+ def test_cannot_install_device_in_disabled_bay(self):
+ """
+ Test that a device cannot be installed into a disabled DeviceBay.
+ """
+ # Create a disabled device bay with a device being installed
+ device_bay = DeviceBay(
+ device=self.parent_device,
+ name='Disabled Bay',
+ enabled=False,
+ installed_device=self.child_device
+ )
+
+ with self.assertRaises(ValidationError) as cm:
+ device_bay.clean()
+
+ self.assertIn('installed_device', cm.exception.message_dict)
+ self.assertIn('disabled device bay', str(cm.exception.message_dict['installed_device']))
+
+ def test_can_disable_bay_with_existing_device(self):
+ """
+ Test that disabling a bay that already has a device installed does NOT raise an error
+ (same installed_device_id).
+ """
+ # First, create an enabled device bay with a device installed
+ device_bay = DeviceBay.objects.create(
+ device=self.parent_device,
+ name='Bay To Disable',
+ enabled=True,
+ installed_device=self.child_device
+ )
+
+ # Now disable the bay while keeping the same installed device
+ device_bay.enabled = False
+ # This should NOT raise a ValidationError
+ device_bay.clean()
+ device_bay.save()
+
+ device_bay.refresh_from_db()
+ self.assertFalse(device_bay.enabled)
+ self.assertEqual(device_bay.installed_device, self.child_device)
+
+ def test_cannot_change_installed_device_in_disabled_bay(self):
+ """
+ Test that changing the installed device in a disabled bay raises a ValidationError.
+ """
+ # Create an enabled device bay with a device installed
+ device_bay = DeviceBay.objects.create(
+ device=self.parent_device,
+ name='Bay With Device',
+ enabled=True,
+ installed_device=self.child_device
+ )
+
+ # Disable the bay and try to change the installed device
+ device_bay.enabled = False
+ device_bay.installed_device = self.child_device_2
+
+ with self.assertRaises(ValidationError) as cm:
+ device_bay.clean()
+
+ self.assertIn('installed_device', cm.exception.message_dict)
+
+
class ModuleBayTestCase(TestCase):
@classmethod
@@ -1011,6 +1117,25 @@ class ModuleBayTestCase(TestCase):
self.assertEqual(RearPort.objects.filter(module=module).count(), 1)
self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0)
+ def test_cannot_install_module_in_disabled_bay(self):
+ """
+ Test that a Module cannot be installed into a disabled ModuleBay.
+ """
+ device = Device.objects.first()
+ manufacturer = Manufacturer.objects.first()
+ module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Test Module Type Disabled')
+
+ # Create a disabled module bay
+ disabled_bay = ModuleBay.objects.create(device=device, name='Disabled Bay', enabled=False)
+
+ # Attempt to install a module into the disabled bay
+ module = Module(device=device, module_bay=disabled_bay, module_type=module_type)
+ with self.assertRaises(ValidationError) as cm:
+ module.clean()
+
+ self.assertIn('module_bay', cm.exception.message_dict)
+ self.assertIn('disabled module bay', str(cm.exception.message_dict['module_bay']))
+
class CableTestCase(TestCase):