ソースを参照

move cooling attributes to rack type

Arthur 1 週間 前
コミット
a69c4e9e74

+ 3 - 3
docs/models/dcim/rack.md

@@ -53,15 +53,15 @@ A unique, locally-administered label used to identify hardware resources.
 
 ### Cooling Capability
 
-The rack's coolant capability: air-only, liquid-capable, or liquid-required.
+The rack's coolant capability: air-only, liquid-capable, or liquid-required. When the rack is assigned a [rack type](./racktype.md), this value is inherited from the rack type.
 
 ### Has RDHx
 
-Indicates whether the rack is equipped with a rear-door heat exchanger (RDHx).
+Indicates whether the rack is equipped with a rear-door heat exchanger (RDHx). This is a per-rack attribute and is not derived from the rack type.
 
 ### Cooling Capacity
 
-The rack's cooling capacity, expressed in kilowatts (kW).
+The rack's cooling capacity, expressed in kilowatts (kW). When the rack is assigned a [rack type](./racktype.md), this value is inherited from the rack type.
 
 !!! note
     Some additional fields pertaining to physical attributes such as height and weight can also be defined on each rack, but should generally be defined instead on the [rack type](./racktype.md).

+ 8 - 0
docs/models/dcim/racktype.md

@@ -54,6 +54,14 @@ The numeric weight of the rack, including a unit designation (e.g. 10 kilograms
 
 The maximum total weight capacity for all installed devices, inclusive of the rack itself.
 
+### Cooling Capability
+
+The rack design's coolant capability: air-only, liquid-capable, or liquid-required. Racks of this type inherit this value.
+
+### Cooling Capacity
+
+The rack design's cooling capacity, expressed in kilowatts (kW). Racks of this type inherit this value.
+
 ### Descending Units
 
 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.)

+ 12 - 4
netbox/dcim/api/serializers_/racks.py

@@ -77,6 +77,12 @@ class RackBaseSerializer(PrimaryModelSerializer):
         required=False,
         allow_null=True
     )
+    cooling_capability = ChoiceField(
+        choices=RackCoolingCapabilityChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
 
 
 class RackTypeSerializer(RackBaseSerializer):
@@ -88,8 +94,9 @@ class RackTypeSerializer(RackBaseSerializer):
         fields = [
             'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
             'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
-            'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'owner', 'comments',
-            'tags', 'custom_fields', 'created', 'last_updated', 'rack_count',
+            'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'cooling_capability',
+            'cooling_capacity', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'rack_count',
         ]
         brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'rack_count')
 
@@ -152,8 +159,9 @@ class RackSerializer(RackBaseSerializer):
             'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'group', 'tenant',
             'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit',
             'weight', 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
-            'outer_unit', 'mounting_depth', 'airflow', 'description', 'owner', 'comments', 'tags', 'custom_fields',
-            'created', 'last_updated', 'device_count', 'powerfeed_count',
+            'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability', 'has_rdhx', 'cooling_capacity',
+            'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
+            'powerfeed_count',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
 

+ 6 - 1
netbox/dcim/filtersets.py

@@ -360,12 +360,17 @@ class RackTypeFilterSet(PrimaryModelFilterSet):
         choices=RackWidthChoices,
         distinct=False,
     )
+    cooling_capability = django_filters.MultipleChoiceFilter(
+        choices=RackCoolingCapabilityChoices,
+        distinct=False,
+    )
 
     class Meta:
         model = RackType
         fields = (
             'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
-            'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
+            'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'cooling_capacity',
+            'description',
 
             # Counters
             'rack_count',

+ 12 - 1
netbox/dcim/forms/bulk_edit.py

@@ -306,6 +306,16 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
         required=False,
         initial=''
     )
+    cooling_capability = forms.ChoiceField(
+        label=_('Cooling capability'),
+        choices=add_blank_choice(RackCoolingCapabilityChoices),
+        required=False
+    )
+    cooling_capacity = forms.DecimalField(
+        label=_('Cooling capacity'),
+        min_value=0,
+        required=False
+    )
 
     model = RackType
     fieldsets = (
@@ -317,10 +327,11 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
             name=_('Dimensions')
         ),
         FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
+        FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
     )
     nullable_fields = (
         'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'weight',
-        'max_weight', 'weight_unit', 'description', 'comments',
+        'max_weight', 'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'comments',
     )
 
 

+ 7 - 1
netbox/dcim/forms/bulk_import.py

