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

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 _ 

5 

6from dcim.choices import * 

7from dcim.models.mixins import MaxFlowMixin 

8from netbox.models import PrimaryModel 

9from netbox.models.features import ContactsMixin, ImageAttachmentsMixin 

10 

11__all__ = ( 

12 'CoolingFeed', 

13 'CoolingSource', 

14) 

15 

16 

17# 

18# Cooling 

19# 

20 

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 ) 

67 

68 clone_fields = ( 

69 'site', 'location', 'type', 'status', 'fluid_type', 'cooling_capacity', 

70 ) 

71 prerequisite_models = ( 

72 'dcim.Site', 

73 ) 

74 

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') 

85 

86 def __str__(self): 

87 return self.name 

88 

89 def get_status_color(self): 

90 return CoolingSourceStatusChoices.colors.get(self.status) 

91 

92 def clean(self): 

93 super().clean() 

94 

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 ) 

101 

102 

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. 

108 

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 ) 

152 

153 clone_fields = ( 

154 'cooling_source', 'rack', 'status', 'cooling_capacity', 'max_flow', 'max_flow_unit', 'tenant', 

155 ) 

156 prerequisite_models = ( 

157 'dcim.CoolingSource', 

158 ) 

159 

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') 

170 

171 def __str__(self): 

172 return self.name 

173 

174 def clean(self): 

175 super().clean() 

176 

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 )) 

187 

188 @property 

189 def parent_object(self): 

190 return self.cooling_source 

191 

192 def get_status_color(self): 

193 return CoolingFeedStatusChoices.colors.get(self.status)