Skip to content

Commit 7274e75

Browse files
13230 Allow Devices to be excluded from Rack utilization (#14099)
* 13230 add exclusion flag to device type * 13230 forms, detail views * 13230 add tests * 13230 extraneous model field * 13230 extraneous form field * Update netbox/dcim/forms/bulk_edit.py Co-authored-by: Jeremy Stretch <[email protected]> * 13230 review feedback --------- Co-authored-by: Jeremy Stretch <[email protected]>
1 parent ae447bd commit 7274e75

File tree

11 files changed

+91
-16
lines changed

11 files changed

+91
-16
lines changed

netbox/dcim/api/serializers.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,9 @@ class Meta:
343343
model = DeviceType
344344
fields = [
345345
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
346-
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
347-
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
348-
'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
346+
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
347+
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
348+
'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
349349
'power_outlet_template_count', 'interface_template_count', 'front_port_template_count',
350350
'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count',
351351
'inventory_item_template_count',

netbox/dcim/filtersets.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
496496
class Meta:
497497
model = DeviceType
498498
fields = [
499-
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
499+
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role',
500+
'airflow', 'weight', 'weight_unit',
500501
]
501502

502503
def search(self, queryset, name, value):

netbox/dcim/forms/bulk_edit.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
420420
widget=BulkEditNullBooleanSelect(),
421421
label=_('Is full depth')
422422
)
423+
exclude_from_utilization = forms.NullBooleanField(
424+
required=False,
425+
widget=BulkEditNullBooleanSelect(),
426+
label=_('Exclude from utilization')
427+
)
423428
airflow = forms.ChoiceField(
424429
label=_('Airflow'),
425430
choices=add_blank_choice(DeviceAirflowChoices),
@@ -445,7 +450,10 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
445450

446451
model = DeviceType
447452
fieldsets = (
448-
(_('Device Type'), ('manufacturer', 'default_platform', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
453+
(_('Device Type'), (
454+
'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
455+
'airflow', 'description',
456+
)),
449457
(_('Weight'), ('weight', 'weight_unit')),
450458
)
451459
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')

netbox/dcim/forms/bulk_import.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
335335
class Meta:
336336
model = DeviceType
337337
fields = [
338-
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
339-
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
338+
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
339+
'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
340340
]
341341

342342

netbox/dcim/forms/model_forms.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -302,17 +302,18 @@ class DeviceTypeForm(NetBoxModelForm):
302302
fieldsets = (
303303
(_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
304304
(_('Chassis'), (
305-
'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
305+
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
306+
'weight', 'weight_unit',
306307
)),
307308
(_('Images'), ('front_image', 'rear_image')),
308309
)
309310

310311
class Meta:
311312
model = DeviceType
312313
fields = [
313-
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'is_full_depth',
314-
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
315-
'comments', 'tags',
314+
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
315+
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
316+
'description', 'comments', 'tags',
316317
]
317318
widgets = {
318319
'front_image': ClearableFileInput(attrs={
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.5 on 2023-10-20 22:30
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
('dcim', '0181_rename_device_role_device_role'),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name='devicetype',
14+
name='exclude_from_utilization',
15+
field=models.BooleanField(default=False),
16+
),
17+
]

netbox/dcim/models/devices.py

+5
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
106106
default=1.0,
107107
verbose_name=_('height (U)')
108108
)
109+
exclude_from_utilization = models.BooleanField(
110+
default=False,
111+
verbose_name=_('exclude from utilization'),
112+
help_text=_('Exclude from rack utilization calculations.')
113+
)
109114
is_full_depth = models.BooleanField(
110115
default=True,
111116
verbose_name=_('is full depth'),

netbox/dcim/models/racks.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=N
357357

358358
return [u for u in elevation.values()]
359359

360-
def get_available_units(self, u_height=1, rack_face=None, exclude=None):
360+
def get_available_units(self, u_height=1, rack_face=None, exclude=None, ignore_excluded_devices=False):
361361
"""
362362
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
363363
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
@@ -366,9 +366,13 @@ def get_available_units(self, u_height=1, rack_face=None, exclude=None):
366366
:param u_height: Minimum number of contiguous free units required
367367
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
368368
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
369+
:param ignore_excluded_devices: Ignore devices that are marked to exclude from utilization calculations
369370
"""
370371
# Gather all devices which consume U space within the rack
371372
devices = self.devices.prefetch_related('device_type').filter(position__gte=1)
373+
if ignore_excluded_devices:
374+
devices = devices.exclude(device_type__exclude_from_utilization=True)
375+
372376
if exclude is not None:
373377
devices = devices.exclude(pk__in=exclude)
374378

@@ -453,7 +457,7 @@ def get_utilization(self):
453457
"""
454458
# Determine unoccupied units
455459
total_units = len(list(self.units))
456-
available_units = self.get_available_units(u_height=0.5)
460+
available_units = self.get_available_units(u_height=0.5, ignore_excluded_devices=True)
457461

458462
# Remove reserved units
459463
for ru in self.get_reserved_units():

netbox/dcim/tables/devicetypes.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class DeviceTypeTable(NetBoxTable):
9898
verbose_name=_('U Height'),
9999
template_code='{{ value|floatformat }}'
100100
)
101+
exclude_from_utilization = columns.BooleanColumn()
101102
weight = columns.TemplateColumn(
102103
verbose_name=_('Weight'),
103104
template_code=WEIGHT,
@@ -142,9 +143,9 @@ class DeviceTypeTable(NetBoxTable):
142143
class Meta(NetBoxTable.Meta):
143144
model = models.DeviceType
144145
fields = (
145-
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth',
146-
'subdevice_role', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created',
147-
'last_updated',
146+
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',
147+
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
148+
'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
148149
)
149150
default_columns = (
150151
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',

netbox/dcim/tests/test_models.py

+34
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,40 @@ def test_change_rack_site(self):
238238
# Check that Device1 is now assigned to Site B
239239
self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
240240

241+
def test_utilization(self):
242+
site = Site.objects.first()
243+
rack = Rack.objects.first()
244+
245+
Device(
246+
name='Device 1',
247+
role=DeviceRole.objects.first(),
248+
device_type=DeviceType.objects.first(),
249+
site=site,
250+
rack=rack,
251+
position=1
252+
).save()
253+
rack.refresh_from_db()
254+
self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
255+
256+
# create device excluded from utilization calculations
257+
dt = DeviceType.objects.create(
258+
manufacturer=Manufacturer.objects.first(),
259+
model='Device Type 4',
260+
slug='device-type-4',
261+
u_height=1,
262+
exclude_from_utilization=True
263+
)
264+
Device(
265+
name='Device 2',
266+
role=DeviceRole.objects.first(),
267+
device_type=dt,
268+
site=site,
269+
rack=rack,
270+
position=5
271+
).save()
272+
rack.refresh_from_db()
273+
self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
274+
241275

242276
class DeviceTestCase(TestCase):
243277

netbox/templates/dcim/devicetype.html

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ <h5 class="card-header">
4040
<td>{% trans "Height (U" %})</td>
4141
<td>{{ object.u_height|floatformat }}</td>
4242
</tr>
43+
<tr>
44+
<td>{% trans "Exclude From Utilization" %})</td>
45+
<td>{% checkmark object.exclude_from_utilization %}</td>
46+
</tr>
4347
<tr>
4448
<td>{% trans "Full Depth" %}</td>
4549
<td>{% checkmark object.is_full_depth %}</td>

0 commit comments

Comments
 (0)