Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #10500: Introduce support for nested modules #16983

Merged
merged 40 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
81b8152
10500 add ModularComponentModel
arthanson Jul 23, 2024
d293a8b
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 23, 2024
4bd27ed
10500 add ModularComponentModel
arthanson Jul 23, 2024
d72d748
10500 add to forms
arthanson Jul 23, 2024
2b41ba6
10500 add to serializer, tables
arthanson Jul 24, 2024
61d84ef
10500 template
arthanson Jul 24, 2024
097e1a6
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 25, 2024
f1e5d14
10500 add docs
arthanson Jul 25, 2024
0c7fb56
10500 check recursion
arthanson Jul 25, 2024
fda7a99
10500 fix graphql
arthanson Jul 26, 2024
c7c6e78
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 26, 2024
844fb1f
10500 fix conflicting migration from merge
arthanson Jul 26, 2024
702c862
10500 token resolution
arthanson Jul 26, 2024
7385c07
10500 don't return reverse
arthanson Jul 29, 2024
acfbae3
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 29, 2024
94316e8
10500 don't return reverse / optimize
arthanson Jul 29, 2024
399a2d6
Add ModuleTypeModuleBaysView
jeremystretch Jul 29, 2024
a16ad74
Fix replication of module bays on new modules
jeremystretch Jul 29, 2024
6144409
Clean up tables & templates
jeremystretch Jul 29, 2024
e583ffa
Adjust uniqueness constraints
jeremystretch Jul 29, 2024
b17ea30
Correct URL
jeremystretch Jul 29, 2024
901215b
Clean up docs
jeremystretch Jul 29, 2024
b5dab4d
Fix up serializers
jeremystretch Jul 29, 2024
a99431e
10500 add filterset tests
arthanson Jul 30, 2024
b7012a0
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 30, 2024
07f497e
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 31, 2024
cf6d336
Merge branch 'feature' into 10500-nested-modules
arthanson Jul 31, 2024
ce69379
10500 add nested validation to Module
arthanson Jul 31, 2024
6bacc13
Misc cleanup
jeremystretch Aug 1, 2024
16e9a9c
Merge branch 'feature' into 10500-nested-modules
arthanson Aug 2, 2024
b4b3ac1
10500 ModuleBay recursion Test
arthanson Aug 2, 2024
4af817f
10500 ModuleBay recursion Test
arthanson Aug 2, 2024
2309be9
10500 ModuleBay recursion Test
arthanson Aug 2, 2024
f8d99ff
10500 ModuleBay recursion Test
arthanson Aug 5, 2024
6372d06
Enable MPTT for module bays
jeremystretch Aug 2, 2024
febb5da
Fix tests
jeremystretch Aug 5, 2024
16ff371
Fix validation of module token in component names
jeremystretch Aug 5, 2024
33514d7
Misc cleanup
jeremystretch Aug 5, 2024
d39ccac
Merge migrations
jeremystretch Aug 5, 2024
5150827
Fix table ordering
jeremystretch Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/models/dcim/modulebay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### Module

The module to which this bay belongs (optional).

### Name

The module bay's name. Must be unique to the parent device.
Expand Down
9 changes: 8 additions & 1 deletion netbox/dcim/api/serializers_/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ class Meta:

class ModuleBaySerializer(NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'module_bay'),
required=False,
allow_null=True,
default=None
)
installed_module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'serial', 'description'),
Expand All @@ -307,7 +314,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
class Meta:
model = ModuleBay
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',
]
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
Expand Down
13 changes: 11 additions & 2 deletions netbox/dcim/api/serializers_/devicetype_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,22 @@ class Meta:

class ModuleBayTemplateSerializer(ValidatedModelSerializer):
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:
model = ModuleBayTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description',
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'position', 'description',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
Expand Down
16 changes: 10 additions & 6 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ class Meta:
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')


class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):

