Skip to content

Commit

Permalink
13230 Allow Devices to be excluded from Rack utilization (#14099)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
arthanson and jeremystretch authored Oct 24, 2023
1 parent ae447bd commit 7274e75
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 16 deletions.
6 changes: 3 additions & 3 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,9 @@ 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',
'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
'exclude_from_utilization', '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',
Expand Down
3 changes: 2 additions & 1 deletion netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = DeviceType
fields = [
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role',
'airflow', 'weight', 'weight_unit',
]

def search(self, queryset, name, value):
Expand Down
10 changes: 9 additions & 1 deletion netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
widget=BulkEditNullBooleanSelect(),
label=_('Is full depth')
)
exclude_from_utilization = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect(),
label=_('Exclude from utilization')
)
airflow = forms.ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
Expand All @@ -445,7 +450,10 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):

model = DeviceType
fieldsets = (
(_('Device Type'), ('manufacturer', 'default_platform', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
(_('Device Type'), (
'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'airflow', 'description',
)),
(_('Weight'), ('weight', 'weight_unit')),
)
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
class Meta:
model = DeviceType
fields = [
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
]


Expand Down
9 changes: 5 additions & 4 deletions netbox/dcim/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,17 +302,18 @@ class DeviceTypeForm(NetBoxModelForm):
fieldsets = (
(_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
(_('Chassis'), (
'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
'weight', 'weight_unit',
)),
(_('Images'), ('front_image', 'rear_image')),
)

class Meta:
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
'comments', 'tags',
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
'description', 'comments', 'tags',
]
widgets = {
'front_image': ClearableFileInput(attrs={
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/migrations/0182_devicetype_exclude_from_utilization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-10-20 22:30

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('dcim', '0181_rename_device_role_device_role'),
]

operations = [
migrations.AddField(
model_name='devicetype',
name='exclude_from_utilization',
field=models.BooleanField(default=False),
),
]
5 changes: 5 additions & 0 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
default=1.0,
verbose_name=_('height (U)')
)
exclude_from_utilization = models.BooleanField(
default=False,
verbose_name=_('exclude from utilization'),
help_text=_('Exclude from rack utilization calculations.')
)
is_full_depth = models.BooleanField(
default=True,
verbose_name=_('is full depth'),
Expand Down
8 changes: 6 additions & 2 deletions netbox/dcim/models/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=N

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

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

if exclude is not None:
devices = devices.exclude(pk__in=exclude)

Expand Down Expand Up @@ -453,7 +457,7 @@ def get_utilization(self):
"""
# Determine unoccupied units
total_units = len(list(self.units))
available_units = self.get_available_units(u_height=0.5)
available_units = self.get_available_units(u_height=0.5, ignore_excluded_devices=True)

# Remove reserved units
for ru in self.get_reserved_units():
Expand Down
7 changes: 4 additions & 3 deletions netbox/dcim/tables/devicetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class DeviceTypeTable(NetBoxTable):
verbose_name=_('U Height'),
template_code='{{ value|floatformat }}'
)
exclude_from_utilization = columns.BooleanColumn()
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
Expand Down Expand Up @@ -142,9 +143,9 @@ class DeviceTypeTable(NetBoxTable):
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',
'exclude_from_utilization', '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',
Expand Down
34 changes: 34 additions & 0 deletions netbox/dcim/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,40 @@ def test_change_rack_site(self):
# Check that Device1 is now assigned to Site B
self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)

def test_utilization(self):
site = Site.objects.first()
rack = Rack.objects.first()

Device(
name='Device 1',
role=DeviceRole.objects.first(),
device_type=DeviceType.objects.first(),
site=site,
rack=rack,
position=1
).save()
rack.refresh_from_db()
self.assertEqual(rack.get_utilization(), 1 / 42 * 100)

# create device excluded from utilization calculations
dt = DeviceType.objects.create(
manufacturer=Manufacturer.objects.first(),
model='Device Type 4',
slug='device-type-4',
u_height=1,
exclude_from_utilization=True
)
Device(
name='Device 2',
role=DeviceRole.objects.first(),
device_type=dt,
site=site,
rack=rack,
position=5
).save()
rack.refresh_from_db()
self.assertEqual(rack.get_utilization(), 1 / 42 * 100)


class DeviceTestCase(TestCase):

Expand Down
4 changes: 4 additions & 0 deletions netbox/templates/dcim/devicetype.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ <h5 class="card-header">
<td>{% trans "Height (U" %})</td>
<td>{{ object.u_height|floatformat }}</td>
</tr>
<tr>
<td>{% trans "Exclude From Utilization" %})</td>
<td>{% checkmark object.exclude_from_utilization %}</td>
</tr>
<tr>
<td>{% trans "Full Depth" %}</td>
<td>{% checkmark object.is_full_depth %}</td>
Expand Down

0 comments on commit 7274e75

Please sign in to comment.