Skip to content

Commit

Permalink
Closes #10675: Add max_weight field to track maximum load capacity fo…
Browse files Browse the repository at this point in the history
…r racks
  • Loading branch information
jeremystretch committed Dec 9, 2022
1 parent 2b12138 commit 0b100b8
Show file tree
Hide file tree
Showing 19 changed files with 128 additions and 45 deletions.
6 changes: 5 additions & 1 deletion docs/models/dcim/rack.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ The maximum depth of a mounted device that the rack can accommodate, in millimet

The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).

### Maximum Weight

The maximum total weight capacity for all installed devices, inclusive of the rack itself.

### Descending Units

If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
3 changes: 2 additions & 1 deletion docs/release-notes/version-3.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* [#815](https://github.com/netbox-community/netbox/issues/815) - Enable specifying terminations when bulk importing circuits
* [#10371](https://github.com/netbox-community/netbox/issues/10371) - Add operational status field for modules
* [#10675](https://github.com/netbox-community/netbox/issues/10675) - Add `max_weight` field to track maximum load capacity for racks
* [#10945](https://github.com/netbox-community/netbox/issues/10945) - Enabled recurring execution of scheduled reports & scripts
* [#11090](https://github.com/netbox-community/netbox/issues/11090) - Add regular expression support to global search engine
* [#11022](https://github.com/netbox-community/netbox/issues/11022) - Introduce `QUEUE_MAPPINGS` configuration parameter to allow customization of background task prioritization
Expand Down Expand Up @@ -146,7 +147,7 @@ This release introduces a new programmatic API that enables plugins and custom s
* Added `description` and `comments` fields
* dcim.Rack
* Added a `description` field
* Added optional `weight` and `weight_unit` fields
* Added optional `weight`, `max_weight`, and `weight_unit` fields
* dcim.RackReservation
* Added a `comments` field
* dcim.VirtualChassis
Expand Down
6 changes: 3 additions & 3 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ class Meta:
model = Rack
fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'powerfeed_count',
'asset_tag', 'type', 'width', 'u_height', 'weight', 'max_weight', 'weight_unit', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
]


Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'weight_unit'
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit'
]

def search(self, queryset, name, value):
Expand Down
8 changes: 6 additions & 2 deletions netbox/dcim/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
min_value=0,
required=False
)
max_weight = forms.IntegerField(
min_value=0,
required=False
)
weight_unit = forms.ChoiceField(
choices=add_blank_choice(WeightUnitChoices),
required=False,
Expand All @@ -316,11 +320,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
('Hardware', (
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
)),
('Weight', ('weight', 'weight_unit')),
('Weight', ('weight', 'max_weight', 'weight_unit')),
)
nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
'weight_unit', 'description', 'comments',
'max_weight', 'weight_unit', 'description', 'comments',
)


Expand Down
9 changes: 7 additions & 2 deletions netbox/dcim/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,18 @@ class RackImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Unit for outer dimensions')
)
weight_unit = CSVChoiceField(
choices=WeightUnitChoices,
required=False,
help_text=_('Unit for rack weights')
)

class Meta:
model = Rack
fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
'description', 'comments', 'tags',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
'max_weight', 'weight_unit', 'description', 'comments', 'tags',
)

