Browse Source

#524 - Added power utilization graphs to power feeds, devices, and racks

John Anderson 6 years ago
parent
commit
2eeffce924

+ 4 - 0
CHANGELOG.md

@@ -12,6 +12,10 @@ v2.6.0 (FUTURE)
 * [#3204](https://github.com/digitalocean/netbox/issues/3204) - Fix interface filtering when connecting cables
 * [#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
 * [#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)
 v2.6-beta1 (2019-04-29)

+ 18 - 0
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),
+        ),
+    ]

+ 51 - 7
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.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 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 django.urls import reverse
 from mptt.models import MPTTModel, TreeForeignKey
 from mptt.models import MPTTModel, TreeForeignKey
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
@@ -734,6 +734,25 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
         u_available = len(self.get_available_units())
         u_available = len(self.get_available_units())
         return int(float(self.u_height - u_available) / self.u_height * 100)
         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):
 class RackReservation(ChangeLoggedModel):
     """
     """
@@ -1961,6 +1980,10 @@ class PowerPort(CableTermination, ComponentModel):
         )
         )
         utilization['outlets'] = len(outlet_ids)
         utilization['outlets'] = len(outlet_ids)
         utilization['available_power'] = powerfeed_available
         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)
         stats.append(utilization)
 
 
         # Per-leg stats for three-phase feeds
         # Per-leg stats for three-phase feeds
@@ -1974,6 +1997,10 @@ class PowerPort(CableTermination, ComponentModel):
                 utilization['name'] = 'Leg {}'.format(leg_name)
                 utilization['name'] = 'Leg {}'.format(leg_name)
                 utilization['outlets'] = len(outlet_ids)
                 utilization['outlets'] = len(outlet_ids)
                 utilization['available_power'] = round(powerfeed_available / 3)
                 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)
                 stats.append(utilization)
 
 
         return stats
         return stats
@@ -2936,6 +2963,9 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         validators=[MinValueValidator(1)],
         validators=[MinValueValidator(1)],
         default=20
         default=20
     )
     )
+    available_power = models.PositiveSmallIntegerField(
+        default=0
+    )
     power_factor = models.PositiveSmallIntegerField(
     power_factor = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(100)],
         validators=[MinValueValidator(1), MaxValueValidator(100)],
         default=80,
         default=80,
@@ -2990,15 +3020,29 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
                 self.rack, self.rack.site, self.power_panel, self.power_panel.site
                 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):
     def get_type_class(self):
         return STATUS_CLASSES[self.type]
         return STATUS_CLASSES[self.type]
 
 
     def get_status_class(self):
     def get_status_class(self):
         return STATUS_CLASSES[self.status]
         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()

+ 2 - 1
netbox/dcim/tables.py

@@ -300,11 +300,12 @@ class RackDetailTable(RackTable):
         verbose_name='Devices'
         verbose_name='Devices'
     )
     )
     get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
     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):
     class Meta(RackTable.Meta):
         fields = (
         fields = (
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
-            'get_utilization',
+            'get_utilization', 'get_power_utilization',
         )
         )
 
 
 
 

+ 2 - 0
netbox/templates/dcim/device.html

@@ -351,6 +351,7 @@
                             <th>Outlets</th>
                             <th>Outlets</th>
                             <th>Allocated/Max (W)</th>
                             <th>Allocated/Max (W)</th>
                             <th>Available (VA)</th>
                             <th>Available (VA)</th>
+                            <th>Utilization (Allocated)</th>
                         </tr>
                         </tr>
                         {% for pp in power_ports %}
                         {% for pp in power_ports %}
                             {% for leg in pp.get_power_stats %}
                             {% for leg in pp.get_power_stats %}
@@ -363,6 +364,7 @@
                                     <td>{{ leg.outlets|placeholder }}</td>
                                     <td>{{ leg.outlets|placeholder }}</td>
                                     <td>{{ leg.allocated_draw_total }} / {{ leg.maximum_draw_total }}</td>
                                     <td>{{ leg.allocated_draw_total }} / {{ leg.maximum_draw_total }}</td>
                                     <td>{{ leg.available_power }}</td>
                                     <td>{{ leg.available_power }}</td>
+                                    <td>{% utilization_graph leg.allocated_utilization %}</td>
                                 </tr>
                                 </tr>
                             {% endfor %}
                             {% endfor %}
                         {% endfor %}
                         {% endfor %}

+ 23 - 0
netbox/templates/dcim/powerfeed.html

@@ -138,6 +138,29 @@
             </table>
             </table>
         </div>
         </div>
     </div>
     </div>
+    <div class="col-md-6">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Power Utilization</strong>
+            </div>
+            <table class="table table-hover panel-body">
+                <tr>
+                    <th>Outlets</th>
+                    <th>Allocated/Max (W)</th>
+                    <th>Available (VA)</th>
+                    <th>Utilization (Allocated)</th>
+                </tr>
+                {% for leg in powerfeed.get_power_stats %}
+                    <tr>
+                        <td>{{ leg.outlets|placeholder }}</td>
+                        <td>{{ leg.allocated_draw_total }} / {{ leg.maximum_draw_total }}</td>
+                        <td>{{ leg.available_power }}</td>
+                        <td>{% utilization_graph leg.allocated_utilization %}</td>
+                    </tr>
+                {% endfor %}
+            </table>
+        </div>
+    </div>
     <div class="col-md-5">
     <div class="col-md-5">
 	</div>
 	</div>
 </div>
 </div>

+ 2 - 0
netbox/templates/dcim/rack.html

@@ -207,6 +207,7 @@
                         <th>Feed</th>
                         <th>Feed</th>
                         <th>Status</th>
                         <th>Status</th>
                         <th>Type</th>
                         <th>Type</th>
+                        <th>Utilization</th>
                     </tr>
                     </tr>
                     {% for powerfeed in power_feeds %}
                     {% for powerfeed in power_feeds %}
                         <tr>
                         <tr>
@@ -222,6 +223,7 @@
                             <td>
                             <td>
                                 <span class="label label-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
                                 <span class="label label-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
                             </td>
                             </td>
+                            <td>{% utilization_graph powerfeed.get_power_stats.0.allocated_utilization %}</td>
                         </tr>
                         </tr>
                     {% endfor %}
                     {% endfor %}
                 </table>
                 </table>