From 5b5444f414da9ef94f644e0414ecde7a06770121 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2023 16:17:58 -0400 Subject: [PATCH] Closes #13269: Cache component template counts on device types --- netbox/dcim/api/serializers.py | 22 +++- netbox/dcim/apps.py | 4 +- .../0176_device_component_counters.py | 12 +- .../0177_devicetype_component_counters.py | 108 ++++++++++++++++++ ...=> 0178_virtual_chassis_member_counter.py} | 2 +- .../dcim/models/device_component_templates.py | 3 +- netbox/dcim/models/devices.py | 42 +++++++ netbox/dcim/tables/devicetypes.py | 46 ++++++-- netbox/dcim/views.py | 20 ++-- 9 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 netbox/dcim/migrations/0177_devicetype_component_counters.py rename netbox/dcim/migrations/{0177_virtual_chassis_member_counter.py => 0178_virtual_chassis_member_counter.py} (94%) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index bde32c871..04929b079 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -327,12 +327,28 @@ class DeviceTypeSerializer(NetBoxModelSerializer): weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) device_count = serializers.IntegerField(read_only=True) + # Counter fields + console_port_template_count = serializers.IntegerField(read_only=True) + console_server_port_template_count = serializers.IntegerField(read_only=True) + power_port_template_count = serializers.IntegerField(read_only=True) + power_outlet_template_count = serializers.IntegerField(read_only=True) + interface_template_count = serializers.IntegerField(read_only=True) + front_port_template_count = serializers.IntegerField(read_only=True) + rear_port_template_count = serializers.IntegerField(read_only=True) + device_bay_template_count = serializers.IntegerField(read_only=True) + module_bay_template_count = serializers.IntegerField(read_only=True) + inventory_item_template_count = serializers.IntegerField(read_only=True) + class Meta: model = DeviceType fields = [ - 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', + 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', + 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', + 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', + 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', + 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', + 'inventory_item_template_count', ] diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index 38e9b9c6b..78ff0d4c1 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -9,7 +9,7 @@ class DCIMConfig(AppConfig): def ready(self): from . import signals, search - from .models import CableTermination, Device, VirtualChassis + from .models import CableTermination, Device, DeviceType, VirtualChassis from utilities.counters import connect_counters # Register denormalized fields @@ -27,4 +27,4 @@ class DCIMConfig(AppConfig): }) # Register counters - connect_counters(Device, VirtualChassis) + connect_counters(Device, DeviceType, VirtualChassis) diff --git a/netbox/dcim/migrations/0176_device_component_counters.py b/netbox/dcim/migrations/0176_device_component_counters.py index fc22de81b..b570ddbd5 100644 --- a/netbox/dcim/migrations/0176_device_component_counters.py +++ b/netbox/dcim/migrations/0176_device_component_counters.py @@ -32,8 +32,16 @@ def recalculate_device_counts(apps, schema_editor): device.inventory_item_count = device._inventory_item_count Device.objects.bulk_update(devices, [ - 'console_port_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'console_port_count', + 'console_server_port_count', + 'power_port_count', + 'power_outlet_count', + 'interface_count', + 'front_port_count', + 'rear_port_count', + 'device_bay_count', + 'module_bay_count', + 'inventory_item_count', ]) diff --git a/netbox/dcim/migrations/0177_devicetype_component_counters.py b/netbox/dcim/migrations/0177_devicetype_component_counters.py new file mode 100644 index 000000000..66d1460d9 --- /dev/null +++ b/netbox/dcim/migrations/0177_devicetype_component_counters.py @@ -0,0 +1,108 @@ +from django.db import migrations +from django.db.models import Count + +import utilities.fields + + +def recalculate_devicetype_template_counts(apps, schema_editor): + DeviceType = apps.get_model("dcim", "DeviceType") + device_types = list(DeviceType.objects.all().annotate( + _console_port_template_count=Count('consoleporttemplates', distinct=True), + _console_server_port_template_count=Count('consoleserverporttemplates', distinct=True), + _power_port_template_count=Count('powerporttemplates', distinct=True), + _power_outlet_template_count=Count('poweroutlettemplates', distinct=True), + _interface_template_count=Count('interfacetemplates', distinct=True), + _front_port_template_count=Count('frontporttemplates', distinct=True), + _rear_port_template_count=Count('rearporttemplates', distinct=True), + _device_bay_template_count=Count('devicebaytemplates', distinct=True), + _module_bay_template_count=Count('modulebaytemplates', distinct=True), + _inventory_item_template_count=Count('inventoryitemtemplates', distinct=True), + )) + + for devicetype in device_types: + devicetype.console_port_template_count = devicetype._console_port_template_count + devicetype.console_server_port_template_count = devicetype._console_server_port_template_count + devicetype.power_port_template_count = devicetype._power_port_template_count + devicetype.power_outlet_template_count = devicetype._power_outlet_template_count + devicetype.interface_template_count = devicetype._interface_template_count + devicetype.front_port_template_count = devicetype._front_port_template_count + devicetype.rear_port_template_count = devicetype._rear_port_template_count + devicetype.device_bay_template_count = devicetype._device_bay_template_count + devicetype.module_bay_template_count = devicetype._module_bay_template_count + devicetype.inventory_item_template_count = devicetype._inventory_item_template_count + + DeviceType.objects.bulk_update(device_types, [ + 'console_port_template_count', + 'console_server_port_template_count', + 'power_port_template_count', + 'power_outlet_template_count', + 'interface_template_count', + 'front_port_template_count', + 'rear_port_template_count', + 'device_bay_template_count', + 'module_bay_template_count', + 'inventory_item_template_count', + ]) + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0176_device_component_counters'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='console_port_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsolePortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='console_server_port_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsoleServerPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='power_port_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='power_outlet_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerOutletTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='interface_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InterfaceTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='front_port_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.FrontPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='rear_port_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.RearPortTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='device_bay_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.DeviceBayTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='module_bay_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ModuleBayTemplate'), + ), + migrations.AddField( + model_name='devicetype', + name='inventory_item_template_count', + field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InventoryItemTemplate'), + ), + migrations.RunPython( + recalculate_devicetype_template_counts, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/dcim/migrations/0177_virtual_chassis_member_counter.py b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py similarity index 94% rename from netbox/dcim/migrations/0177_virtual_chassis_member_counter.py rename to netbox/dcim/migrations/0178_virtual_chassis_member_counter.py index 2c6863f5c..e3ade1344 100644 --- a/netbox/dcim/migrations/0177_virtual_chassis_member_counter.py +++ b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py @@ -17,7 +17,7 @@ def populate_virtualchassis_members(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0176_device_component_counters'), + ('dcim', '0177_devicetype_component_counters'), ] operations = [ diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 0355d7028..7d669bca0 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -12,6 +12,7 @@ from netbox.models import ChangeLoggedModel from utilities.fields import ColorField, NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface +from utilities.tracking import TrackingModelMixin from .device_components import ( ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, RearPort, @@ -32,7 +33,7 @@ __all__ = ( ) -class ComponentTemplateModel(ChangeLoggedModel): +class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin): device_type = models.ForeignKey( to='dcim.DeviceType', on_delete=models.CASCADE, diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6eed6b09d..4aba73fde 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -129,6 +129,48 @@ class DeviceType(PrimaryModel, WeightMixin): blank=True ) + # Counter fields + console_port_template_count = CounterCacheField( + to_model='dcim.ConsolePortTemplate', + to_field='device_type' + ) + console_server_port_template_count = CounterCacheField( + to_model='dcim.ConsoleServerPortTemplate', + to_field='device_type' + ) + power_port_template_count = CounterCacheField( + to_model='dcim.PowerPortTemplate', + to_field='device_type' + ) + power_outlet_template_count = CounterCacheField( + to_model='dcim.PowerOutletTemplate', + to_field='device_type' + ) + interface_template_count = CounterCacheField( + to_model='dcim.InterfaceTemplate', + to_field='device_type' + ) + front_port_template_count = CounterCacheField( + to_model='dcim.FrontPortTemplate', + to_field='device_type' + ) + rear_port_template_count = CounterCacheField( + to_model='dcim.RearPortTemplate', + to_field='device_type' + ) + device_bay_template_count = CounterCacheField( + to_model='dcim.DeviceBayTemplate', + to_field='device_type' + ) + module_bay_template_count = CounterCacheField( + to_model='dcim.ModuleBayTemplate', + to_field='device_type' + ) + inventory_item_template_count = CounterCacheField( + to_model='dcim.InventoryItemTemplate', + to_field='device_type' + ) + images = GenericRelation( to='extras.ImageAttachment' ) diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 0536e8940..65d0c1707 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -1,4 +1,5 @@ import django_tables2 as tables +from django.utils.translation import gettext as _ from dcim import models from netbox.tables import NetBoxTable, columns @@ -83,11 +84,6 @@ class DeviceTypeTable(NetBoxTable): is_full_depth = columns.BooleanColumn( verbose_name='Full Depth' ) - instance_count = columns.LinkedCountColumn( - viewname='dcim:device_list', - url_params={'device_type_id': 'pk'}, - verbose_name='Instances' - ) comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='dcim:devicetype_list' @@ -99,12 +95,48 @@ class DeviceTypeTable(NetBoxTable): template_code=WEIGHT, order_by=('_abs_weight', 'weight_unit') ) + instance_count = columns.LinkedCountColumn( + viewname='dcim:device_list', + url_params={'device_type_id': 'pk'}, + verbose_name='Instances' + ) + console_port_template_count = tables.Column( + verbose_name=_('Console ports') + ) + console_server_port_template_count = tables.Column( + verbose_name=_('Console server ports') + ) + power_port_template_count = tables.Column( + verbose_name=_('Power ports') + ) + power_outlet_template_count = tables.Column( + verbose_name=_('Power outlets') + ) + interface_template_count = tables.Column( + verbose_name=_('Interfaces') + ) + front_port_template_count = tables.Column( + verbose_name=_('Front ports') + ) + rear_port_template_count = tables.Column( + verbose_name=_('Rear ports') + ) + device_bay_template_count = tables.Column( + verbose_name=_('Device bays') + ) + module_bay_template_count = tables.Column( + verbose_name=_('Module bays') + ) + inventory_item_template_count = tables.Column( + verbose_name=_('Inventory items') + ) class Meta(NetBoxTable.Meta): model = models.DeviceType fields = ( - 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', - 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated', + 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth', + 'subdevice_role', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', + 'last_updated', ) default_columns = ( 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9c6fd6b44..8611c136d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -951,7 +951,7 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_consoleports' tab = ViewTab( label=_('Console Ports'), - badge=lambda obj: obj.consoleporttemplates.count(), + badge=lambda obj: obj.console_port_template_count, permission='dcim.view_consoleporttemplate', weight=550, hide_if_empty=True @@ -966,7 +966,7 @@ class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_consoleserverports' tab = ViewTab( label=_('Console Server Ports'), - badge=lambda obj: obj.consoleserverporttemplates.count(), + badge=lambda obj: obj.console_server_port_template_count, permission='dcim.view_consoleserverporttemplate', weight=560, hide_if_empty=True @@ -981,7 +981,7 @@ class DeviceTypePowerPortsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_powerports' tab = ViewTab( label=_('Power Ports'), - badge=lambda obj: obj.powerporttemplates.count(), + badge=lambda obj: obj.power_port_template_count, permission='dcim.view_powerporttemplate', weight=570, hide_if_empty=True @@ -996,7 +996,7 @@ class DeviceTypePowerOutletsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_poweroutlets' tab = ViewTab( label=_('Power Outlets'), - badge=lambda obj: obj.poweroutlettemplates.count(), + badge=lambda obj: obj.power_outlet_template_count, permission='dcim.view_poweroutlettemplate', weight=580, hide_if_empty=True @@ -1011,7 +1011,7 @@ class DeviceTypeInterfacesView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_interfaces' tab = ViewTab( label=_('Interfaces'), - badge=lambda obj: obj.interfacetemplates.count(), + badge=lambda obj: obj.interface_template_count, permission='dcim.view_interfacetemplate', weight=520, hide_if_empty=True @@ -1026,7 +1026,7 @@ class DeviceTypeFrontPortsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_frontports' tab = ViewTab( label=_('Front Ports'), - badge=lambda obj: obj.frontporttemplates.count(), + badge=lambda obj: obj.front_port_template_count, permission='dcim.view_frontporttemplate', weight=530, hide_if_empty=True @@ -1041,7 +1041,7 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_rearports' tab = ViewTab( label=_('Rear Ports'), - badge=lambda obj: obj.rearporttemplates.count(), + badge=lambda obj: obj.rear_port_template_count, permission='dcim.view_rearporttemplate', weight=540, hide_if_empty=True @@ -1056,7 +1056,7 @@ class DeviceTypeModuleBaysView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_modulebays' tab = ViewTab( label=_('Module Bays'), - badge=lambda obj: obj.modulebaytemplates.count(), + badge=lambda obj: obj.module_bay_template_count, permission='dcim.view_modulebaytemplate', weight=510, hide_if_empty=True @@ -1071,7 +1071,7 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_devicebays' tab = ViewTab( label=_('Device Bays'), - badge=lambda obj: obj.devicebaytemplates.count(), + badge=lambda obj: obj.device_bay_template_count, permission='dcim.view_devicebaytemplate', weight=500, hide_if_empty=True @@ -1086,7 +1086,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView): viewname = 'dcim:devicetype_inventoryitems' tab = ViewTab( label=_('Inventory Items'), - badge=lambda obj: obj.inventoryitemtemplates.count(), + badge=lambda obj: obj.inventory_item_template_count, permission='dcim.view_invenotryitemtemplate', weight=590, hide_if_empty=True