Skip to content

Commit cb15117

Browse files
committed
Closes #19740: Enable recursive nesting for platforms
1 parent 33d891e commit cb15117

File tree

22 files changed

+283
-38
lines changed

22 files changed

+283
-38
lines changed

docs/models/dcim/platform.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
A platform defines the type of software running on a [device](./device.md) or [virtual machine](../virtualization/virtualmachine.md). This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15.
44

5+
Platforms may be nested under parents to form a hierarchy. For example, platforms named "Debian" and "RHEL" might both be created under a generic "Linux" parent.
6+
57
Platforms may optionally be limited by [manufacturer](./manufacturer.md): If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
68

7-
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
9+
The assignment of platforms to devices and virtual machines is an optional.
810

911
## Fields
1012

13+
## Parent
14+
15+
!!! "This field was introduced in NetBox v4.4."
16+
17+
The parent platform class to which this platform belongs (optional).
18+
1119
### Name
1220

1321
A human-friendly name for the platform. Must be unique per manufacturer.

netbox/dcim/api/serializers_/nested.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
__all__ = (
88
'NestedDeviceBaySerializer',
9+
'NestedDeviceRoleSerializer',
910
'NestedDeviceSerializer',
1011
'NestedInterfaceSerializer',
1112
'NestedInterfaceTemplateSerializer',
1213
'NestedLocationSerializer',
1314
'NestedModuleBaySerializer',
15+
'NestedPlatformSerializer',
1416
'NestedRegionSerializer',
1517
'NestedSiteGroupSerializer',
1618
)
@@ -102,3 +104,10 @@ class NestedModuleBaySerializer(WritableNestedSerializer):
102104
class Meta:
103105
model = models.ModuleBay
104106
fields = ['id', 'url', 'display_url', 'display', 'name']
107+
108+
109+
class NestedPlatformSerializer(WritableNestedSerializer):
110+
111+
class Meta:
112+
model = models.Platform
113+
fields = ['id', 'url', 'display_url', 'display', 'name']
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from dcim.models import Platform
22
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
33
from netbox.api.fields import RelatedObjectCountField
4-
from netbox.api.serializers import NetBoxModelSerializer
4+
from netbox.api.serializers import NestedGroupModelSerializer
55
from .manufacturers import ManufacturerSerializer
6+
from .nested import NestedPlatformSerializer
67

78
__all__ = (
89
'PlatformSerializer',
910
)
1011

1112

12-
class PlatformSerializer(NetBoxModelSerializer):
13+
class PlatformSerializer(NestedGroupModelSerializer):
14+
parent = NestedPlatformSerializer(required=False, allow_null=True, default=None)
1315
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
1416
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
1517

@@ -20,7 +22,10 @@ class PlatformSerializer(NetBoxModelSerializer):
2022
class Meta:
2123
model = Platform
2224
fields = [
23-
'id', 'url', 'display_url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description',
24-
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
25+
'id', 'url', 'display_url', 'display', 'parent', 'name', 'slug', 'manufacturer', 'config_template',
26+
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
27+
'_depth',
2528
]
26-
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
29+
brief_fields = (
30+
'id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count', '_depth',
31+
)

