diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md index 505160d3e12..b603e7a0b16 100644 --- a/docs/models/dcim/rack.md +++ b/docs/models/dcim/rack.md @@ -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.) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 6a52d48ae77..9c172da7f55 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -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 @@ -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 diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 020697b2527..5ca57480b34 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -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', ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 1412cf571d3..1ebcb2e34df 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -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): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 2987beebc98..38fa55738f0 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -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, @@ -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', ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index f4f95ff39f4..f2ad17117c9 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -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): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 68c7cd07b43..815f0586ae8 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -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(), @@ -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), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 867d362a388..e13300a679e 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -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"), diff --git a/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py b/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py index 09bef573603..ddcc0116414 100644 --- a/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py +++ b/netbox/dcim/migrations/0163_rack_devicetype_moduletype_weights.py @@ -1,5 +1,3 @@ -# Generated by Django 4.0.7 on 2022-09-23 01:01 - from django.db import migrations, models @@ -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', @@ -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', @@ -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), + ), ] diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index b5449332b5d..486945b0f95 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -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 = '' diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index ab56ddd09bb..03be2fdb3e6 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -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 @@ -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, @@ -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', @@ -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( @@ -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): """ diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index a52d41b7061..3f3d28d4937 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -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', @@ -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') ) diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index a66fa4aa67b..8e4ca309e87 100644 --- a/netbox/dcim/tables/modules.py +++ b/netbox/dcim/tables/modules.py @@ -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', @@ -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') ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index b360002d260..cb9aae6fd85 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -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', @@ -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', diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 4a44e33ba5b..dd0581ddc7a 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -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 = """ diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 3e4ab55fcbc..874e3474c69 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -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) @@ -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) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 3bb5b890acb..b8050f244b0 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -388,15 +388,18 @@ def setUpTestData(cls): 'outer_width': 500, 'outer_depth': 500, 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER, + 'weight': 100, + 'max_weight': 2000, + 'weight_unit': WeightUnitChoices.UNIT_POUND, 'comments': 'Some comments', 'tags': [t.pk for t in tags], } cls.csv_data = ( - "site,location,name,status,width,u_height", - "Site 1,,Rack 4,active,19,42", - "Site 1,Location 1,Rack 5,active,19,42", - "Site 2,Location 2,Rack 6,active,19,42", + "site,location,name,status,width,u_height,weight,max_weight,weight_unit", + "Site 1,,Rack 4,active,19,42,100,2000,kg", + "Site 1,Location 1,Rack 5,active,19,42,100,2000,kg", + "Site 2,Location 2,Rack 6,active,19,42,100,2000,kg", ) cls.csv_update_data = ( @@ -420,6 +423,9 @@ def setUpTestData(cls): 'outer_width': 30, 'outer_depth': 30, 'outer_unit': RackDimensionUnitChoices.UNIT_INCH, + 'weight': 200, + 'max_weight': 4000, + 'weight_unit': WeightUnitChoices.UNIT_POUND, 'comments': 'New comments', } diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 15fca784691..e2cb1597ed5 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -169,6 +169,16 @@
Dimensions
{% endif %} + + Maximum Weight + + {% if object.max_weight %} + {{ object.max_weight }} {{ object.get_weight_unit_display }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + Total Weight diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 1d8dee3ab98..cd9ed637a30 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -58,10 +58,14 @@
Dimensions
-
+
{{ form.weight }}
Weight
+
+ {{ form.max_weight }} +
Maximum Weight
+
{{ form.weight_unit }}
Unit