mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
* 10500 add ModularComponentModel * 10500 add ModularComponentModel * 10500 add to forms * 10500 add to serializer, tables * 10500 template * 10500 add docs * 10500 check recursion * 10500 fix graphql * 10500 fix conflicting migration from merge * 10500 token resolution * 10500 don't return reverse * 10500 don't return reverse / optimize * Add ModuleTypeModuleBaysView * Fix replication of module bays on new modules * Clean up tables & templates * Adjust uniqueness constraints * Correct URL * Clean up docs * Fix up serializers * 10500 add filterset tests * 10500 add nested validation to Module * Misc cleanup * 10500 ModuleBay recursion Test * 10500 ModuleBay recursion Test * 10500 ModuleBay recursion Test * 10500 ModuleBay recursion Test * Enable MPTT for module bays * Fix tests * Fix validation of module token in component names * Misc cleanup * Merge migrations * Fix table ordering --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
57fe2071a4
commit
796b9e84af
@ -14,6 +14,10 @@ Module bays represent a space or slot within a device in which a field-replaceab
|
|||||||
|
|
||||||
The device to which this module bay belongs.
|
The device to which this module bay belongs.
|
||||||
|
|
||||||
|
### Module
|
||||||
|
|
||||||
|
The module to which this bay belongs (optional).
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
The module bay's name. Must be unique to the parent device.
|
The module bay's name. Must be unique to the parent device.
|
||||||
|
@ -297,6 +297,13 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
|||||||
|
|
||||||
class ModuleBaySerializer(NetBoxModelSerializer):
|
class ModuleBaySerializer(NetBoxModelSerializer):
|
||||||
device = DeviceSerializer(nested=True)
|
device = DeviceSerializer(nested=True)
|
||||||
|
module = ModuleSerializer(
|
||||||
|
nested=True,
|
||||||
|
fields=('id', 'url', 'display', 'module_bay'),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
installed_module = ModuleSerializer(
|
installed_module = ModuleSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
fields=('id', 'url', 'display', 'serial', 'description'),
|
fields=('id', 'url', 'display', 'serial', 'description'),
|
||||||
@ -307,7 +314,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'name', 'installed_module', 'label', 'position',
|
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
|
||||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
||||||
|
@ -253,13 +253,22 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
|
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
|
||||||
device_type = DeviceTypeSerializer(
|
device_type = DeviceTypeSerializer(
|
||||||
nested=True
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
module_type = ModuleTypeSerializer(
|
||||||
|
nested=True,
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBayTemplate
|
model = ModuleBayTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'position', 'description',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
@ -858,7 +858,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
|||||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBayTemplate
|
model = ModuleBayTemplate
|
||||||
@ -1322,11 +1322,11 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
|||||||
to_field_name='model',
|
to_field_name='model',
|
||||||
label=_('Module type (model)'),
|
label=_('Module type (model)'),
|
||||||
)
|
)
|
||||||
module_bay_id = django_filters.ModelMultipleChoiceFilter(
|
module_bay_id = TreeNodeMultipleChoiceFilter(
|
||||||
field_name='module_bay',
|
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.all(),
|
||||||
to_field_name='id',
|
field_name='module_bay',
|
||||||
label=_('Module Bay (ID)')
|
lookup_expr='in',
|
||||||
|
label=_('Module bay (ID)'),
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -1793,7 +1793,11 @@ class RearPortFilterSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
class ModuleBayFilterSet(ModularDeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||||
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=ModuleBay.objects.all(),
|
||||||
|
label=_('Parent module bay (ID)'),
|
||||||
|
)
|
||||||
installed_module_id = django_filters.ModelMultipleChoiceFilter(
|
installed_module_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='installed_module',
|
field_name='installed_module',
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.all(),
|
||||||
|
@ -70,6 +70,18 @@ class InterfaceCommonForm(forms.Form):
|
|||||||
|
|
||||||
class ModuleCommonForm(forms.Form):
|
class ModuleCommonForm(forms.Form):
|
||||||
|
|
||||||
|
def _get_module_bay_tree(self, module_bay):
|
||||||
|
module_bays = []
|
||||||
|
while module_bay:
|
||||||
|
module_bays.append(module_bay)
|
||||||
|
if module_bay.module:
|
||||||
|
module_bay = module_bay.module.module_bay
|
||||||
|
else:
|
||||||
|
module_bay = None
|
||||||
|
|
||||||
|
module_bays.reverse()
|
||||||
|
return module_bays
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -88,6 +100,8 @@ class ModuleCommonForm(forms.Form):
|
|||||||
self.instance._disable_replication = True
|
self.instance._disable_replication = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
module_bays = self._get_module_bay_tree(module_bay)
|
||||||
|
|
||||||
for templates, component_attribute in [
|
for templates, component_attribute in [
|
||||||
("consoleporttemplates", "consoleports"),
|
("consoleporttemplates", "consoleports"),
|
||||||
("consoleserverporttemplates", "consoleserverports"),
|
("consoleserverporttemplates", "consoleserverports"),
|
||||||
@ -104,13 +118,24 @@ class ModuleCommonForm(forms.Form):
|
|||||||
|
|
||||||
# Get the templates for the module type.
|
# Get the templates for the module type.
|
||||||
for template in getattr(module_type, templates).all():
|
for template in getattr(module_type, templates).all():
|
||||||
|
resolved_name = template.name
|
||||||
# Installing modules with placeholders require that the bay has a position value
|
# Installing modules with placeholders require that the bay has a position value
|
||||||
if MODULE_TOKEN in template.name and not module_bay.position:
|
if MODULE_TOKEN in template.name:
|
||||||
raise forms.ValidationError(
|
if not module_bay.position:
|
||||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
raise forms.ValidationError(
|
||||||
)
|
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(module_bays) != template.name.count(MODULE_TOKEN):
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("Cannot install module with placeholder values in a module bay tree {level} in tree but {tokens} placeholders given.").format(
|
||||||
|
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for module_bay in module_bays:
|
||||||
|
resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
|
||||||
|
|
||||||
resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
|
|
||||||
existing_item = installed_components.get(resolved_name)
|
existing_item = installed_components.get(resolved_name)
|
||||||
|
|
||||||
# It is not possible to adopt components already belonging to a module
|
# It is not possible to adopt components already belonging to a module
|
||||||
|
@ -1033,15 +1033,15 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateForm(ComponentTemplateForm):
|
class ModuleBayTemplateForm(ModularComponentTemplateForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('device_type', 'name', 'label', 'position', 'description'),
|
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBayTemplate
|
model = ModuleBayTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'name', 'label', 'position', 'description',
|
'device_type', 'module_type', 'name', 'label', 'position', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1453,15 +1453,15 @@ class RearPortForm(ModularDeviceComponentForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayForm(DeviceComponentForm):
|
class ModuleBayForm(ModularDeviceComponentForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('device', 'name', 'label', 'position', 'description', 'tags',),
|
FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'label', 'position', 'description', 'tags',
|
'device', 'module', 'name', 'label', 'position', 'description', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -496,12 +496,18 @@ class ModuleType(NetBoxObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.ModuleBay,
|
models.ModuleBay,
|
||||||
fields='__all__',
|
# fields='__all__',
|
||||||
|
exclude=('parent',),
|
||||||
filters=ModuleBayFilter
|
filters=ModuleBayFilter
|
||||||
)
|
)
|
||||||
class ModuleBayType(ComponentType):
|
class ModuleBayType(ModularComponentType):
|
||||||
|
|
||||||
installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None
|
installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
|
children: List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
@strawberry_django.field
|
||||||
|
def parent(self) -> Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] | None:
|
||||||
|
return self.parent
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -509,7 +515,7 @@ class ModuleBayType(ComponentType):
|
|||||||
fields='__all__',
|
fields='__all__',
|
||||||
filters=ModuleBayTemplateFilter
|
filters=ModuleBayTemplateFilter
|
||||||
)
|
)
|
||||||
class ModuleBayTemplateType(ComponentTemplateType):
|
class ModuleBayTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
_name: str
|
||||||
|
|
||||||
|
|
||||||
|
74
netbox/dcim/migrations/0190_nested_modules.py
Normal file
74
netbox/dcim/migrations/0190_nested_modules.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0189_moduletype_airflow_rack_airflow_racktype_airflow'),
|
||||||
|
('extras', '0120_customfield_related_object_filter'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='modulebaytemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', '_name')},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='dcim_modulebay_unique_device_name',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='level',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='lft',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='module',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='parent',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.modulebay'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='rght',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='tree_id',
|
||||||
|
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
name='module_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
name='device_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='modulebay',
|
||||||
|
constraint=models.UniqueConstraint(fields=('device', 'module', 'name'), name='dcim_modulebay_unique_device_module_name'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_modulebaytemplate_unique_module_type_name'),
|
||||||
|
),
|
||||||
|
]
|
@ -158,14 +158,41 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
_("A component template must be associated with either a device type or a module type.")
|
_("A component template must be associated with either a device type or a module type.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_module_tree(self, module):
|
||||||
|
modules = []
|
||||||
|
all_module_bays = module.device.modulebays.all().select_related('module')
|
||||||
|
while module:
|
||||||
|
modules.append(module)
|
||||||
|
if module.module_bay:
|
||||||
|
module = module.module_bay.module
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
|
||||||
|
modules.reverse()
|
||||||
|
return modules
|
||||||
|
|
||||||
def resolve_name(self, module):
|
def resolve_name(self, module):
|
||||||
|
if MODULE_TOKEN not in self.name:
|
||||||
|
return self.name
|
||||||
|
|
||||||
if module:
|
if module:
|
||||||
return self.name.replace(MODULE_TOKEN, module.module_bay.position)
|
modules = self._get_module_tree(module)
|
||||||
|
name = self.name
|
||||||
|
for module in modules:
|
||||||
|
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
||||||
|
return name
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def resolve_label(self, module):
|
def resolve_label(self, module):
|
||||||
|
if MODULE_TOKEN not in self.label:
|
||||||
|
return self.label
|
||||||
|
|
||||||
if module:
|
if module:
|
||||||
return self.label.replace(MODULE_TOKEN, module.module_bay.position)
|
modules = self._get_module_tree(module)
|
||||||
|
label = self.label
|
||||||
|
for module in modules:
|
||||||
|
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
||||||
|
return label
|
||||||
return self.label
|
return self.label
|
||||||
|
|
||||||
|
|
||||||
@ -628,7 +655,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplate(ComponentTemplateModel):
|
class ModuleBayTemplate(ModularComponentTemplateModel):
|
||||||
"""
|
"""
|
||||||
A template for a ModuleBay to be created for a new parent Device.
|
A template for a ModuleBay to be created for a new parent Device.
|
||||||
"""
|
"""
|
||||||
@ -641,16 +668,16 @@ class ModuleBayTemplate(ComponentTemplateModel):
|
|||||||
|
|
||||||
component_model = ModuleBay
|
component_model = ModuleBay
|
||||||
|
|
||||||
class Meta(ComponentTemplateModel.Meta):
|
class Meta(ModularComponentTemplateModel.Meta):
|
||||||
verbose_name = _('module bay template')
|
verbose_name = _('module bay template')
|
||||||
verbose_name_plural = _('module bay templates')
|
verbose_name_plural = _('module bay templates')
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, **kwargs):
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
device=device,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
position=self.position
|
position=self.position,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
instantiate.do_not_call_in_templates = True
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import F, Sum
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
@ -1087,10 +1087,19 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
|||||||
# Bays
|
# Bays
|
||||||
#
|
#
|
||||||
|
|
||||||
class ModuleBay(ComponentModel, TrackingModelMixin):
|
class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
"""
|
"""
|
||||||
|
parent = TreeForeignKey(
|
||||||
|
to='self',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='children',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
editable=False,
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
position = models.CharField(
|
position = models.CharField(
|
||||||
verbose_name=_('position'),
|
verbose_name=_('position'),
|
||||||
max_length=30,
|
max_length=30,
|
||||||
@ -1098,15 +1107,45 @@ class ModuleBay(ComponentModel, TrackingModelMixin):
|
|||||||
help_text=_('Identifier to reference when renaming installed components')
|
help_text=_('Identifier to reference when renaming installed components')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = TreeManager()
|
||||||
|
|
||||||
clone_fields = ('device',)
|
clone_fields = ('device',)
|
||||||
|
|
||||||
class Meta(ComponentModel.Meta):
|
class Meta(ModularComponentModel.Meta):
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('device', 'module', 'name'),
|
||||||
|
name='%(app_label)s_%(class)s_unique_device_module_name'
|
||||||
|
),
|
||||||
|
)
|
||||||
verbose_name = _('module bay')
|
verbose_name = _('module bay')
|
||||||
verbose_name_plural = _('module bays')
|
verbose_name_plural = _('module bays')
|
||||||
|
|
||||||
|
class MPTTMeta:
|
||||||
|
order_insertion_by = ('module',)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
|
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Check for recursion
|
||||||
|
if module := self.module:
|
||||||
|
module_bays = [self.pk]
|
||||||
|
modules = []
|
||||||
|
while module:
|
||||||
|
if module.pk in modules or module.module_bay.pk in module_bays:
|
||||||
|
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
||||||
|
modules.append(module.pk)
|
||||||
|
module_bays.append(module.module_bay.pk)
|
||||||
|
module = module.module_bay.module if module.module_bay else None
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.module:
|
||||||
|
self.parent = self.module.module_bay
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBay(ComponentModel, TrackingModelMixin):
|
class DeviceBay(ComponentModel, TrackingModelMixin):
|
||||||
"""
|
"""
|
||||||
|
@ -1046,7 +1046,8 @@ class Device(
|
|||||||
self._instantiate_components(self.device_type.interfacetemplates.all())
|
self._instantiate_components(self.device_type.interfacetemplates.all())
|
||||||
self._instantiate_components(self.device_type.rearporttemplates.all())
|
self._instantiate_components(self.device_type.rearporttemplates.all())
|
||||||
self._instantiate_components(self.device_type.frontporttemplates.all())
|
self._instantiate_components(self.device_type.frontporttemplates.all())
|
||||||
self._instantiate_components(self.device_type.modulebaytemplates.all())
|
# Disable bulk_create to accommodate MPTT
|
||||||
|
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
|
||||||
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
||||||
# Disable bulk_create to accommodate MPTT
|
# Disable bulk_create to accommodate MPTT
|
||||||
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
|
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
|
||||||
@ -1207,6 +1208,17 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for recursion
|
||||||
|
module = self
|
||||||
|
module_bays = []
|
||||||
|
modules = []
|
||||||
|
while module:
|
||||||
|
if module.pk in modules or module.module_bay.pk in module_bays:
|
||||||
|
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
||||||
|
modules.append(module.pk)
|
||||||
|
module_bays.append(module.module_bay.pk)
|
||||||
|
module = module.module_bay.module if module.module_bay else None
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
is_new = self.pk is None
|
is_new = self.pk is None
|
||||||
|
|
||||||
@ -1228,7 +1240,8 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
("powerporttemplates", "powerports", PowerPort),
|
("powerporttemplates", "powerports", PowerPort),
|
||||||
("poweroutlettemplates", "poweroutlets", PowerOutlet),
|
("poweroutlettemplates", "poweroutlets", PowerOutlet),
|
||||||
("rearporttemplates", "rearports", RearPort),
|
("rearporttemplates", "rearports", RearPort),
|
||||||
("frontporttemplates", "frontports", FrontPort)
|
("frontporttemplates", "frontports", FrontPort),
|
||||||
|
("modulebaytemplates", "modulebays", ModuleBay),
|
||||||
]:
|
]:
|
||||||
create_instances = []
|
create_instances = []
|
||||||
update_instances = []
|
update_instances = []
|
||||||
@ -1257,17 +1270,22 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
if not disable_replication:
|
if not disable_replication:
|
||||||
create_instances.append(template_instance)
|
create_instances.append(template_instance)
|
||||||
|
|
||||||
component_model.objects.bulk_create(create_instances)
|
if component_model is not ModuleBay:
|
||||||
# Emit the post_save signal for each newly created object
|
component_model.objects.bulk_create(create_instances)
|
||||||
for component in create_instances:
|
# Emit the post_save signal for each newly created object
|
||||||
post_save.send(
|
for component in create_instances:
|
||||||
sender=component_model,
|
post_save.send(
|
||||||
instance=component,
|
sender=component_model,
|
||||||
created=True,
|
instance=component,
|
||||||
raw=False,
|
created=True,
|
||||||
using='default',
|
raw=False,
|
||||||
update_fields=None
|
using='default',
|
||||||
)
|
update_fields=None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# ModuleBays must be saved individually for MPTT
|
||||||
|
for instance in create_instances:
|
||||||
|
instance.save()
|
||||||
|
|
||||||
update_fields = ['module']
|
update_fields = ['module']
|
||||||
component_model.objects.bulk_update(update_instances, update_fields)
|
component_model.objects.bulk_update(update_instances, update_fields)
|
||||||
|
@ -313,6 +313,9 @@ class ModularDeviceComponentTable(DeviceComponentTable):
|
|||||||
verbose_name=_('Inventory Items'),
|
verbose_name=_('Inventory Items'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CableTerminationTable(NetBoxTable):
|
class CableTerminationTable(NetBoxTable):
|
||||||
cable = tables.Column(
|
cable = tables.Column(
|
||||||
@ -844,7 +847,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
|||||||
default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
|
default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTable(DeviceComponentTable):
|
class ModuleBayTable(ModularDeviceComponentTable):
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
verbose_name=_('Device'),
|
verbose_name=_('Device'),
|
||||||
linkify={
|
linkify={
|
||||||
@ -852,6 +855,10 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
'args': [Accessor('device_id')],
|
'args': [Accessor('device_id')],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
parent = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
verbose_name=_('Parent'),
|
||||||
|
)
|
||||||
installed_module = tables.Column(
|
installed_module = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Installed Module')
|
verbose_name=_('Installed Module')
|
||||||
@ -873,25 +880,40 @@ class ModuleBayTable(DeviceComponentTable):
|
|||||||
verbose_name=_('Module Status')
|
verbose_name=_('Module Status')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(ModularDeviceComponentTable.Meta):
|
||||||
model = models.ModuleBay
|
model = models.ModuleBay
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
|
'pk', 'id', 'name', 'device', 'parent', 'label', 'position', 'installed_module', 'module_status',
|
||||||
'module_asset_tag', 'description', 'tags',
|
'module_serial', 'module_asset_tag', 'description', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'module_status', 'description')
|
default_columns = (
|
||||||
|
'pk', 'name', 'device', 'parent', 'label', 'installed_module', 'module_status', 'description',
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_parent_bay(self, value):
|
||||||
|
return value.name if value else ''
|
||||||
|
|
||||||
|
def render_installed_module(self, value):
|
||||||
|
return value.module_type if value else ''
|
||||||
|
|
||||||
|
|
||||||
class DeviceModuleBayTable(ModuleBayTable):
|
class DeviceModuleBayTable(ModuleBayTable):
|
||||||
|
name = tables.TemplateColumn(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
||||||
|
'{{ value }}</a>',
|
||||||
|
order_by=Accessor('_name'),
|
||||||
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
extra_buttons=MODULEBAY_BUTTONS
|
extra_buttons=MODULEBAY_BUTTONS
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(DeviceComponentTable.Meta):
|
class Meta(ModuleBayTable.Meta):
|
||||||
model = models.ModuleBay
|
model = models.ModuleBay
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag',
|
'pk', 'id', 'parent', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
|
||||||
'description', 'tags', 'actions',
|
'module_asset_tag', 'description', 'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
|
default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
|
||||||
|
|
||||||
|
@ -1352,7 +1352,8 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
|
|||||||
ModuleBay(device=device, name='Module Bay 5'),
|
ModuleBay(device=device, name='Module Bay 5'),
|
||||||
ModuleBay(device=device, name='Module Bay 6'),
|
ModuleBay(device=device, name='Module Bay 6'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=device, module_bay=module_bays[0], module_type=module_types[0]),
|
Module(device=device, module_bay=module_bays[0], module_type=module_types[0]),
|
||||||
@ -1810,12 +1811,13 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
|||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||||
device = Device.objects.create(device_type=device_type, role=role, name='Device 1', site=site)
|
device = Device.objects.create(device_type=device_type, role=role, name='Device 1', site=site)
|
||||||
|
|
||||||
device_bays = (
|
module_bays = (
|
||||||
ModuleBay(device=device, name='Device Bay 1'),
|
ModuleBay(device=device, name='Device Bay 1'),
|
||||||
ModuleBay(device=device, name='Device Bay 2'),
|
ModuleBay(device=device, name='Device Bay 2'),
|
||||||
ModuleBay(device=device, name='Device Bay 3'),
|
ModuleBay(device=device, name='Device Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(device_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
|
@ -1871,16 +1871,27 @@ class ModuleBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
|
|||||||
)
|
)
|
||||||
DeviceType.objects.bulk_create(device_types)
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
|
module_types = (
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 1'),
|
||||||
|
ModuleType(manufacturer=manufacturer, model='Module Type 2'),
|
||||||
|
)
|
||||||
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
ModuleBayTemplate.objects.bulk_create((
|
ModuleBayTemplate.objects.bulk_create((
|
||||||
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1', description='foobar1'),
|
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1', description='foobar1'),
|
||||||
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2', description='foobar2'),
|
ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2', description='foobar2', module_type=module_types[0]),
|
||||||
ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3', description='foobar3'),
|
ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3', description='foobar3', module_type=module_types[1]),
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Module Bay 1', 'Module Bay 2']}
|
params = {'name': ['Module Bay 1', 'Module Bay 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_module_type(self):
|
||||||
|
module_types = ModuleType.objects.all()[:2]
|
||||||
|
params = {'module_type_id': [module_types[0].pk, module_types[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
|
class DeviceBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = DeviceBayTemplate.objects.all()
|
queryset = DeviceBayTemplate.objects.all()
|
||||||
@ -2309,10 +2320,8 @@ 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.objects.create(device=devices[0], name='Module Bay 1')
|
||||||
ModuleBay(device=devices[0], name='Module Bay 1'),
|
ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
|
||||||
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'),
|
||||||
@ -2624,7 +2633,8 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ModuleBay(device=devices[2], name='Module Bay 2'),
|
ModuleBay(device=devices[2], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(
|
Module(
|
||||||
@ -2827,7 +2837,8 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -3007,7 +3018,8 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -3187,7 +3199,8 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -3375,7 +3388,8 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -3606,7 +3620,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
ModuleBay(device=devices[3], name='Module Bay 4'),
|
ModuleBay(device=devices[3], name='Module Bay 4'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -4053,7 +4068,8 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -4242,7 +4258,8 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 2'),
|
ModuleBay(device=devices[1], name='Module Bay 2'),
|
||||||
ModuleBay(device=devices[2], name='Module Bay 3'),
|
ModuleBay(device=devices[2], name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
@ -4421,8 +4438,22 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
ModuleBay(device=devices[0], name='Module Bay 1', label='A', description='First'),
|
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[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 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.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
|
modules = (
|
||||||
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
|
||||||
|
Module(device=devices[1], module_bay=module_bays[1], module_type=module_type),
|
||||||
|
)
|
||||||
|
Module.objects.bulk_create(modules)
|
||||||
|
module_bays[3].module = modules[0]
|
||||||
|
module_bays[3].save()
|
||||||
|
module_bays[4].module = modules[1]
|
||||||
|
module_bays[4].save()
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Module Bay 1', 'Module Bay 2']}
|
params = {'name': ['Module Bay 1', 'Module Bay 2']}
|
||||||
@ -4478,6 +4509,11 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
params = {'device': [devices[0].name, devices[1].name]}
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_module(self):
|
||||||
|
modules = Module.objects.all()[:2]
|
||||||
|
params = {'module_id': [modules[0].pk, modules[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||||
queryset = DeviceBay.objects.all()
|
queryset = DeviceBay.objects.all()
|
||||||
|
@ -620,6 +620,100 @@ class DeviceTestCase(TestCase):
|
|||||||
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
|
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBayTestCase(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')
|
||||||
|
device_type = DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||||
|
)
|
||||||
|
device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
|
||||||
|
|
||||||
|
# Create a CustomField with a default value & assign it to all component models
|
||||||
|
location = Location.objects.create(name='Location 1', slug='location-1', site=site)
|
||||||
|
rack = Rack.objects.create(name='Rack 1', site=site)
|
||||||
|
device = Device.objects.create(name='Device 1', device_type=device_type, role=device_role, site=site, location=location, rack=rack)
|
||||||
|
|
||||||
|
module_bays = (
|
||||||
|
ModuleBay(device=device, name='Module Bay 1', label='A', description='First'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 2', label='B', description='Second'),
|
||||||
|
ModuleBay(device=device, name='Module Bay 3', label='C', description='Third'),
|
||||||
|
)
|
||||||
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
||||||
|
modules = (
|
||||||
|
Module(device=device, module_bay=module_bays[0], module_type=module_type),
|
||||||
|
Module(device=device, module_bay=module_bays[1], module_type=module_type),
|
||||||
|
Module(device=device, module_bay=module_bays[2], module_type=module_type),
|
||||||
|
)
|
||||||
|
# M3 -> MB3 -> M2 -> MB2 -> M1 -> MB1
|
||||||
|
Module.objects.bulk_create(modules)
|
||||||
|
module_bays[1].module = modules[0]
|
||||||
|
module_bays[1].clean()
|
||||||
|
module_bays[1].save()
|
||||||
|
module_bays[2].module = modules[1]
|
||||||
|
module_bays[2].clean()
|
||||||
|
module_bays[2].save()
|
||||||
|
|
||||||
|
def test_module_bay_recursion(self):
|
||||||
|
module_bay_1 = ModuleBay.objects.get(name='Module Bay 1')
|
||||||
|
module_bay_2 = ModuleBay.objects.get(name='Module Bay 2')
|
||||||
|
module_bay_3 = ModuleBay.objects.get(name='Module Bay 3')
|
||||||
|
module_1 = Module.objects.get(module_bay=module_bay_1)
|
||||||
|
module_2 = Module.objects.get(module_bay=module_bay_2)
|
||||||
|
module_3 = Module.objects.get(module_bay=module_bay_3)
|
||||||
|
|
||||||
|
# Confirm error if ModuleBay recurses
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
module_bay_1.module = module_3
|
||||||
|
module_bay_1.clean()
|
||||||
|
module_bay_1.save()
|
||||||
|
|
||||||
|
# Confirm error if Module recurses
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
module_1.module_bay = module_bay_3
|
||||||
|
module_1.clean()
|
||||||
|
module_1.save()
|
||||||
|
|
||||||
|
def test_single_module_token(self):
|
||||||
|
module_bays = ModuleBay.objects.all()
|
||||||
|
modules = Module.objects.all()
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
device_role = DeviceRole.objects.first()
|
||||||
|
site = Site.objects.first()
|
||||||
|
location = Location.objects.first()
|
||||||
|
rack = Rack.objects.first()
|
||||||
|
|
||||||
|
# Create DeviceType components
|
||||||
|
ConsolePortTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='{module}',
|
||||||
|
label='{module}',
|
||||||
|
)
|
||||||
|
ModuleBayTemplate.objects.create(
|
||||||
|
device_type=device_type,
|
||||||
|
name='Module Bay 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
device = Device.objects.create(
|
||||||
|
name='Device 2',
|
||||||
|
device_type=device_type,
|
||||||
|
role=device_role,
|
||||||
|
site=site,
|
||||||
|
location=location,
|
||||||
|
rack=rack
|
||||||
|
)
|
||||||
|
cp = device.consoleports.first()
|
||||||
|
|
||||||
|
def test_nested_module_token(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CableTestCase(TestCase):
|
class CableTestCase(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1899,12 +1899,9 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_device_modulebays(self):
|
def test_device_modulebays(self):
|
||||||
device = Device.objects.first()
|
device = Device.objects.first()
|
||||||
device_bays = (
|
ModuleBay.objects.create(device=device, name='Module Bay 1')
|
||||||
ModuleBay(device=device, name='Module Bay 1'),
|
ModuleBay.objects.create(device=device, name='Module Bay 2')
|
||||||
ModuleBay(device=device, name='Module Bay 2'),
|
ModuleBay.objects.create(device=device, name='Module Bay 3')
|
||||||
ModuleBay(device=device, name='Module Bay 3'),
|
|
||||||
)
|
|
||||||
ModuleBay.objects.bulk_create(device_bays)
|
|
||||||
|
|
||||||
url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
|
url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
|
||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
@ -1980,7 +1977,8 @@ class ModuleTestCase(
|
|||||||
ModuleBay(device=devices[1], name='Module Bay 4'),
|
ModuleBay(device=devices[1], name='Module Bay 4'),
|
||||||
ModuleBay(device=devices[1], name='Module Bay 5'),
|
ModuleBay(device=devices[1], name='Module Bay 5'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0]),
|
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0]),
|
||||||
@ -2782,7 +2780,8 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
ModuleBay(device=device, name='Module Bay 2'),
|
ModuleBay(device=device, name='Module Bay 2'),
|
||||||
ModuleBay(device=device, name='Module Bay 3'),
|
ModuleBay(device=device, name='Module Bay 3'),
|
||||||
)
|
)
|
||||||
ModuleBay.objects.bulk_create(module_bays)
|
for module_bay in module_bays:
|
||||||
|
module_bay.save()
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
@ -1314,6 +1314,21 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(ModuleType, 'modulebays', path='module-bays')
|
||||||
|
class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
|
||||||
|
child_model = ModuleBayTemplate
|
||||||
|
table = tables.ModuleBayTemplateTable
|
||||||
|
filterset = filtersets.ModuleBayTemplateFilterSet
|
||||||
|
viewname = 'dcim:moduletype_modulebays'
|
||||||
|
tab = ViewTab(
|
||||||
|
label=_('Module Bays'),
|
||||||
|
badge=lambda obj: obj.modulebaytemplates.count(),
|
||||||
|
permission='dcim.view_modulebaytemplate',
|
||||||
|
weight=570,
|
||||||
|
hide_if_empty=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeImportView(generic.BulkImportView):
|
class ModuleTypeImportView(generic.BulkImportView):
|
||||||
additional_permissions = [
|
additional_permissions = [
|
||||||
'dcim.add_moduletype',
|
'dcim.add_moduletype',
|
||||||
|
@ -39,6 +39,9 @@
|
|||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.device.pk %}">{% trans "Rear Ports" %}</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.device.pk %}">{% trans "Rear Ports" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_modulebay %}
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -22,6 +22,10 @@
|
|||||||
<a href="{% url 'dcim:device_modulebays' pk=object.device.pk %}">{{ object.device }}</a>
|
<a href="{% url 'dcim:device_modulebays' pk=object.device.pk %}">{{ object.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Module" %}</th>
|
||||||
|
<td>{{ object.module|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Name" %}</th>
|
<th scope="row">{% trans "Name" %}</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
@ -31,8 +35,8 @@
|
|||||||
<td>{{ object.label|placeholder }}</td>
|
<td>{{ object.label|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Position" %}</th>
|
<th scope="row">{% trans "Position" %}</th>
|
||||||
<td>{{ object.position|placeholder }}</td>
|
<td>{{ object.position|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
|
@ -27,10 +27,8 @@
|
|||||||
<td>{{ object.description|placeholder }}</td>
|
<td>{{ object.description|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Airflow" %}</th>
|
<th scope="row">{% trans "Airflow" %}</th>
|
||||||
<td>
|
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||||
{{ object.get_airflow_display|placeholder }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Weight" %}</th>
|
<th scope="row">{% trans "Weight" %}</th>
|
||||||
|
@ -39,6 +39,9 @@
|
|||||||
{% if perms.dcim.add_rearporttemplate %}
|
{% if perms.dcim.add_rearporttemplate %}
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_modulebaytemplate %}
|
||||||
|
<li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
Reference in New Issue
Block a user