| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- from django.core.exceptions import ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from django.urls import reverse
- from dcim.choices import *
- from dcim.constants import *
- from extras.utils import extras_features
- from netbox.models import PrimaryModel
- from utilities.querysets import RestrictedQuerySet
- from utilities.validators import ExclusionValidator
- from .device_components import CableTermination, PathEndpoint
- __all__ = (
- 'PowerFeed',
- 'PowerPanel',
- )
- #
- # Power
- #
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
- class PowerPanel(PrimaryModel):
- """
- A distribution point for electrical power; e.g. a data center RPP.
- """
- site = models.ForeignKey(
- to='Site',
- on_delete=models.PROTECT
- )
- location = models.ForeignKey(
- to='dcim.Location',
- on_delete=models.PROTECT,
- blank=True,
- null=True
- )
- name = models.CharField(
- max_length=100
- )
- objects = RestrictedQuerySet.as_manager()
- csv_headers = ['site', 'location', 'name']
- class Meta:
- ordering = ['site', 'name']
- unique_together = ['site', 'name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerpanel', args=[self.pk])
- def to_csv(self):
- return (
- self.site.name,
- self.location.name if self.location else None,
- self.name,
- )
- def clean(self):
- super().clean()
- # Location must belong to assigned Site
- if self.location and self.location.site != self.site:
- raise ValidationError(
- f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
- )
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
- class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
- """
- An electrical circuit delivered from a PowerPanel.
- """
- power_panel = models.ForeignKey(
- to='PowerPanel',
- on_delete=models.PROTECT,
- related_name='powerfeeds'
- )
- rack = models.ForeignKey(
- to='Rack',
- on_delete=models.PROTECT,
- blank=True,
- null=True
- )
- name = models.CharField(
- max_length=100
- )
- status = models.CharField(
- max_length=50,
- choices=PowerFeedStatusChoices,
- default=PowerFeedStatusChoices.STATUS_ACTIVE
- )
- type = models.CharField(
- max_length=50,
- choices=PowerFeedTypeChoices,
- default=PowerFeedTypeChoices.TYPE_PRIMARY
- )
- supply = models.CharField(
- max_length=50,
- choices=PowerFeedSupplyChoices,
- default=PowerFeedSupplyChoices.SUPPLY_AC
- )
- phase = models.CharField(
- max_length=50,
- choices=PowerFeedPhaseChoices,
- default=PowerFeedPhaseChoices.PHASE_SINGLE
- )
- voltage = models.SmallIntegerField(
- default=POWERFEED_VOLTAGE_DEFAULT,
- validators=[ExclusionValidator([0])]
- )
- amperage = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1)],
- default=POWERFEED_AMPERAGE_DEFAULT
- )
- max_utilization = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1), MaxValueValidator(100)],
- default=POWERFEED_MAX_UTILIZATION_DEFAULT,
- help_text="Maximum permissible draw (percentage)"
- )
- available_power = models.PositiveIntegerField(
- default=0,
- editable=False
- )
- comments = models.TextField(
- blank=True
- )
- objects = RestrictedQuerySet.as_manager()
- csv_headers = [
- 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
- 'voltage', 'amperage', 'max_utilization', 'comments',
- ]
- clone_fields = [
- 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
- 'max_utilization', 'available_power',
- ]
- class Meta:
- ordering = ['power_panel', 'name']
- unique_together = ['power_panel', 'name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerfeed', args=[self.pk])
- def to_csv(self):
- return (
- self.power_panel.site.name,
- self.power_panel.name,
- self.rack.location.name if self.rack and self.rack.location else None,
- self.rack.name if self.rack else None,
- self.name,
- self.get_status_display(),
- self.get_type_display(),
- self.mark_connected,
- self.get_supply_display(),
- self.get_phase_display(),
- self.voltage,
- self.amperage,
- self.max_utilization,
- self.comments,
- )
- def clean(self):
- super().clean()
- # Rack must belong to same Site as PowerPanel
- if self.rack and self.rack.site != self.power_panel.site:
- raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
- self.rack, self.rack.site, self.power_panel, self.power_panel.site
- ))
- # AC voltage cannot be negative
- if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
- raise ValidationError({
- "voltage": "Voltage cannot be negative for AC supply"
- })
- def save(self, *args, **kwargs):
- # Cache the available_power property on the instance
- kva = abs(self.voltage) * self.amperage * (self.max_utilization / 100)
- if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
- self.available_power = round(kva * 1.732)
- else:
- self.available_power = round(kva)
- super().save(*args, **kwargs)
- @property
- def parent_object(self):
- return self.power_panel
- def get_type_class(self):
- return PowerFeedTypeChoices.CSS_CLASSES.get(self.type)
- def get_status_class(self):
- return PowerFeedStatusChoices.CSS_CLASSES.get(self.status)
|