class Meta:
model = ModuleBayTemplate
Expand Down Expand Up @@ -1322,11 +1322,11 @@ class ModuleFilterSet(NetBoxModelFilterSet):
to_field_name='model',
label=_('Module type (model)'),
)
module_bay_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_bay',
module_bay_id = TreeNodeMultipleChoiceFilter(
queryset=ModuleBay.objects.all(),
to_field_name='id',
label=_('Module Bay (ID)')
field_name='module_bay',
lookup_expr='in',
label=_('Module bay (ID)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
Expand Down Expand Up @@ -1793,7 +1793,11 @@ class Meta:
)


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(
field_name='installed_module',
queryset=ModuleBay.objects.all(),
Expand Down
35 changes: 30 additions & 5 deletions netbox/dcim/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ def clean(self):

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):
super().clean()

Expand All @@ -88,6 +100,8 @@ def clean(self):
self.instance._disable_replication = True
return

module_bays = self._get_module_bay_tree(module_bay)

for templates, component_attribute in [
("consoleporttemplates", "consoleports"),
("consoleserverporttemplates", "consoleserverports"),
Expand All @@ -104,13 +118,24 @@ def clean(self):

# Get the templates for the module type.
for template in getattr(module_type, templates).all():
resolved_name = template.name
# Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError(
_("Cannot install module with placeholder values in a module bay with no position defined.")
)
if MODULE_TOKEN in template.name:
if not module_bay.position:
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)

# It is not possible to adopt components already belonging to a module
Expand Down
12 changes: 6 additions & 6 deletions netbox/dcim/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,15 +1033,15 @@ class Meta:
]


class ModuleBayTemplateForm(ComponentTemplateForm):
class ModuleBayTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet('device_type', 'name', 'label', 'position', 'description'),
FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
)

class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
'device_type', 'module_type', 'name', 'label', 'position', 'description',
]


Expand Down Expand Up @@ -1453,15 +1453,15 @@ class Meta:
]


class ModuleBayForm(DeviceComponentForm):
class ModuleBayForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet('device', 'name', 'label', 'position', 'description', 'tags',),
FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
)

class Meta:
model = ModuleBay
fields = [
'device', 'name', 'label', 'position', 'description', 'tags',
'device', 'module', 'name', 'label', 'position', 'description', 'tags',
]


Expand Down
12 changes: 9 additions & 3 deletions netbox/dcim/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,20 +496,26 @@ class ModuleType(NetBoxObjectType):

@strawberry_django.type(
models.ModuleBay,
fields='__all__',
# fields='__all__',
exclude=('parent',),
filters=ModuleBayFilter
)
class ModuleBayType(ComponentType):
class ModuleBayType(ModularComponentType):

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(
models.ModuleBayTemplate,
fields='__all__',
filters=ModuleBayTemplateFilter
)
class ModuleBayTemplateType(ComponentTemplateType):
class ModuleBayTemplateType(ModularComponentTemplateType):
_name: str


Expand Down
74 changes: 74 additions & 0 deletions netbox/dcim/migrations/0190_nested_modules.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
41 changes: 34 additions & 7 deletions netbox/dcim/models/device_component_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,41 @@ def clean(self):
_("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):
if MODULE_TOKEN not in self.name:
return self.name

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

def resolve_label(self, module):
if MODULE_TOKEN not in self.label:
return self.label

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


Expand Down Expand Up @@ -628,7 +655,7 @@ def to_yaml(self):
}


class ModuleBayTemplate(ComponentTemplateModel):
class ModuleBayTemplate(ModularComponentTemplateModel):
"""
A template for a ModuleBay to be created for a new parent Device.
"""
Expand All @@ -641,16 +668,16 @@ class ModuleBayTemplate(ComponentTemplateModel):

component_model = ModuleBay

class Meta(ComponentTemplateModel.Meta):
class Meta(ModularComponentTemplateModel.Meta):
verbose_name = _('module bay template')
verbose_name_plural = _('module bay templates')

def instantiate(self, device):
def instantiate(self, **kwargs):
return self.component_model(
device=device,
name=self.name,
label=self.label,
position=self.position
position=self.position,
**kwargs
)
instantiate.do_not_call_in_templates = True

Expand Down
Loading