power.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from django.core.exceptions import ValidationError
  2. from django.core.validators import MaxValueValidator, MinValueValidator
  3. from django.db import models
  4. from django.urls import reverse
  5. from taggit.managers import TaggableManager
  6. from dcim.choices import *
  7. from dcim.constants import *
  8. from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
  9. from extras.utils import extras_features
  10. from utilities.querysets import RestrictedQuerySet
  11. from utilities.validators import ExclusionValidator
  12. from .device_components import CableTermination
  13. __all__ = (
  14. 'PowerFeed',
  15. 'PowerPanel',
  16. )
  17. #
  18. # Power
  19. #
  20. @extras_features('custom_links', 'export_templates', 'webhooks')
  21. class PowerPanel(ChangeLoggedModel):
  22. """
  23. A distribution point for electrical power; e.g. a data center RPP.
  24. """
  25. site = models.ForeignKey(
  26. to='Site',
  27. on_delete=models.PROTECT
  28. )
  29. rack_group = models.ForeignKey(
  30. to='RackGroup',
  31. on_delete=models.PROTECT,
  32. blank=True,
  33. null=True
  34. )
  35. name = models.CharField(
  36. max_length=50
  37. )
  38. tags = TaggableManager(through=TaggedItem)
  39. objects = RestrictedQuerySet.as_manager()
  40. csv_headers = ['site', 'rack_group', 'name']
  41. class Meta:
  42. ordering = ['site', 'name']
  43. unique_together = ['site', 'name']
  44. def __str__(self):
  45. return self.name
  46. def get_absolute_url(self):
  47. return reverse('dcim:powerpanel', args=[self.pk])
  48. def to_csv(self):
  49. return (
  50. self.site.name,
  51. self.rack_group.name if self.rack_group else None,
  52. self.name,
  53. )
  54. def clean(self):
  55. # RackGroup must belong to assigned Site
  56. if self.rack_group and self.rack_group.site != self.site:
  57. raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
  58. self.rack_group, self.rack_group.site, self.site
  59. ))
  60. @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
  61. class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
  62. """
  63. An electrical circuit delivered from a PowerPanel.
  64. """
  65. power_panel = models.ForeignKey(
  66. to='PowerPanel',
  67. on_delete=models.PROTECT,
  68. related_name='powerfeeds'
  69. )
  70. rack = models.ForeignKey(
  71. to='Rack',
  72. on_delete=models.PROTECT,
  73. blank=True,
  74. null=True
  75. )
  76. connected_endpoint = models.OneToOneField(
  77. to='dcim.PowerPort',
  78. on_delete=models.SET_NULL,
  79. related_name='+',
  80. blank=True,
  81. null=True
  82. )
  83. connection_status = models.BooleanField(
  84. choices=CONNECTION_STATUS_CHOICES,
  85. blank=True,
  86. null=True
  87. )
  88. name = models.CharField(
  89. max_length=50
  90. )
  91. status = models.CharField(
  92. max_length=50,
  93. choices=PowerFeedStatusChoices,
  94. default=PowerFeedStatusChoices.STATUS_ACTIVE
  95. )
  96. type = models.CharField(
  97. max_length=50,
  98. choices=PowerFeedTypeChoices,
  99. default=PowerFeedTypeChoices.TYPE_PRIMARY
  100. )
  101. supply = models.CharField(
  102. max_length=50,
  103. choices=PowerFeedSupplyChoices,
  104. default=PowerFeedSupplyChoices.SUPPLY_AC
  105. )
  106. phase = models.CharField(
  107. max_length=50,
  108. choices=PowerFeedPhaseChoices,
  109. default=PowerFeedPhaseChoices.PHASE_SINGLE
  110. )
  111. voltage = models.SmallIntegerField(
  112. default=POWERFEED_VOLTAGE_DEFAULT,
  113. validators=[ExclusionValidator([0])]
  114. )
  115. amperage = models.PositiveSmallIntegerField(
  116. validators=[MinValueValidator(1)],
  117. default=POWERFEED_AMPERAGE_DEFAULT
  118. )
  119. max_utilization = models.PositiveSmallIntegerField(
  120. validators=[MinValueValidator(1), MaxValueValidator(100)],
  121. default=POWERFEED_MAX_UTILIZATION_DEFAULT,
  122. help_text="Maximum permissible draw (percentage)"
  123. )
  124. available_power = models.PositiveIntegerField(
  125. default=0,
  126. editable=False
  127. )
  128. comments = models.TextField(
  129. blank=True
  130. )
  131. tags = TaggableManager(through=TaggedItem)
  132. objects = RestrictedQuerySet.as_manager()
  133. csv_headers = [
  134. 'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
  135. 'amperage', 'max_utilization', 'comments',
  136. ]
  137. clone_fields = [
  138. 'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
  139. 'available_power',
  140. ]
  141. STATUS_CLASS_MAP = {
  142. PowerFeedStatusChoices.STATUS_OFFLINE: 'warning',
  143. PowerFeedStatusChoices.STATUS_ACTIVE: 'success',
  144. PowerFeedStatusChoices.STATUS_PLANNED: 'info',
  145. PowerFeedStatusChoices.STATUS_FAILED: 'danger',
  146. }
  147. TYPE_CLASS_MAP = {
  148. PowerFeedTypeChoices.TYPE_PRIMARY: 'success',
  149. PowerFeedTypeChoices.TYPE_REDUNDANT: 'info',
  150. }
  151. class Meta:
  152. ordering = ['power_panel', 'name']
  153. unique_together = ['power_panel', 'name']
  154. def __str__(self):
  155. return self.name
  156. def get_absolute_url(self):
  157. return reverse('dcim:powerfeed', args=[self.pk])
  158. def to_csv(self):
  159. return (
  160. self.power_panel.site.name,
  161. self.power_panel.name,
  162. self.rack.group.name if self.rack and self.rack.group else None,
  163. self.rack.name if self.rack else None,
  164. self.name,
  165. self.get_status_display(),
  166. self.get_type_display(),
  167. self.get_supply_display(),
  168. self.get_phase_display(),
  169. self.voltage,
  170. self.amperage,
  171. self.max_utilization,
  172. self.comments,
  173. )
  174. def clean(self):
  175. # Rack must belong to same Site as PowerPanel
  176. if self.rack and self.rack.site != self.power_panel.site:
  177. raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
  178. self.rack, self.rack.site, self.power_panel, self.power_panel.site
  179. ))
  180. # AC voltage cannot be negative
  181. if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
  182. raise ValidationError({
  183. "voltage": "Voltage cannot be negative for AC supply"
  184. })
  185. def save(self, *args, **kwargs):
  186. # Cache the available_power property on the instance
  187. kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100)
  188. if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
  189. self.available_power = round(kva * 1.732)
  190. else:
  191. self.available_power = round(kva)
  192. super().save(*args, **kwargs)
  193. @property
  194. def parent(self):
  195. return self.power_panel
  196. def get_type_class(self):
  197. return self.TYPE_CLASS_MAP.get(self.type)
  198. def get_status_class(self):
  199. return self.STATUS_CLASS_MAP.get(self.status)