def __init__(self, data=None, *args, **kwargs):
Expand Down
9 changes: 7 additions & 2 deletions netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'weight_unit')),
('Weight', ('weight', 'max_weight', 'weight_unit')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
Expand Down Expand Up @@ -284,7 +284,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
)
tag = TagFilterField(model)
weight = forms.DecimalField(
required=False
required=False,
min_value=1
)
max_weight = forms.IntegerField(
required=False,
min_value=1
)
weight_unit = forms.ChoiceField(
choices=add_blank_choice(WeightUnitChoices),
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class Meta:
fields = [
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'description', 'comments', 'tags',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
]
help_texts = {
'site': _("The site at which the rack exists"),
Expand Down
32 changes: 23 additions & 9 deletions netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Generated by Django 4.0.7 on 2022-09-23 01:01

from django.db import migrations, models


Expand All @@ -10,11 +8,8 @@ class Migration(migrations.Migration):
]

operations = [
migrations.AddField(
model_name='devicetype',
name='_abs_weight',
field=models.PositiveBigIntegerField(blank=True, null=True),
),

# Device types
migrations.AddField(
model_name='devicetype',
name='weight',
Expand All @@ -26,10 +21,12 @@ class Migration(migrations.Migration):
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='moduletype',
model_name='devicetype',
name='_abs_weight',
field=models.PositiveBigIntegerField(blank=True, null=True),
),

# Module types
migrations.AddField(
model_name='moduletype',
name='weight',
Expand All @@ -41,18 +38,35 @@ class Migration(migrations.Migration):
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='rack',
model_name='moduletype',
name='_abs_weight',
field=models.PositiveBigIntegerField(blank=True, null=True),
),

# Racks
migrations.AddField(
model_name='rack',
name='weight',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
),
migrations.AddField(
model_name='rack',
name='max_weight',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rack',
name='weight_unit',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='rack',
name='_abs_weight',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rack',
name='_abs_max_weight',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
]
4 changes: 1 addition & 3 deletions netbox/dcim/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,5 @@ def clean(self):
super().clean()

# Validate weight and weight_unit
if self.weight is not None and not self.weight_unit:
if self.weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a weight")
elif self.weight is None:
self.weight_unit = ''
28 changes: 26 additions & 2 deletions netbox/dcim/models/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.utils import array_to_string, drange
from utilities.utils import array_to_string, drange, to_grams
from .device_components import PowerPort
from .devices import Device, Module
from .mixins import WeightMixin
Expand Down Expand Up @@ -149,6 +149,16 @@ class Rack(PrimaryModel, WeightMixin):
choices=RackDimensionUnitChoices,
blank=True,
)
max_weight = models.PositiveIntegerField(
blank=True,
null=True,
help_text=_('Maximum load capacity for the rack')
)
# Stores the normalized max weight (in grams) for database ordering
_abs_max_weight = models.PositiveBigIntegerField(
blank=True,
null=True
)
mounting_depth = models.PositiveSmallIntegerField(
blank=True,
null=True,
Expand All @@ -174,7 +184,7 @@ class Rack(PrimaryModel, WeightMixin):

clone_fields = (
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
)
prerequisite_models = (
'dcim.Site',
Expand Down Expand Up @@ -215,6 +225,10 @@ def clean(self):
elif self.outer_width is None and self.outer_depth is None:
self.outer_unit = ''

# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a maximum weight")

if self.pk:
# Validate that Rack is tall enough to house the installed Devices
top_device = Device.objects.filter(
Expand All @@ -237,6 +251,16 @@ def clean(self):
'location': f"Location must be from the same site, {self.site}."
})

def save(self, *args, **kwargs):

# Store the given max weight (if any) in grams for use in database ordering
if self.max_weight and self.weight_unit:
self._abs_max_weight = to_grams(self.max_weight, self.weight_unit)
else:
self._abs_max_weight = None

super().save(*args, **kwargs)

@property
def units(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/tables/devicetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dcim import models
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, WEIGHT

__all__ = (
'ConsolePortTemplateTable',
Expand Down Expand Up @@ -84,7 +84,7 @@ class DeviceTypeTable(NetBoxTable):
template_code='{{ value|floatformat }}'
)
weight = columns.TemplateColumn(
template_code=DEVICE_WEIGHT,
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)

Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/tables/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from dcim.models import Module, ModuleType
from netbox.tables import NetBoxTable, columns
from .template_code import DEVICE_WEIGHT
from .template_code import WEIGHT

__all__ = (
'ModuleTable',
Expand All @@ -28,7 +28,7 @@ class ModuleTypeTable(NetBoxTable):
url_name='dcim:moduletype_list'
)
weight = columns.TemplateColumn(
template_code=DEVICE_WEIGHT,
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)

Expand Down
12 changes: 8 additions & 4 deletions netbox/dcim/tables/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dcim.models import Rack, RackReservation, RackRole
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .template_code import DEVICE_WEIGHT
from .template_code import WEIGHT

__all__ = (
'RackTable',
Expand Down Expand Up @@ -81,17 +81,21 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
verbose_name='Outer Depth'
)
weight = columns.TemplateColumn(
template_code=DEVICE_WEIGHT,
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
max_weight = columns.TemplateColumn(
template_code=WEIGHT,
order_by=('_abs_max_weight', 'weight_unit')
)

class Meta(NetBoxTable.Meta):
model = Rack
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
'asset_tag', 'type', 'u_height', 'width', 'outer_width', 'outer_depth', 'mounting_depth', 'weight',
'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags',
'created', 'last_updated',
'max_weight', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'description',
'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/tables/template_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
"""

DEVICE_WEIGHT = """
WEIGHT = """
{% load helpers %}
{% if record.weight %}{{ record.weight|simplify_decimal }} {{ record.weight_unit }}{% endif %}
{% if value %}{{ value|simplify_decimal }} {{ record.weight_unit }}{% endif %}
"""

DEVICE_LINK = """
Expand Down
10 changes: 7 additions & 3 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,9 @@ def setUpTestData(cls):
Tenant.objects.bulk_create(tenants)

racks = (
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, max_weight=1000, weight_unit=WeightUnitChoices.UNIT_POUND),
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, max_weight=2000, weight_unit=WeightUnitChoices.UNIT_POUND),
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, max_weight=3000, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
)
Rack.objects.bulk_create(racks)

Expand Down Expand Up @@ -521,6 +521,10 @@ def test_weight(self):
params = {'weight': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_max_weight(self):
params = {'max_weight': [1000, 2000]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_weight_unit(self):
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
Expand Down
Loading

0 comments on commit 0b100b8

Please sign in to comment.