diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 80d7558c9b..dad3c3a0de 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -16,6 +16,8 @@ RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30 RACK_ELEVATION_DEFAULT_MARGIN_WIDTH = 15 +RACK_STARTING_UNIT_DEFAULT = 1 + # # RearPorts diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 56542d70cb..b578d74ebd 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -221,8 +221,8 @@ class Meta: model = Rack fields = [ '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', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', + 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', + 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags', ] diff --git a/netbox/dcim/migrations/0174_rack_starting_unit.py b/netbox/dcim/migrations/0174_rack_starting_unit.py new file mode 100644 index 0000000000..e32738660a --- /dev/null +++ b/netbox/dcim/migrations/0174_rack_starting_unit.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.9 on 2023-05-31 15:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0174_device_latitude_device_longitude'), + ] + + operations = [ + migrations.AddField( + model_name='rack', + name='starting_unit', + field=models.PositiveSmallIntegerField(default=1), + ), + ] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 54de5c434d..8e86a17024 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -129,6 +129,11 @@ class Rack(PrimaryModel, WeightMixin): validators=[MinValueValidator(1), MaxValueValidator(100)], help_text=_('Height in rack units') ) + starting_unit = models.PositiveSmallIntegerField( + default=RACK_STARTING_UNIT_DEFAULT, + verbose_name='Starting unit', + help_text=_('Starting unit for rack') + ) desc_units = models.BooleanField( default=False, verbose_name='Descending units', @@ -228,20 +233,24 @@ def clean(self): 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( - rack=self - ).exclude( - position__isnull=True - ).order_by('-position').first() - if top_device: - min_height = top_device.position + top_device.device_type.u_height - 1 + mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position') + + # Validate that Rack is tall enough to house the highest mounted Device + if top_device := mounted_devices.last(): + min_height = top_device.position + top_device.device_type.u_height - self.starting_unit if self.u_height < min_height: raise ValidationError({ - 'u_height': "Rack must be at least {}U tall to house currently installed devices.".format( - min_height - ) + 'u_height': f"Rack must be at least {min_height}U tall to house currently installed devices." }) + + # Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device + if last_device := mounted_devices.first(): + if self.starting_unit > last_device.position: + raise ValidationError({ + 'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house " + f"currently installed devices." + }) + # Validate that Rack was assigned a Location of its same site, if applicable if self.location: if self.location.site != self.site: @@ -269,8 +278,8 @@ def units(self): Return a list of unit numbers, top to bottom. """ if self.desc_units: - return drange(decimal.Decimal(1.0), self.u_height + 1, 0.5) - return drange(self.u_height + decimal.Decimal(0.5), 0.5, -0.5) + return drange(decimal.Decimal(self.starting_unit), self.u_height + self.starting_unit, 0.5) + return drange(self.u_height + decimal.Decimal(0.5) + self.starting_unit - 1, 0.5 + self.starting_unit - 1, -0.5) def get_status_color(self): return RackStatusChoices.colors.get(self.status) diff --git a/netbox/dcim/svg/racks.py b/netbox/dcim/svg/racks.py index 9c317ea160..6333abcf18 100644 --- a/netbox/dcim/svg/racks.py +++ b/netbox/dcim/svg/racks.py @@ -150,9 +150,9 @@ def _get_device_coords(self, position, height): x = self.legend_width + RACK_ELEVATION_BORDER_WIDTH y = RACK_ELEVATION_BORDER_WIDTH if self.rack.desc_units: - y += int((position - 1) * self.unit_height) + y += int((position - self.rack.starting_unit) * self.unit_height) else: - y += int((self.rack.u_height - position + 1) * self.unit_height) - int(height * self.unit_height) + y += int((self.rack.u_height - position + self.rack.starting_unit) * self.unit_height) - int(height * self.unit_height) return x, y @@ -237,6 +237,7 @@ def draw_legend(self): start_y = ru * self.unit_height + RACK_ELEVATION_BORDER_WIDTH position_coordinates = (self.legend_width / 2, start_y + self.unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH) unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru + unit = unit + self.rack.starting_unit - 1 self.drawing.add( Text(str(unit), position_coordinates, class_='unit') ) @@ -278,6 +279,7 @@ def draw_background(self, face): for ru in range(0, self.rack.u_height): unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru + unit = unit + self.rack.starting_unit - 1 y_offset = RACK_ELEVATION_BORDER_WIDTH + ru * self.unit_height text_coords = ( x_offset + self.unit_width / 2, diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a327d6400f..3908975466 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -389,6 +389,7 @@ def setUpTestData(cls): 'outer_width': 500, 'outer_depth': 500, 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER, + 'starting_unit': 1, 'weight': 100, 'max_weight': 2000, 'weight_unit': WeightUnitChoices.UNIT_POUND, diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 52b5d4bfec..01aeacff17 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -101,6 +101,12 @@