Skip to content

Commit 149a496

Browse files
6347 Cache the number of each component type assigned to devices/VMs (#12632)
--------- Co-authored-by: Jeremy Stretch <[email protected]>
1 parent a4acb50 commit 149a496

23 files changed

+623
-35
lines changed

docs/development/application-registry.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The registry can be inspected by importing `registry` from `extras.registry`.
88

99
## Stores
1010

11+
### `counter_fields`
12+
13+
A dictionary mapping of models to foreign keys with which cached counter fields are associated.
14+
1115
### `data_backends`
1216

1317
A dictionary mapping data backend types to their respective classes. These are used to interact with [remote data sources](../models/core/datasource.md).

netbox/dcim/api/serializers.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -669,14 +669,28 @@ class DeviceSerializer(NetBoxModelSerializer):
669669
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
670670
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
671671

672+
# Counter fields
673+
console_port_count = serializers.IntegerField(read_only=True)
674+
console_server_port_count = serializers.IntegerField(read_only=True)
675+
power_port_count = serializers.IntegerField(read_only=True)
676+
power_outlet_count = serializers.IntegerField(read_only=True)
677+
interface_count = serializers.IntegerField(read_only=True)
678+
front_port_count = serializers.IntegerField(read_only=True)
679+
rear_port_count = serializers.IntegerField(read_only=True)
680+
device_bay_count = serializers.IntegerField(read_only=True)
681+
module_bay_count = serializers.IntegerField(read_only=True)
682+
inventory_item_count = serializers.IntegerField(read_only=True)
683+
672684
class Meta:
673685
model = Device
674686
fields = [
675687
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
676-
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
677-
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
678-
'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created',
679-
'last_updated',
688+
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status',
689+
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
690+
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
691+
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
692+
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
693+
'module_bay_count', 'inventory_item_count',
680694
]
681695

682696
@extend_schema_field(NestedDeviceSerializer)
@@ -700,7 +714,9 @@ class Meta(DeviceSerializer.Meta):
700714
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
701715
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
702716
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template',
703-
'created', 'last_updated',
717+
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
718+
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
719+
'module_bay_count', 'inventory_item_count',
704720
]
705721

706722
@extend_schema_field(serializers.JSONField(allow_null=True))

netbox/dcim/apps.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class DCIMConfig(AppConfig):
99

1010
def ready(self):
1111
from . import signals, search
12-
from .models import CableTermination
12+
from .models import CableTermination, Device
13+
from utilities.counters import connect_counters
1314

