power.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. from django.contrib.contenttypes.fields import GenericRelation
  2. from django.core.exceptions import ValidationError
  3. from django.core.validators import MaxValueValidator, MinValueValidator
  4. from django.db import models
  5. from django.urls import reverse
  6. from dcim.choices import *
  7. from dcim.constants import *
  8. from netbox.models import NetBoxModel
  9. from utilities.validators import ExclusionValidator
  10. from .device_components import LinkTermination, PathEndpoint
  11. __all__ = (
  12. 'PowerFeed',
  13. 'PowerPanel',
  14. )
  15. #
  16. # Power
  17. #
  18. class PowerPanel(NetBoxModel):
  19. """
  20. A distribution point for electrical power; e.g. a data center RPP.
  21. """
  22. site = models.ForeignKey(
  23. to='Site',
  24. on_delete=models.PROTECT
  25. )
  26. location = models.ForeignKey(
  27. to='dcim.Location',
  28. on_delete=models.PROTECT,
  29. blank=True,
  30. null=True
  31. )
  32. name = models.CharField(
  33. max_length=100
  34. )
  35. # Generic relations
  36. contacts = GenericRelation(
  37. to='tenancy.ContactAssignment'
  38. )
  39. images = GenericRelation(
  40. to='extras.ImageAttachment'
  41. )
  42. class Meta:
  43. ordering = ['site', 'name']
  44. unique_together = ['site', 'name']
  45. def __str__(self):
  46. return self.name
  47. def get_absolute_url(self):
  48. return reverse('dcim:powerpanel', args=[self.pk])
  49. def clean(self):
  50. super().clean()
  51. # Location must belong to assigned Site
  52. if self.location and self.location.site != self.site:
  53. raise ValidationError(
  54. f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
  55. )
  56. class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
  57. """
  58. An electrical circuit delivered from a PowerPanel.
  59. """
  60. power_panel = models.ForeignKey(
  61. to='PowerPanel',
  62. on_delete=models.PROTECT,
  63. related_name='powerfeeds'
  64. )
  65. rack = models.ForeignKey(
  66. to='Rack',
  67. on_delete=models.PROTECT,
  68. blank=True,
  69. null=True
  70. )
  71. name = models.CharField(
  72. max_length=100
  73. )
  74. status = models.CharField(
  75. max_length=50,
  76. choices=PowerFeedStatusChoices,
  77. default=PowerFeedStatusChoices.STATUS_ACTIVE
  78. )
  79. type = models.CharField(
  80. max_length=50,
  81. choices=PowerFeedTypeChoices,
  82. default=PowerFeedTypeChoices.TYPE_PRIMARY
  83. )
  84. supply = models.CharField(
  85. max_length=50,
  86. choices=PowerFeedSupplyChoices,
  87. default=PowerFeedSupplyChoices.SUPPLY_AC
  88. )
  89. phase = models.CharField(
  90. max_length=50,
  91. choices=PowerFeedPhaseChoices,
  92. default=PowerFeedPhaseChoices.PHASE_SINGLE
  93. )
  94. voltage = models.SmallIntegerField(
  95. default=POWERFEED_VOLTAGE_DEFAULT,
  96. validators=[ExclusionValidator([0])]
  97. )
  98. amperage = models.PositiveSmallIntegerField(
  99. validators=[MinValueValidator(1)],
  100. default=POWERFEED_AMPERAGE_DEFAULT
  101. )
  102. max_utilization = models.PositiveSmallIntegerField(
  103. validators=[MinValueValidator(1), MaxValueValidator(100)],
  104. default=POWERFEED_MAX_UTILIZATION_DEFAULT,
  105. help_text="Maximum permissible draw (percentage)"
  106. )
  107. available_power = models.PositiveIntegerField(
  108. default=0,
  109. editable=False
  110. )
  111. comments = models.TextField(
  112. blank=True
  113. )
  114. clone_fields = [
  115. 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
  116. 'max_utilization', 'available_power',
  117. ]
  118. class Meta:
  119. ordering = ['power_panel', 'name']
  120. unique_together = ['power_panel', 'name']
  121. def __str__(self):
  122. return self.name
  123. def get_absolute_url(self):
  124. return reverse('dcim:powerfeed', args=[self.pk])
  125. def clean(self):
  126. super().clean()
  127. # Rack must belong to same Site as PowerPanel
  128. if self.rack and self.rack.site != self.power_panel.site:
  129. raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
  130. self.rack, self.rack.site, self.power_panel, self.power_panel.site
  131. ))
  132. # AC voltage cannot be negative
  133. if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
  134. raise ValidationError({
  135. "voltage": "Voltage cannot be negative for AC supply"
  136. })
  137. def save(self, *args, **kwargs):
  138. # Cache the available_power property on the instance
  139. kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100)
  140. if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
  141. self.available_power = round(kva * 1.732)
  142. else:
  143. self.available_power = round(kva)
  144. super().save(*args, **kwargs)
  145. @property
  146. def parent_object(self):
  147. return self.power_panel