netbox/dcim/filtersets.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -547,14 +547,17 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
547547
to_field_name='slug',
548548
label=_('Manufacturer (slug)'),
549549
)
550-
default_platform_id = django_filters.ModelMultipleChoiceFilter(
550+
default_platform_id = TreeNodeMultipleChoiceFilter(
551551
queryset=Platform.objects.all(),
552+
field_name='default_platform',
553+
lookup_expr='in',
552554
label=_('Default platform (ID)'),
553555
)
554-
default_platform = django_filters.ModelMultipleChoiceFilter(
555-
field_name='default_platform__slug',
556+
default_platform = TreeNodeMultipleChoiceFilter(
556557
queryset=Platform.objects.all(),
558+
field_name='default_platform',
557559
to_field_name='slug',
560+
lookup_expr='in',
558561
label=_('Default platform (slug)'),
559562
)
560563
has_front_image = django_filters.BooleanFilter(
@@ -979,6 +982,29 @@ class Meta:
979982

980983

981984
class PlatformFilterSet(OrganizationalModelFilterSet):
985+
parent_id = django_filters.ModelMultipleChoiceFilter(
986+
queryset=Platform.objects.all(),
987+
label=_('Immediate parent platform (ID)'),
988+
)
989+
parent = django_filters.ModelMultipleChoiceFilter(
990+
field_name='parent__slug',
991+
queryset=Platform.objects.all(),
992+
to_field_name='slug',
993+
label=_('Immediate parent platform (slug)'),
994+
)
995+
ancestor_id = TreeNodeMultipleChoiceFilter(
996+
queryset=Platform.objects.all(),
997+
field_name='parent',
998+
lookup_expr='in',
999+
label=_('Parent platform (ID)'),
1000+
)
1001+
ancestor = TreeNodeMultipleChoiceFilter(
1002+
queryset=Platform.objects.all(),
1003+
field_name='parent',
1004+
lookup_expr='in',
1005+
to_field_name='slug',
1006+
label=_('Parent platform (slug)'),
1007+
)
9821008
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
9831009
field_name='manufacturer',
9841010
queryset=Manufacturer.objects.all(),
@@ -1058,14 +1084,17 @@ class DeviceFilterSet(
10581084
queryset=Device.objects.all(),
10591085
label=_('Parent Device (ID)'),
10601086
)
1061-
platform_id = django_filters.ModelMultipleChoiceFilter(
1087+
platform_id = TreeNodeMultipleChoiceFilter(
10621088
queryset=Platform.objects.all(),
1089+
field_name='platform',
1090+
lookup_expr='in',
10631091
label=_('Platform (ID)'),
10641092
)
1065-
platform = django_filters.ModelMultipleChoiceFilter(
1066-
field_name='platform__slug',
1093+
platform = TreeNodeMultipleChoiceFilter(
1094+
field_name='platform',
10671095
queryset=Platform.objects.all(),
10681096
to_field_name='slug',
1097+
lookup_expr='in',
10691098
label=_('Platform (slug)'),
10701099
)
10711100
region_id = TreeNodeMultipleChoiceFilter(

netbox/dcim/forms/bulk_edit.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,11 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
682682

683683

684684
class PlatformBulkEditForm(NetBoxModelBulkEditForm):
685+
parent = DynamicModelChoiceField(
686+
label=_('Parent'),
687+
queryset=Platform.objects.all(),
688+
required=False,
689+
)
685690
manufacturer = DynamicModelChoiceField(
686691
label=_('Manufacturer'),
687692
queryset=Manufacturer.objects.all(),
@@ -697,12 +702,13 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
697702
max_length=200,
698703
required=False
699704
)
705+
comments = CommentField()
700706

701707
model = Platform
702708
fieldsets = (
703-
FieldSet('manufacturer', 'config_template', 'description'),
709+
FieldSet('parent', 'manufacturer', 'config_template', 'description'),
704710
)
705-
nullable_fields = ('manufacturer', 'config_template', 'description')
711+
nullable_fields = ('parent', 'manufacturer', 'config_template', 'description', 'comments')
706712

707713

708714
class DeviceBulkEditForm(NetBoxModelBulkEditForm):

netbox/dcim/forms/bulk_import.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ class Meta:
504504

505505
class PlatformImportForm(NetBoxModelImportForm):
506506
slug = SlugField()
507+
parent = CSVModelChoiceField(
508+
label=_('Parent'),
509+
queryset=Platform.objects.all(),
510+
required=False,
511+
to_field_name='name',
512+
help_text=_('Parent platform'),
513+
error_messages={
514+
'invalid_choice': _('Platform not found.'),
515+
}
516+
)
507517
manufacturer = CSVModelChoiceField(
508518
label=_('Manufacturer'),
509519
queryset=Manufacturer.objects.all(),
@@ -522,7 +532,7 @@ class PlatformImportForm(NetBoxModelImportForm):
522532
class Meta:
523533
model = Platform
524534
fields = (
525-
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
535+
'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags',
526536
)
527537

528538

netbox/dcim/forms/filtersets.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,11 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
714714
class PlatformFilterForm(NetBoxModelFilterSetForm):
715715
model = Platform
716716
selector_fields = ('filter_id', 'q', 'manufacturer_id')
717+
parent_id = DynamicModelMultipleChoiceField(
718+
queryset=Platform.objects.all(),
719+
required=False,
720+
label=_('Parent')
721+
)
717722
manufacturer_id = DynamicModelMultipleChoiceField(
718723
queryset=Manufacturer.objects.all(),
719724
required=False,

netbox/dcim/forms/model_forms.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,11 @@ class Meta:
536536

537537

538538
class PlatformForm(NetBoxModelForm):
539+
parent = DynamicModelChoiceField(
540+
label=_('Parent'),
541+
queryset=Platform.objects.all(),
542+
required=False,
543+
)
539544
manufacturer = DynamicModelChoiceField(
540545
label=_('Manufacturer'),
541546
queryset=Manufacturer.objects.all(),
@@ -551,15 +556,18 @@ class PlatformForm(NetBoxModelForm):
551556
label=_('Slug'),
552557
max_length=64
553558
)
559+
comments = CommentField()
554560

555561
fieldsets = (
556-
FieldSet('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform')),
562+
FieldSet(
563+
'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'tags', name=_('Platform'),
564+
),
557565
)
558566

559567
class Meta:
560568
model = Platform
561569
fields = [
562-
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
570+
'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'comments', 'tags',
563571
]
564572

565573

netbox/dcim/graphql/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,8 @@ class ModuleTypeType(NetBoxObjectType):
633633
pagination=True
634634
)
635635
class PlatformType(OrganizationalObjectType):
636+
parent: Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')] | None
637+
children: List[Annotated['PlatformType', strawberry.lazy('dcim.graphql.types')]]
636638
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] | None
637639
config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None
638640

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import django.db.models.deletion
2+
import mptt.fields
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dcim', '0210_interface_tx_power_negative'),
10+
]
11+
12+
operations = [
13+
# Add parent & MPTT fields
14+
migrations.AddField(
15+
model_name='platform',
16+
name='parent',
17+
field=mptt.fields.TreeForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.CASCADE,
21+
related_name='children',
22+
to='dcim.platform'
23+
),
24+
),
25+
migrations.AddField(
26+
model_name='platform',
27+
name='level',
28+
field=models.PositiveIntegerField(default=0, editable=False),
29+
preserve_default=False,
30+
),
31+
migrations.AddField(
32+
model_name='platform',
33+
name='lft',
34+
field=models.PositiveIntegerField(default=0, editable=False),
35+
preserve_default=False,
36+
),
37+
migrations.AddField(
38+
model_name='platform',
39+
name='rght',
40+
field=models.PositiveIntegerField(default=0, editable=False),
41+
preserve_default=False,
42+
),
43+
migrations.AddField(
44+
model_name='platform',
45+
name='tree_id',
46+
field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
47+
preserve_default=False,
48+
),
49+
# Add comments field
50+
migrations.AddField(
51+
model_name='platform',
52+
name='comments',
53+
field=models.TextField(blank=True),
54+
),
55+
]

0 commit comments

Comments
 (0)