1415
# Register denormalized fields
1516
denormalized.register(CableTermination, '_device', {
@@ -24,3 +25,6 @@ def ready(self):
2425
denormalized.register(CableTermination, '_location', {
2526
'_site': 'site',
2627
})
28+
29+
# Register counters
30+
connect_counters(Device)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from django.db import migrations
2+
from django.db.models import Count
3+
4+
import utilities.fields
5+
6+
7+
def recalculate_device_counts(apps, schema_editor):
8+
Device = apps.get_model("dcim", "Device")
9+
devices = list(Device.objects.all().annotate(
10+
_console_port_count=Count('consoleports', distinct=True),
11+
_console_server_port_count=Count('consoleserverports', distinct=True),
12+
_power_port_count=Count('powerports', distinct=True),
13+
_power_outlet_count=Count('poweroutlets', distinct=True),
14+
_interface_count=Count('interfaces', distinct=True),
15+
_front_port_count=Count('frontports', distinct=True),
16+
_rear_port_count=Count('rearports', distinct=True),
17+
_device_bay_count=Count('devicebays', distinct=True),
18+
_module_bay_count=Count('modulebays', distinct=True),
19+
_inventory_item_count=Count('inventoryitems', distinct=True),
20+
))
21+
22+
for device in devices:
23+
device.console_port_count = device._console_port_count
24+
device.console_server_port_count = device._console_server_port_count
25+
device.power_port_count = device._power_port_count
26+
device.power_outlet_count = device._power_outlet_count
27+
device.interface_count = device._interface_count
28+
device.front_port_count = device._front_port_count
29+
device.rear_port_count = device._rear_port_count
30+
device.device_bay_count = device._device_bay_count
31+
device.module_bay_count = device._module_bay_count
32+
device.inventory_item_count = device._inventory_item_count
33+
34+
Device.objects.bulk_update(devices, [
35+
'console_port_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
36+
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
37+
])
38+
39+
40+
class Migration(migrations.Migration):
41+
dependencies = [
42+
('dcim', '0174_rack_starting_unit'),
43+
]
44+
45+
operations = [
46+
migrations.AddField(
47+
model_name='device',
48+
name='console_port_count',
49+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsolePort'),
50+
),
51+
migrations.AddField(
52+
model_name='device',
53+
name='console_server_port_count',
54+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsoleServerPort'),
55+
),
56+
migrations.AddField(
57+
model_name='device',
58+
name='power_port_count',
59+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerPort'),
60+
),
61+
migrations.AddField(
62+
model_name='device',
63+
name='power_outlet_count',
64+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerOutlet'),
65+
),
66+
migrations.AddField(
67+
model_name='device',
68+
name='interface_count',
69+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.Interface'),
70+
),
71+
migrations.AddField(
72+
model_name='device',
73+
name='front_port_count',
74+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.FrontPort'),
75+
),
76+
migrations.AddField(
77+
model_name='device',
78+
name='rear_port_count',
79+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.RearPort'),
80+
),
81+
migrations.AddField(
82+
model_name='device',
83+
name='device_bay_count',
84+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.DeviceBay'),
85+
),
86+
migrations.AddField(
87+
model_name='device',
88+
name='module_bay_count',
89+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ModuleBay'),
90+
),
91+
migrations.AddField(
92+
model_name='device',
93+
name='inventory_item_count',
94+
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.InventoryItem'),
95+
),
96+
migrations.RunPython(
97+
recalculate_device_counts,
98+
reverse_code=migrations.RunPython.noop
99+
),
100+
]

netbox/dcim/models/device_components.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from utilities.mptt import TreeManager
2020
from utilities.ordering import naturalize_interface
2121
from utilities.query_functions import CollateAsChar
22+
from utilities.tracking import TrackingModelMixin
2223
from wireless.choices import *
2324
from wireless.utils import get_channel_attr
2425

@@ -269,7 +270,7 @@ def connected_endpoints(self):
269270
# Console components
270271
#
271272

272-
class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
273+
class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
273274
"""
274275
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
275276
"""
@@ -292,7 +293,7 @@ def get_absolute_url(self):
292293
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
293294

294295

295-
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
296+
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
296297
"""
297298
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
298299
"""
@@ -319,7 +320,7 @@ def get_absolute_url(self):
319320
# Power components
320321
#
321322

322-
class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
323+
class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
323324
"""
324325
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
325326
"""
@@ -428,7 +429,7 @@ def get_power_draw(self):
428429
}
429430

430431

431-
class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
432+
class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
432433
"""
433434
A physical power outlet (output) within a Device which provides power to a PowerPort.
434435
"""
@@ -537,7 +538,7 @@ def count_fhrp_groups(self):
537538
return self.fhrp_group_assignments.count()
538539

539540

540-
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint):
541+
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
541542
"""
542543
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
543544
"""
@@ -888,7 +889,7 @@ def l2vpn_termination(self):
888889
# Pass-through ports
889890
#
890891

891-
class FrontPort(ModularComponentModel, CabledObjectModel):
892+
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
892893
"""
893894
A pass-through port on the front of a Device.
894895
"""
@@ -949,7 +950,7 @@ def clean(self):
949950
})
950951

951952

952-
class RearPort(ModularComponentModel, CabledObjectModel):
953+
class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
953954
"""
954955
A pass-through port on the rear of a Device.
955956
"""
@@ -990,7 +991,7 @@ def clean(self):
990991
# Bays
991992
#
992993

993-
class ModuleBay(ComponentModel):
994+
class ModuleBay(ComponentModel, TrackingModelMixin):
994995
"""
995996
An empty space within a Device which can house a child device
996997
"""
@@ -1006,7 +1007,7 @@ def get_absolute_url(self):
10061007
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
10071008

