mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-08 00:28:16 -06:00
Cascade power draw calculation on all downstream power ports
This commit is contained in:
parent
8bcf12dbd1
commit
be307b4574
@ -708,19 +708,17 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
|||||||
"""
|
"""
|
||||||
Determine the utilization rate of power in the rack and return it as a percentage.
|
Determine the utilization rate of power in the rack and return it as a percentage.
|
||||||
"""
|
"""
|
||||||
power_stats = PowerFeed.objects.filter(
|
# Sum up all of the available power from the power feeds assigned to the rack
|
||||||
rack=self
|
available_power = PowerFeed.objects.filter(rack=self).aggregate(total=Sum('available_power'))
|
||||||
).annotate(
|
|
||||||
allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'),
|
|
||||||
).values(
|
|
||||||
'allocated_draw_total',
|
|
||||||
'available_power'
|
|
||||||
)
|
|
||||||
|
|
||||||
if power_stats:
|
# Get the power draw of the power ports from the power feeds assigned to the rack
|
||||||
allocated_draw_total = sum(x['allocated_draw_total'] for x in power_stats)
|
power_ports = PowerPort.objects.filter(_connected_powerfeed__rack=self)
|
||||||
available_power_total = sum(x['available_power'] for x in power_stats)
|
feeds_stats = [x.get_power_draw(leg_stats=False) for x in power_ports]
|
||||||
return int(allocated_draw_total / available_power_total * 100) or 0
|
|
||||||
|
if available_power.get('total') and feeds_stats:
|
||||||
|
available_power_total = available_power.get('total')
|
||||||
|
allocated_draw_total = sum([x.get('allocated') or 0 for x in feeds_stats])
|
||||||
|
return round(allocated_draw_total / available_power_total * 100)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from cacheops import cached_as
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -383,37 +384,55 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
"Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
|
"Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_power_draw(self):
|
def get_power_draw(self, leg_stats=True):
|
||||||
"""
|
"""
|
||||||
Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
|
Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
|
||||||
|
If `leg_stats` is True, the `legs` key in the returned dict is populated with a list of per-leg statistics.
|
||||||
"""
|
"""
|
||||||
|
def get_power_stats(leg=None):
|
||||||
|
"""
|
||||||
|
Return tuple of (outlet_count, allocated_draw_total, maximum_draw_total).
|
||||||
|
"""
|
||||||
|
if leg:
|
||||||
|
outlets = PowerOutlet.objects.filter(power_port=self, feed_leg=leg)
|
||||||
|
else:
|
||||||
|
outlets = PowerOutlet.objects.filter(power_port=self)
|
||||||
|
|
||||||
|
# The outlets are used as extra to invalidate the cache when an outlet's leg is changed
|
||||||
|
@cached_as(self, extra=outlets)
|
||||||
|
def _stats():
|
||||||
|
# Power ports drawing power from the local outlets
|
||||||
|
return PowerPort.objects.filter(
|
||||||
|
pk__in=outlets.values_list('downstream_powerports', flat=True),
|
||||||
|
).aggregate(
|
||||||
|
Sum('allocated_draw'),
|
||||||
|
Sum('maximum_draw'),
|
||||||
|
)
|
||||||
|
|
||||||
|
stats = _stats()
|
||||||
|
|
||||||
|
return outlets.count(), stats.get('allocated_draw__sum') or 0, stats.get('maximum_draw__sum') or 0
|
||||||
|
|
||||||
# Calculate aggregate draw of all child power outlets if no numbers have been defined manually
|
# Calculate aggregate draw of all child power outlets if no numbers have been defined manually
|
||||||
if self.allocated_draw is None and self.maximum_draw is None:
|
if self.allocated_draw is None and self.maximum_draw is None:
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
# Global results
|
||||||
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
|
outlet_count, allocated_draw_total, maximum_draw_total = get_power_stats()
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
|
||||||
)
|
|
||||||
ret = {
|
ret = {
|
||||||
'allocated': utilization['allocated_draw_total'] or 0,
|
'allocated': allocated_draw_total,
|
||||||
'maximum': utilization['maximum_draw_total'] or 0,
|
'maximum': maximum_draw_total,
|
||||||
'outlet_count': len(outlet_ids),
|
'outlet_count': outlet_count,
|
||||||
'legs': [],
|
'legs': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate per-leg aggregates for three-phase feeds
|
# Calculate per-leg aggregates for three-phase feeds
|
||||||
if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
|
if leg_stats and getattr(self._connected_powerfeed, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE:
|
||||||
for leg, leg_name in PowerOutletFeedLegChoices:
|
for leg, leg_name in PowerOutletFeedLegChoices:
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
outlet_count, allocated_draw_total, maximum_draw_total = get_power_stats(leg)
|
||||||
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate(
|
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
|
||||||
)
|
|
||||||
ret['legs'].append({
|
ret['legs'].append({
|
||||||
'name': leg_name,
|
'name': leg_name,
|
||||||
'allocated': utilization['allocated_draw_total'] or 0,
|
'allocated': allocated_draw_total,
|
||||||
'maximum': utilization['maximum_draw_total'] or 0,
|
'maximum': maximum_draw_total,
|
||||||
'outlet_count': len(outlet_ids),
|
'outlet_count': outlet_count,
|
||||||
})
|
})
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -552,3 +552,173 @@ class CablePathTestCase(TestCase):
|
|||||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||||
self.assertIsNone(interface2.connected_endpoint)
|
self.assertIsNone(interface2.connected_endpoint)
|
||||||
self.assertIsNone(interface2.connection_status)
|
self.assertIsNone(interface2.connection_status)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerCalculationTestCase(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
The power toplogy:
|
||||||
|
|
||||||
|
power_feed --> power_port1
|
||||||
|
power_outlet12 --> power_port21
|
||||||
|
power_outlet13 --> power_port31
|
||||||
|
power_outlet34 --> power_port43
|
||||||
|
|
||||||
|
The two numbers at the end denote the direction, so power_outlet12 is from device 1 to device 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||||
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
|
||||||
|
power_panel = PowerPanel.objects.create(name='Power Panel 1', site=site)
|
||||||
|
|
||||||
|
self.device1 = Device.objects.create(
|
||||||
|
site=site,
|
||||||
|
device_type=device_type,
|
||||||
|
device_role=device_role,
|
||||||
|
name='Device 1'
|
||||||
|
)
|
||||||
|
self.device2 = Device.objects.create(
|
||||||
|
site=site,
|
||||||
|
device_type=device_type,
|
||||||
|
device_role=device_role,
|
||||||
|
name='Device 2'
|
||||||
|
)
|
||||||
|
self.device3 = Device.objects.create(
|
||||||
|
site=site,
|
||||||
|
device_type=device_type,
|
||||||
|
device_role=device_role,
|
||||||
|
name='Device 3'
|
||||||
|
)
|
||||||
|
self.device4 = Device.objects.create(
|
||||||
|
site=site,
|
||||||
|
device_type=device_type,
|
||||||
|
device_role=device_role,
|
||||||
|
name='Device 4'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Power from power feed to device 1
|
||||||
|
self.power_feed = PowerFeed.objects.create(
|
||||||
|
name='Power Feed 1',
|
||||||
|
power_panel=power_panel,
|
||||||
|
phase=PowerFeedPhaseChoices.PHASE_3PHASE
|
||||||
|
)
|
||||||
|
self.power_port1 = PowerPort.objects.create(device=self.device1, name='Power Port 1')
|
||||||
|
self.power_feed_cable = Cable.objects.create(termination_a=self.power_port1, termination_b=self.power_feed)
|
||||||
|
|
||||||
|
# Power from device 1 to device 2
|
||||||
|
self.power_outlet12 = PowerOutlet.objects.create(
|
||||||
|
device=self.device1,
|
||||||
|
name='Power Outlet 12',
|
||||||
|
power_port=self.power_port1,
|
||||||
|
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
|
||||||
|
)
|
||||||
|
self.power_port21 = PowerPort.objects.create(
|
||||||
|
device=self.device2,
|
||||||
|
name='Power Port 21',
|
||||||
|
maximum_draw=25,
|
||||||
|
allocated_draw=10,
|
||||||
|
)
|
||||||
|
Cable.objects.create(termination_a=self.power_outlet12, termination_b=self.power_port21)
|
||||||
|
|
||||||
|
# Power from device 1 to device 3
|
||||||
|
self.power_outlet13 = PowerOutlet.objects.create(
|
||||||
|
device=self.device1,
|
||||||
|
name='Power Outlet 13',
|
||||||
|
power_port=self.power_port1,
|
||||||
|
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||||
|
)
|
||||||
|
self.power_port31 = PowerPort.objects.create(
|
||||||
|
device=self.device3,
|
||||||
|
name='Power Port 31',
|
||||||
|
maximum_draw=7,
|
||||||
|
allocated_draw=5,
|
||||||
|
)
|
||||||
|
Cable.objects.create(termination_a=self.power_outlet13, termination_b=self.power_port31)
|
||||||
|
|
||||||
|
# Power from device 3 to device 4
|
||||||
|
self.power_outlet34 = PowerOutlet.objects.create(
|
||||||
|
device=self.device3,
|
||||||
|
name='Power Outlet 34',
|
||||||
|
power_port=self.power_port31,
|
||||||
|
)
|
||||||
|
self.power_port43 = PowerPort.objects.create(
|
||||||
|
device=self.device4,
|
||||||
|
name='Power Port 43',
|
||||||
|
maximum_draw=4,
|
||||||
|
allocated_draw=3,
|
||||||
|
)
|
||||||
|
Cable.objects.create(termination_a=self.power_outlet34, termination_b=self.power_port43)
|
||||||
|
|
||||||
|
def test_power_cascade(self):
|
||||||
|
"""
|
||||||
|
Ensure the power calculation cascades over all children. The outlet count is local-only.
|
||||||
|
"""
|
||||||
|
stats = self.power_port1.get_power_draw()
|
||||||
|
|
||||||
|
# Global stats: all devices/feeds
|
||||||
|
self.assertEqual(stats['maximum'], 25 + 7 + 4)
|
||||||
|
self.assertEqual(stats['allocated'], 10 + 5 + 3)
|
||||||
|
self.assertEqual(stats['outlet_count'], 2)
|
||||||
|
|
||||||
|
# Feed A stats: device 2
|
||||||
|
self.assertEqual(stats['legs'][0]['maximum'], 25)
|
||||||
|
self.assertEqual(stats['legs'][0]['allocated'], 10)
|
||||||
|
self.assertEqual(stats['legs'][0]['outlet_count'], 1)
|
||||||
|
|
||||||
|
# Feed B stats: devices 3 and 4 (through 3)
|
||||||
|
self.assertEqual(stats['legs'][1]['maximum'], 7 + 4)
|
||||||
|
self.assertEqual(stats['legs'][1]['allocated'], 5 + 3)
|
||||||
|
self.assertEqual(stats['legs'][1]['outlet_count'], 1)
|
||||||
|
|
||||||
|
# Feed C stats: no devices
|
||||||
|
self.assertEqual(stats['legs'][2]['maximum'], 0)
|
||||||
|
self.assertEqual(stats['legs'][2]['allocated'], 0)
|
||||||
|
self.assertEqual(stats['legs'][2]['outlet_count'], 0)
|
||||||
|
|
||||||
|
def test_power_loop(self):
|
||||||
|
"""
|
||||||
|
Remove the connection between the power feed and device 1. Instead connect device 4 back to 1. The
|
||||||
|
calculation should continue to be correct and the code should not infinitely loop. The children outlets should
|
||||||
|
have their downstream power ports updated.
|
||||||
|
|
||||||
|
The power toplogy becomes:
|
||||||
|
|
||||||
|
power_outlet41 -> power_port1 (loop)
|
||||||
|
power_outlet12 -> power_port21
|
||||||
|
power_outlet13 -> power_port31
|
||||||
|
power_outlet34 -> power_port43
|
||||||
|
power_outlet41 -> power_port1 (loop)
|
||||||
|
"""
|
||||||
|
# Power from device 4 to device 1
|
||||||
|
self.power_outlet41 = PowerOutlet.objects.create(
|
||||||
|
device=self.device4,
|
||||||
|
name='Power Outlet 43',
|
||||||
|
power_port=self.power_port43,
|
||||||
|
)
|
||||||
|
self.power_feed_cable.delete()
|
||||||
|
loop_cable = Cable.objects.create(termination_a=self.power_outlet41, termination_b=self.power_port1)
|
||||||
|
|
||||||
|
stats = self.power_port1.get_power_draw()
|
||||||
|
|
||||||
|
self.assertEqual(stats['maximum'], 25 + 7 + 4)
|
||||||
|
self.assertEqual(stats['allocated'], 10 + 5 + 3)
|
||||||
|
|
||||||
|
# With a loop in the topology, all of the outlets affected by the loop have the same children. power_outlet12
|
||||||
|
# is not part of the loop and should only have one child, power_port21.
|
||||||
|
self.assertEqual(self.power_outlet12.downstream_powerports.count(), 1)
|
||||||
|
self.assertEqual(self.power_outlet13.downstream_powerports.count(), 4)
|
||||||
|
self.assertEqual(self.power_outlet34.downstream_powerports.count(), 4)
|
||||||
|
self.assertEqual(self.power_outlet41.downstream_powerports.count(), 4)
|
||||||
|
|
||||||
|
# When a loop-causing cable is removed, the downstream_powerports of the other outlets in the loop should be
|
||||||
|
# updated appropriately. This test is necessary because, in a loop, each outlet is upstream and downstream of
|
||||||
|
# every other outlet in that loop.
|
||||||
|
loop_cable.delete()
|
||||||
|
|
||||||
|
self.assertEqual(self.power_outlet12.downstream_powerports.count(), 1)
|
||||||
|
self.assertEqual(self.power_outlet13.downstream_powerports.count(), 2)
|
||||||
|
self.assertEqual(self.power_outlet34.downstream_powerports.count(), 1)
|
||||||
|
self.assertEqual(self.power_outlet41.downstream_powerports.count(), 0)
|
||||||
|
Loading…
Reference in New Issue
Block a user