From 2eeffce924321b635e7c0270691b45fa871bc433 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Jun 2019 06:11:32 -0400 Subject: [PATCH] #524 - Added power utilization graphs to power feeds, devices, and racks --- CHANGELOG.md | 4 ++ .../0074_powerfeed_available_power_cache.py | 18 ++++++ netbox/dcim/models.py | 58 ++++++++++++++++--- netbox/dcim/tables.py | 3 +- netbox/templates/dcim/device.html | 2 + netbox/templates/dcim/powerfeed.html | 23 ++++++++ netbox/templates/dcim/rack.html | 2 + 7 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 netbox/dcim/migrations/0074_powerfeed_available_power_cache.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 705736d30..e4c641ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ v2.6.0 (FUTURE) * [#3204](https://github.com/digitalocean/netbox/issues/3204) - Fix interface filtering when connecting cables * [#3207](https://github.com/digitalocean/netbox/issues/3207) - Fix link for connecting interface to rear port +## Enhancements (From Beta) + +* [#524](https://github.com/digitalocean/netbox/issues/524) - Added power utilization graphs to power feeds, devices, and racks + --- v2.6-beta1 (2019-04-29) diff --git a/netbox/dcim/migrations/0074_powerfeed_available_power_cache.py b/netbox/dcim/migrations/0074_powerfeed_available_power_cache.py new file mode 100644 index 000000000..526ef89b1 --- /dev/null +++ b/netbox/dcim/migrations/0074_powerfeed_available_power_cache.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2019-06-16 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0073_interface_form_factor_to_type'), + ] + + operations = [ + migrations.AddField( + model_name='powerfeed', + name='available_power', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 61f2159be..73579b3bb 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, Q, Sum +from django.db.models import Case, Count, Q, Sum, When, F, Subquery, OuterRef from django.urls import reverse from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager @@ -734,6 +734,25 @@ class Rack(ChangeLoggedModel, CustomFieldModel): u_available = len(self.get_available_units()) return int(float(self.u_height - u_available) / self.u_height * 100) + def get_power_utilization(self): + """ + Determine the utilization rate of power in the rack and return it as a percentage. + """ + power_stats = PowerFeed.objects.filter( + rack=self + ).annotate( + allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'), + ).values( + 'allocated_draw_total', + 'available_power' + ) + + if power_stats: + allocated_draw_total = sum(x['allocated_draw_total'] for x in power_stats) + available_power_total = sum(x['available_power'] for x in power_stats) + return int(allocated_draw_total / available_power_total * 100) or 0 + return 0 + class RackReservation(ChangeLoggedModel): """ @@ -1961,6 +1980,10 @@ class PowerPort(CableTermination, ComponentModel): ) utilization['outlets'] = len(outlet_ids) utilization['available_power'] = powerfeed_available + allocated_utilization = int( + float(utilization['allocated_draw_total'] or 0) / powerfeed_available * 100 + ) + utilization['allocated_utilization'] = allocated_utilization stats.append(utilization) # Per-leg stats for three-phase feeds @@ -1974,6 +1997,10 @@ class PowerPort(CableTermination, ComponentModel): utilization['name'] = 'Leg {}'.format(leg_name) utilization['outlets'] = len(outlet_ids) utilization['available_power'] = round(powerfeed_available / 3) + allocated_utilization = int( + float(utilization['allocated_draw_total'] or 0) / powerfeed_available * 100 + ) + utilization['allocated_utilization'] = allocated_utilization stats.append(utilization) return stats @@ -2936,6 +2963,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): validators=[MinValueValidator(1)], default=20 ) + available_power = models.PositiveSmallIntegerField( + default=0 + ) power_factor = models.PositiveSmallIntegerField( validators=[MinValueValidator(1), MaxValueValidator(100)], default=80, @@ -2990,15 +3020,29 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): self.rack, self.rack.site, self.power_panel, self.power_panel.site )) + def save(self, *args, **kwargs): + + # Cache the available_power property on the instance + kva = self.voltage * self.amperage * (self.power_factor / 100) + if self.phase == POWERFEED_PHASE_3PHASE: + self.available_power = round(kva * 1.732) + self.available_power = round(kva) + + super().save(*args, **kwargs) + def get_type_class(self): return STATUS_CLASSES[self.type] def get_status_class(self): return STATUS_CLASSES[self.status] - @property - def available_power(self): - kva = self.voltage * self.amperage * (self.power_factor / 100) - if self.phase == POWERFEED_PHASE_3PHASE: - return round(kva * 1.732) - return round(kva) + def get_power_stats(self): + """ + Return power utilization statistics + """ + power_port = self.connected_endpoint + if not power_port: + # Nothing is connected to the feed so it is not being utilized + return None + + return power_port.get_power_stats() diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 55afb224e..bbd30ac0b 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -300,11 +300,12 @@ class RackDetailTable(RackTable): verbose_name='Devices' ) get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') + get_power_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Power Utilization') class Meta(RackTable.Meta): fields = ( 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', - 'get_utilization', + 'get_utilization', 'get_power_utilization', ) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 19a59a47a..5a0515b06 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -351,6 +351,7 @@ Outlets Allocated/Max (W) Available (VA) + Utilization (Allocated) {% for pp in power_ports %} {% for leg in pp.get_power_stats %} @@ -363,6 +364,7 @@ {{ leg.outlets|placeholder }} {{ leg.allocated_draw_total }} / {{ leg.maximum_draw_total }} {{ leg.available_power }} + {% utilization_graph leg.allocated_utilization %} {% endfor %} {% endfor %} diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index 5a52f6336..1bfea4c73 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -138,6 +138,29 @@ +
+
+
+ Power Utilization +
+ + + + + + + + {% for leg in powerfeed.get_power_stats %} + + + + + + + {% endfor %} +
OutletsAllocated/Max (W)Available (VA)Utilization (Allocated)
{{ leg.outlets|placeholder }}{{ leg.allocated_draw_total }} / {{ leg.maximum_draw_total }}{{ leg.available_power }}{% utilization_graph leg.allocated_utilization %}
+
+
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 116489fbd..300b6ee80 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -207,6 +207,7 @@ Feed Status Type + Utilization {% for powerfeed in power_feeds %} @@ -222,6 +223,7 @@ {{ powerfeed.get_type_display }} + {% utilization_graph powerfeed.get_power_stats.0.allocated_utilization %} {% endfor %}