10081009

1009-
class DeviceBay(ComponentModel):
1010+
class DeviceBay(ComponentModel, TrackingModelMixin):
10101011
"""
10111012
An empty space within a Device which can house a child device
10121013
"""
@@ -1064,7 +1065,7 @@ def get_absolute_url(self):
10641065
return reverse('dcim:inventoryitemrole', args=[self.pk])
10651066

10661067

1067-
class InventoryItem(MPTTModel, ComponentModel):
1068+
class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
10681069
"""
10691070
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
10701071
InventoryItems are used only for inventory purposes.

netbox/dcim/models/devices.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from netbox.config import ConfigItem
2222
from netbox.models import OrganizationalModel, PrimaryModel
2323
from utilities.choices import ColorChoices
24-
from utilities.fields import ColorField, NaturalOrderingField
24+
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
2525
from .device_components import *
2626
from .mixins import WeightMixin
2727

@@ -639,6 +639,48 @@ class Device(PrimaryModel, ConfigContextModel):
639639
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
640640
)
641641

642+
# Counter fields
643+
console_port_count = CounterCacheField(
644+
to_model='dcim.ConsolePort',
645+
to_field='device'
646+
)
647+
console_server_port_count = CounterCacheField(
648+
to_model='dcim.ConsoleServerPort',
649+
to_field='device'
650+
)
651+
power_port_count = CounterCacheField(
652+
to_model='dcim.PowerPort',
653+
to_field='device'
654+
)
655+
power_outlet_count = CounterCacheField(
656+
to_model='dcim.PowerOutlet',
657+
to_field='device'
658+
)
659+
interface_count = CounterCacheField(
660+
to_model='dcim.Interface',
661+
to_field='device'
662+
)
663+
front_port_count = CounterCacheField(
664+
to_model='dcim.FrontPort',
665+
to_field='device'
666+
)
667+
rear_port_count = CounterCacheField(
668+
to_model='dcim.RearPort',
669+
to_field='device'
670+
)
671+
device_bay_count = CounterCacheField(
672+
to_model='dcim.DeviceBay',
673+
to_field='device'
674+
)
675+
module_bay_count = CounterCacheField(
676+
to_model='dcim.ModuleBay',
677+
to_field='device'
678+
)
679+
inventory_item_count = CounterCacheField(
680+
to_model='dcim.InventoryItem',
681+
to_field='device'
682+
)
683+
642684
# Generic relations
643685
contacts = GenericRelation(
644686
to='tenancy.ContactAssignment'

netbox/dcim/tables/devices.py

+33-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import django_tables2 as tables
2-
from dcim import models
32
from django_tables2.utils import Accessor
4-
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
3+
from django.utils.translation import gettext as _
54

5+
from dcim import models
66
from netbox.tables import NetBoxTable, columns
7-
7+
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
88
from .template_code import *
99

1010
__all__ = (
@@ -230,6 +230,36 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
230230
tags = columns.TagColumn(
231231
url_name='dcim:device_list'
232232
)
233+
console_port_count = tables.Column(
234+
verbose_name=_('Console ports')
235+
)
236+
console_server_port_count = tables.Column(
237+
verbose_name=_('Console server ports')
238+
)
239+
power_port_count = tables.Column(
240+
verbose_name=_('Power ports')
241+
)
242+
power_outlet_count = tables.Column(
243+
verbose_name=_('Power outlets')
244+
)
245+
interface_count = tables.Column(
246+
verbose_name=_('Interfaces')
247+
)
248+
front_port_count = tables.Column(
249+
verbose_name=_('Front ports')
250+
)
251+
rear_port_count = tables.Column(
252+
verbose_name=_('Rear ports')
253+
)
254+
device_bay_count = tables.Column(
255+
verbose_name=_('Device bays')
256+
)
257+
module_bay_count = tables.Column(
258+
verbose_name=_('Module bays')
259+
)
260+
inventory_item_count = tables.Column(
261+
verbose_name=_('Inventory items')
262+
)
233263

234264
class Meta(NetBoxTable.Meta):
235265
model = models.Device

0 commit comments

Comments
 (0)