power.py 6.0 KB

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