@@ -242,13 +242,19 @@ class RackTypeImportForm(PrimaryModelImportForm):
         required=False,
         help_text=_('Unit for rack weights')
     )
+    cooling_capability = CSVChoiceField(
+        label=_('Cooling capability'),
+        choices=RackCoolingCapabilityChoices,
+        required=False,
+        help_text=_('Cooling capability')
+    )
 
     class Meta:
         model = RackType
         fields = (
             'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
             'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
-            'weight_unit', 'description', 'owner', 'comments', 'tags',
+            'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags',
         )
 
     def __init__(self, data=None, *args, **kwargs):

+ 6 - 5
netbox/dcim/forms/filtersets.py

@@ -343,6 +343,11 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
         choices=add_blank_choice(WeightUnitChoices),
         required=False
     )
+    cooling_capability = forms.MultipleChoiceField(
+        label=_('Cooling capability'),
+        choices=RackCoolingCapabilityChoices,
+        required=False
+    )
 
 
 class RackTypeFilterForm(RackBaseFilterForm):
@@ -352,6 +357,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
         FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
         FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
         FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
+        FieldSet('cooling_capability', name=_('Cooling')),
         FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
     )
     selector_fields = ('filter_id', 'q', 'manufacturer_id')
@@ -445,11 +451,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
         choices=add_blank_choice(RackAirflowChoices),
         required=False
     )
-    cooling_capability = forms.MultipleChoiceField(
-        label=_('Cooling capability'),
-        choices=RackCoolingCapabilityChoices,
-        required=False
-    )
     has_rdhx = forms.NullBooleanField(
         required=False,
         label=_('Has RDHx'),

+ 2 - 1
netbox/dcim/forms/model_forms.py

@@ -284,6 +284,7 @@ class RackTypeForm(PrimaryModelForm):
             'mounting_depth', name=_('Dimensions')
         ),
         FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
+        FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
     )
 
     class Meta:
@@ -291,7 +292,7 @@ class RackTypeForm(PrimaryModelForm):
         fields = [
             'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
             'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
-            'weight_unit', 'description', 'owner', 'comments', 'tags',
+            'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags',
         ]
 
 

+ 13 - 1
netbox/dcim/migrations/0241_device_cooling_method_device_cooling_outlet_count_and_more.py

@@ -1,4 +1,4 @@
-# Generated by Django 6.0.6 on 2026-06-23 17:04
+# Generated by Django 6.0.6 on 2026-06-23 18:45
 
 import django.contrib.postgres.fields
 import django.core.validators
@@ -109,6 +109,18 @@ class Migration(migrations.Migration):
             name="has_rdhx",
             field=models.BooleanField(default=False),
         ),
