From b5b18278934847dc7fe8b90fabf889808d094586 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 25 Mar 2025 13:04:06 -0700 Subject: [PATCH] 18981 Make Device Roles Hierarchical --- netbox/dcim/graphql/types.py | 2 + .../migrations/0203_device_role_nested.py | 56 +++++++++++++++++++ .../migrations/0204_device_role_rebuild.py | 22 ++++++++ netbox/dcim/models/devices.py | 11 +++- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 netbox/dcim/migrations/0203_device_role_nested.py create mode 100644 netbox/dcim/migrations/0204_device_role_rebuild.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 9554a9f60..24fa16263 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -338,6 +338,8 @@ class InventoryItemTemplateType(ComponentTemplateType): pagination=True ) class DeviceRoleType(OrganizationalObjectType): + parent: Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')] | None + children: List[Annotated['DeviceRoleType', strawberry.lazy('dcim.graphql.types')]] color: str config_template: Annotated["ConfigTemplateType", strawberry.lazy('extras.graphql.types')] | None diff --git a/netbox/dcim/migrations/0203_device_role_nested.py b/netbox/dcim/migrations/0203_device_role_nested.py new file mode 100644 index 000000000..c935f789f --- /dev/null +++ b/netbox/dcim/migrations/0203_device_role_nested.py @@ -0,0 +1,56 @@ +# Generated by Django 5.1.7 on 2025-03-25 18:06 + +import django.db.models.manager +import mptt.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0202_location_comments_region_comments_sitegroup_comments'), + ] + + operations = [ + migrations.AlterModelManagers( + name='devicerole', + managers=[ + ('_tree_manager', django.db.models.manager.Manager()), + ], + ), + migrations.AddField( + model_name='devicerole', + name='level', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='devicerole', + name='lft', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='devicerole', + name='rght', + field=models.PositiveIntegerField(default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='devicerole', + name='tree_id', + field=models.PositiveIntegerField(db_index=True, default=0, editable=False), + preserve_default=False, + ), + migrations.AddField( + model_name='devicerole', + name='parent', + field=mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='children', + to='dcim.devicerole', + ), + ), + ] diff --git a/netbox/dcim/migrations/0204_device_role_rebuild.py b/netbox/dcim/migrations/0204_device_role_rebuild.py new file mode 100644 index 000000000..69837c522 --- /dev/null +++ b/netbox/dcim/migrations/0204_device_role_rebuild.py @@ -0,0 +1,22 @@ +from django.db import migrations +import mptt +import mptt.managers + + +def rebuild_mptt(apps, schema_editor): + manager = mptt.managers.TreeManager() + DeviceRole = apps.get_model('dcim', 'DeviceRole') + manager.model = DeviceRole + mptt.register(DeviceRole) + manager.contribute_to_class(DeviceRole, 'objects') + manager.rebuild() + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0203_device_role_nested'), + ] + + operations = [ + migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 2acd98801..601eb770d 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -21,6 +21,7 @@ from dcim.constants import * from dcim.fields import MACAddressField from extras.models import ConfigContextModel, CustomField from extras.querysets import ConfigContextModelQuerySet +from mptt.models import MPTTModel, TreeForeignKey from netbox.choices import ColorChoices from netbox.config import ConfigItem from netbox.models import OrganizationalModel, PrimaryModel @@ -468,12 +469,20 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): # Devices # -class DeviceRole(OrganizationalModel): +class DeviceRole(OrganizationalModel, MPTTModel): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to virtual machines as well. """ + parent = TreeForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='children', + blank=True, + null=True, + db_index=True + ) color = ColorField( verbose_name=_('color'), default=ColorChoices.COLOR_GREY