Coverage for dcim/models/cooling.py: 75%
57 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.core.exceptions import ValidationError
2from django.core.validators import MinValueValidator
3from django.db import models
4from django.utils.translation import gettext_lazy as _
6from dcim.choices import *
7from dcim.models.mixins import MaxFlowMixin
8from netbox.models import PrimaryModel
9from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
11__all__ = (
12 'CoolingFeed',
13 'CoolingSource',
14)
17#
18# Cooling
19#
21class CoolingSource(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
22 """
23 A facility-level source of cooling; e.g. a chiller, cooling tower, or dry cooler.
24 """
25 site = models.ForeignKey(
26 to='Site',
27 on_delete=models.PROTECT
28 )
29 location = models.ForeignKey(
30 to='dcim.Location',
31 on_delete=models.PROTECT,
32 blank=True,
33 null=True
34 )
35 name = models.CharField(
36 verbose_name=_('name'),
37 max_length=100,
38 db_collation="natural_sort"
39 )
40 type = models.CharField(
41 verbose_name=_('type'),
42 max_length=50,
43 choices=CoolingSourceTypeChoices
44 )
45 status = models.CharField(
46 verbose_name=_('status'),
47 max_length=50,
48 choices=CoolingSourceStatusChoices,
49 default=CoolingSourceStatusChoices.STATUS_ACTIVE
50 )
51 fluid_type = models.CharField(
52 verbose_name=_('fluid type'),
53 max_length=50,
54 choices=FluidTypeChoices,
55 blank=True,
56 null=True
57 )
58 cooling_capacity = models.DecimalField(
59 verbose_name=_('cooling capacity'),
60 max_digits=10,
61 decimal_places=2,
62 blank=True,
63 null=True,
64 validators=[MinValueValidator(0)],
65 help_text=_('Total rated cooling capacity (kW)')
66 )
68 clone_fields = (
69 'site', 'location', 'type', 'status', 'fluid_type', 'cooling_capacity',
70 )
71 prerequisite_models = (
72 'dcim.Site',
73 )
75 class Meta:
76 ordering = ['site', 'name']
77 constraints = (
78 models.UniqueConstraint(
79 fields=('site', 'name'),
80 name='%(app_label)s_%(class)s_unique_site_name'
81 ),
82 )
83 verbose_name = _('cooling source')
84 verbose_name_plural = _('cooling sources')
86 def __str__(self):
87 return self.name
89 def get_status_color(self):
90 return CoolingSourceStatusChoices.colors.get(self.status)
92 def clean(self):
93 super().clean()
95 # Location must belong to assigned Site
96 if self.location and self.location.site != self.site:
97 raise ValidationError(
98 _("Location {location} ({location_site}) is in a different site than {site}").format(
99 location=self.location, location_site=self.location.site, site=self.site)
100 )
103class CoolingFeed(MaxFlowMixin, PrimaryModel):
104 """
105 A coolant loop delivered from a CoolingSource to a rack or CDU. A single feed represents the entire
106 loop (both the supply and return paths). A CoolingFeed serves a rack; the cooling intakes it supplies
107 are derived from the devices installed in that rack rather than referenced explicitly.
109 Maximum flow is a rated specification (the flow the loop supports), not live telemetry; runtime
110 readings belong in an external monitoring system.
111 """
112 cooling_source = models.ForeignKey(
113 to='CoolingSource',
114 on_delete=models.PROTECT,
115 related_name='cooling_feeds'
116 )
117 rack = models.ForeignKey(
118 to='Rack',
119 on_delete=models.PROTECT,
120 related_name='cooling_feeds',
121 blank=True,
122 null=True
123 )
124 name = models.CharField(
125 verbose_name=_('name'),
126 max_length=100,
127 db_collation="natural_sort"
128 )
129 status = models.CharField(
130 verbose_name=_('status'),
131 max_length=50,
132 choices=CoolingFeedStatusChoices,
133 default=CoolingFeedStatusChoices.STATUS_ACTIVE
134 )
135 cooling_capacity = models.DecimalField(
136 verbose_name=_('cooling capacity'),
137 max_digits=10,
138 decimal_places=2,
139 blank=True,
140 null=True,
141 validators=[MinValueValidator(0)],
142 help_text=_('Rated cooling capacity (kW)')
143 )
144 # max_flow, max_flow_unit, _abs_max_flow provided by MaxFlowMixin
145 tenant = models.ForeignKey(
146 to='tenancy.Tenant',
147 on_delete=models.PROTECT,
148 related_name='cooling_feeds',
149 blank=True,
150 null=True
151 )
153 clone_fields = (
154 'cooling_source', 'rack', 'status', 'cooling_capacity', 'max_flow', 'max_flow_unit', 'tenant',
155 )
156 prerequisite_models = (
157 'dcim.CoolingSource',
158 )
160 class Meta:
161 ordering = ['cooling_source', 'name']
162 constraints = (
163 models.UniqueConstraint(
164 fields=('cooling_source', 'name'),
165 name='%(app_label)s_%(class)s_unique_cooling_source_name'
166 ),
167 )
168 verbose_name = _('cooling feed')
169 verbose_name_plural = _('cooling feeds')
171 def __str__(self):
172 return self.name
174 def clean(self):
175 super().clean()
177 # Rack must belong to same Site as CoolingSource
178 if self.rack and self.rack.site != self.cooling_source.site:
179 raise ValidationError(_(
180 "Rack {rack} ({rack_site}) and cooling source {source} ({source_site}) are in different sites."
181 ).format(
182 rack=self.rack,
183 rack_site=self.rack.site,
184 source=self.cooling_source,
185 source_site=self.cooling_source.site
186 ))
188 @property
189 def parent_object(self):
190 return self.cooling_source
192 def get_status_color(self):
193 return CoolingFeedStatusChoices.colors.get(self.status)