+        migrations.AddField(
+            model_name="racktype",
+            name="cooling_capability",
+            field=models.CharField(blank=True, max_length=50, null=True),
+        ),
+        migrations.AddField(
+            model_name="racktype",
+            name="cooling_capacity",
+            field=models.DecimalField(
+                blank=True, decimal_places=2, max_digits=8, null=True
+            ),
+        ),
         migrations.CreateModel(
             name="CoolingPort",
             fields=[

+ 21 - 17
netbox/dcim/models/racks.py

@@ -135,6 +135,23 @@ class RackBase(WeightMixin, PrimaryModel):
         null=True
     )
 
+    # Cooling
+    cooling_capability = models.CharField(
+        verbose_name=_('cooling capability'),
+        max_length=50,
+        choices=RackCoolingCapabilityChoices,
+        blank=True,
+        null=True
+    )
+    cooling_capacity = models.DecimalField(
+        verbose_name=_('cooling capacity'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+        help_text=_('Cooling capacity (kW)')
+    )
+
     class Meta:
         abstract = True
 
@@ -174,7 +191,8 @@ class RackType(ImageAttachmentsMixin, RackBase):
 
     clone_fields = (
         'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
-        'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
+        'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'cooling_capability',
+        'cooling_capacity',
     )
     prerequisite_models = (
         'dcim.Manufacturer',
@@ -269,7 +287,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
     # Fields which cannot be set locally if a RackType is assigned
     RACKTYPE_FIELDS = (
         'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
-        'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
+        'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight', 'cooling_capability',
+        'cooling_capacity',
     )
 
     form_factor = models.CharField(
@@ -359,26 +378,11 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
         blank=True,
         null=True
     )
-    cooling_capability = models.CharField(
-        verbose_name=_('cooling capability'),
-        max_length=50,
-        choices=RackCoolingCapabilityChoices,
-        blank=True,
-        null=True
-    )
     has_rdhx = models.BooleanField(
         verbose_name=_('has RDHx'),
         default=False,
         help_text=_('Rack is equipped with a rear-door heat exchanger')
     )
-    cooling_capacity = models.DecimalField(
-        verbose_name=_('cooling capacity'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True,
-        help_text=_('Cooling capacity (kW)')
-    )
 
     # Generic relations
     vlan_groups = GenericRelation(

+ 8 - 2
netbox/dcim/tables/racks.py

@@ -106,6 +106,12 @@ class RackTypeTable(PrimaryModelTable):
         url_params={'rack_type_id': 'pk'},
         verbose_name=_('Rack Count'),
     )
+    cooling_capability = columns.ChoiceFieldColumn(
+        verbose_name=_('Cooling Capability'),
+    )
+    cooling_capacity = tables.Column(
+        verbose_name=_('Cooling Capacity (kW)')
+    )
     tags = columns.TagColumn(
         url_name='dcim:rack_list'
     )
@@ -114,8 +120,8 @@ class RackTypeTable(PrimaryModelTable):
         model = RackType
         fields = (
             'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
-            'outer_height', 'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'description', 'comments',
-            'rack_count', 'tags', 'created', 'last_updated',
+            'outer_height', 'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'cooling_capability',
+            'cooling_capacity', 'description', 'comments', 'rack_count', 'tags', 'created', 'last_updated',
         )
         default_columns = (
             'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'rack_count',

+ 5 - 0
netbox/dcim/tests/test_api.py

@@ -648,6 +648,8 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase):
     brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'rack_count', 'slug', 'url']
     bulk_update_data = {
         'description': 'new description',
+        'cooling_capability': RackCoolingCapabilityChoices.LIQUID_CAPABLE,
+        'cooling_capacity': 50,
     }
     user_permissions = ('dcim.view_manufacturer',)
 
@@ -687,12 +689,15 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase):
                 'model': 'Rack Type 4',
                 'slug': 'rack-type-4',
                 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+                'cooling_capability': RackCoolingCapabilityChoices.LIQUID_REQUIRED,
+                'cooling_capacity': 80,
             },
             {
                 'manufacturer': manufacturers[1].pk,
                 'model': 'Rack Type 5',
                 'slug': 'rack-type-5',
                 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+                'cooling_capability': RackCoolingCapabilityChoices.LIQUID_CAPABLE,
             },
             {
                 'manufacturer': manufacturers[1].pk,

+ 8 - 1
netbox/dcim/tests/test_models.py

@@ -201,6 +201,8 @@ class RackTypeTestCase(TestCase):
             weight_unit=WeightUnitChoices.UNIT_POUND,
             max_weight=7777,
             mounting_depth=8,
+            cooling_capability=RackCoolingCapabilityChoices.LIQUID_REQUIRED,
+            cooling_capacity=80,
         )
 
     def test_rack_creation(self):
@@ -220,7 +222,8 @@ class RackTypeTestCase(TestCase):
             facility_id='A101',
             site=sites[0],
             location=locations[0],
-            rack_type=rack_type
+            rack_type=rack_type,
+            has_rdhx=True,
         )
         self.assertEqual(rack.width, rack_type.width)
         self.assertEqual(rack.u_height, rack_type.u_height)
@@ -233,6 +236,10 @@ class RackTypeTestCase(TestCase):
         self.assertEqual(rack.weight_unit, rack_type.weight_unit)
         self.assertEqual(rack.max_weight, rack_type.max_weight)
         self.assertEqual(rack.mounting_depth, rack_type.mounting_depth)
+        # Cooling capability/capacity are inherited from the rack type; has_rdhx is rack-specific
+        self.assertEqual(rack.cooling_capability, rack_type.cooling_capability)
+        self.assertEqual(rack.cooling_capacity, rack_type.cooling_capacity)
+        self.assertTrue(rack.has_rdhx)
 
 
 class RackTestCase(TestCase):

+ 2 - 0
netbox/dcim/ui/panels.py

@@ -85,6 +85,8 @@ class RackTypePanel(panels.ObjectAttributesPanel):
     manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
     model = attrs.TextAttr('model')
     description = attrs.TextAttr('description')
+    cooling_capability = attrs.ChoiceAttr('cooling_capability')
+    cooling_capacity = attrs.TextAttr('cooling_capacity', format_string=_('{} kW'))
 
 
 class DevicePanel(panels.ObjectAttributesPanel):