diff --git a/netbox/dcim/migrations/0188_racktype.py b/netbox/dcim/migrations/0188_racktype.py new file mode 100644 index 000000000..4f6e1ec49 --- /dev/null +++ b/netbox/dcim/migrations/0188_racktype.py @@ -0,0 +1,85 @@ +# Generated by Django 4.2.11 on 2024-06-25 16:43 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers +import utilities.fields +import utilities.json +import utilities.ordering + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0117_customfield_uniqueness'), + ('dcim', '0187_alter_device_vc_position'), + ] + + operations = [ + migrations.CreateModel( + name='RackType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ( + 'custom_field_data', + models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), + ), + ('description', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('weight', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)), + ('weight_unit', models.CharField(blank=True, max_length=50)), + ('_abs_weight', models.PositiveBigIntegerField(blank=True, null=True)), + ('name', models.CharField(max_length=100)), + ( + '_name', + utilities.fields.NaturalOrderingField( + 'name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize + ), + ), + ('type', models.CharField(blank=True, max_length=50)), + ('width', models.PositiveSmallIntegerField(default=19)), + ( + 'u_height', + models.PositiveSmallIntegerField( + default=42, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(100), + ], + ), + ), + ( + 'starting_unit', + models.PositiveSmallIntegerField( + default=1, validators=[django.core.validators.MinValueValidator(1)] + ), + ), + ('desc_units', models.BooleanField(default=False)), + ('outer_width', models.PositiveSmallIntegerField(blank=True, null=True)), + ('outer_depth', models.PositiveSmallIntegerField(blank=True, null=True)), + ('outer_unit', models.CharField(blank=True, max_length=50)), + ('max_weight', models.PositiveIntegerField(blank=True, null=True)), + ('_abs_max_weight', models.PositiveBigIntegerField(blank=True, null=True)), + ('mounting_depth', models.PositiveSmallIntegerField(blank=True, null=True)), + ( + 'role', + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='racktypes', + to='dcim.rackrole', + ), + ), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'racktype', + 'verbose_name_plural': 'racktypes', + 'ordering': ('_name', 'pk'), + }, + ), + ] diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 289c38133..d68fdadb0 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -29,13 +29,178 @@ __all__ = ( 'Rack', 'RackReservation', 'RackRole', + 'RackType', ) +# +# Rack Types +# + +class RackType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): + """ + 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. + """ + name = models.CharField( + verbose_name=_('name'), + max_length=100 + ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) + role = models.ForeignKey( + to='dcim.RackRole', + on_delete=models.PROTECT, + related_name='racktypes', + blank=True, + null=True, + help_text=_('Functional role') + ) + type = models.CharField( + choices=RackTypeChoices, + max_length=50, + blank=True, + verbose_name=_('type') + ) + width = models.PositiveSmallIntegerField( + choices=RackWidthChoices, + default=RackWidthChoices.WIDTH_19IN, + verbose_name=_('width'), + help_text=_('Rail-to-rail width') + ) + u_height = models.PositiveSmallIntegerField( + default=RACK_U_HEIGHT_DEFAULT, + verbose_name=_('height (U)'), + validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)], + help_text=_('Height in rack units') + ) + starting_unit = models.PositiveSmallIntegerField( + default=RACK_STARTING_UNIT_DEFAULT, + verbose_name=_('starting unit'), + validators=[MinValueValidator(1),], + help_text=_('Starting unit for rack') + ) + desc_units = models.BooleanField( + default=False, + verbose_name=_('descending units'), + help_text=_('Units are numbered top-to-bottom') + ) + outer_width = models.PositiveSmallIntegerField( + verbose_name=_('outer width'), + blank=True, + null=True, + help_text=_('Outer dimension of rack (width)') + ) + outer_depth = models.PositiveSmallIntegerField( + verbose_name=_('outer depth'), + blank=True, + null=True, + help_text=_('Outer dimension of rack (depth)') + ) + outer_unit = models.CharField( + verbose_name=_('outer unit'), + max_length=50, + choices=RackDimensionUnitChoices, + blank=True, + ) + max_weight = models.PositiveIntegerField( + verbose_name=_('max weight'), + 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( + verbose_name=_('mounting depth'), + blank=True, + null=True, + help_text=( + _('Maximum depth of a mounted device, in millimeters. For four-post racks, this is the ' + 'distance between the front and rear rails.') + ) + ) + + clone_fields = ( + 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', + ) + prerequisite_models = ( + 'dcim.Site', + ) + + class Meta: + ordering = ('_name', 'pk') # (site, location, name) may be non-unique + verbose_name = _('racktype') + verbose_name_plural = _('racktypes') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:racktype', args=[self.pk]) + + def clean(self): + super().clean() + + # Validate outer dimensions and unit + if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: + raise ValidationError(_("Must specify a unit when setting an outer width/depth")) + + # 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")) + + 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 + + # Clear unit if outer width & depth are not set + if self.outer_width is None and self.outer_depth is None: + self.outer_unit = '' + + super().save(*args, **kwargs) + + @property + def units(self): + """ + Return a list of unit numbers, top to bottom. + """ + if self.desc_units: + 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) + + @cached_property + def total_weight(self): + total_weight = sum( + device.device_type._abs_weight + for device in self.devices.exclude(device_type___abs_weight__isnull=True).prefetch_related('device_type') + ) + total_weight += sum( + module.module_type._abs_weight + for module in Module.objects.filter(device__rack=self) + .exclude(module_type___abs_weight__isnull=True) + .prefetch_related('module_type') + ) + if self._abs_weight: + total_weight += self._abs_weight + return round(total_weight / 1000, 2) + # # Racks # + class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices.