diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 7d35a40f9..38720a614 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1314,6 +1314,24 @@ class CableLengthUnitChoices(ChoiceSet): ) +class DeviceWeightUnitChoices(ChoiceSet): + + # Metric + UNIT_KILOGRAM = 'kg' + UNIT_GRAM = 'g' + + # Imperial + UNIT_POUND = 'lb' + UNIT_OUNCE = 'oz' + + CHOICES = ( + (UNIT_KILOGRAM, 'Kilograms'), + (UNIT_GRAM, 'Grams'), + (UNIT_POUND, 'Pounds'), + (UNIT_OUNCE, 'Ounce'), + ) + + # # CableTerminations # diff --git a/netbox/dcim/migrations/0162_devicetype__abs_weight_devicetype_weight_and_more.py b/netbox/dcim/migrations/0162_devicetype__abs_weight_devicetype_weight_and_more.py new file mode 100644 index 000000000..14e2c9475 --- /dev/null +++ b/netbox/dcim/migrations/0162_devicetype__abs_weight_devicetype_weight_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.0.7 on 2022-09-23 01:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0161_cabling_cleanup'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='_abs_weight', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True), + ), + migrations.AddField( + model_name='devicetype', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='devicetype', + name='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='moduletype', + name='_abs_weight', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True), + ), + migrations.AddField( + model_name='moduletype', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='moduletype', + name='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='rack', + name='_abs_weight', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True), + ), + 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='weight_unit', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index ccf4613bf..41da36db6 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -20,6 +20,7 @@ from netbox.models import OrganizationalModel, NetBoxModel from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField from .device_components import * +from .mixins import DeviceWeightMixin __all__ = ( @@ -70,7 +71,7 @@ class Manufacturer(OrganizationalModel): return reverse('dcim:manufacturer', args=[self.pk]) -class DeviceType(NetBoxModel): +class DeviceType(NetBoxModel, DeviceWeightMixin): """ A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as well as high-level functional role(s). @@ -308,7 +309,7 @@ class DeviceType(NetBoxModel): return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD -class ModuleType(NetBoxModel): +class ModuleType(NetBoxModel, DeviceWeightMixin): """ A ModuleType represents a hardware element that can be installed within a device and which houses additional components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py new file mode 100644 index 000000000..99aa7a738 --- /dev/null +++ b/netbox/dcim/models/mixins.py @@ -0,0 +1,37 @@ +from django.db import models +from dcim.choices import * +from utilities.utils import to_kilograms + + +class DeviceWeightMixin(models.Model): + weight = models.DecimalField( + max_digits=8, + decimal_places=2, + blank=True, + null=True + ) + weight_unit = models.CharField( + max_length=50, + choices=DeviceWeightUnitChoices, + blank=True, + ) + # Stores the normalized length (in meters) for database ordering + _abs_weight = models.DecimalField( + max_digits=10, + decimal_places=4, + blank=True, + null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + + # Store the given weight (if any) in meters for use in database ordering + if self.weight and self.weight_unit: + self._abs_weight = to_kilograms(self.length, self.length_unit) + else: + self._abs_weight = None + + super().save(*args, **kwargs) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 20027675a..74f63a730 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -20,6 +20,7 @@ from utilities.fields import ColorField, NaturalOrderingField from utilities.utils import array_to_string, drange from .device_components import PowerOutlet, PowerPort from .devices import Device +from .mixins import DeviceWeightMixin from .power import PowerFeed __all__ = ( @@ -63,7 +64,7 @@ class RackRole(OrganizationalModel): return reverse('dcim:rackrole', args=[self.pk]) -class Rack(NetBoxModel): +class Rack(NetBoxModel, DeviceWeightMixin): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a Location. @@ -449,6 +450,11 @@ class Rack(NetBoxModel): return int(allocated_draw / available_power_total * 100) + def get_total_weight(self): + total_weight = sum(device._abs_weight for device in self.devices.exclude(_abs_weight__isnull=True)) + total_weight += self._abs_weight + return total_weight + class RackReservation(NetBoxModel): """ diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 69ab615fc..d7c3c78d0 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -270,6 +270,38 @@ def to_meters(length, unit): raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.") +def to_kilograms(weight, unit): + """ + Convert the given length to kilograms. + """ + try: + if weight < 0: + raise ValueError("Weight must be a positive number") + except TypeError: + raise TypeError(f"Invalid value '{weight}' for weight (must be a number)") + + valid_units = DeviceWeightUnitChoices.values() + if unit not in valid_units: + raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}") + + UNIT_KILOGRAM = 'kg' + UNIT_GRAM = 'g' + + # Imperial + UNIT_POUND = 'lb' + UNIT_OUNCE = 'oz' + + if unit == DeviceWeightUnitChoices.UNIT_KILOGRAM: + return weight + if unit == DeviceWeightUnitChoices.UNIT_GRAM: + return weight * 1000 + if unit == DeviceWeightUnitChoices.UNIT_POUND: + return weight * Decimal(0.453592) + if unit == DeviceWeightUnitChoices.UNIT_OUNCE: + return weight * Decimal(0.0283495) + raise ValueError(f"Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.") + + def render_jinja2(template_code, context): """ Render a Jinja2 template with the provided context. Return the rendered content.