Browse Source

Merge pull request #686 from digitalocean/rir_stats

#667: Add utilization statistics to RIR list view
Jeremy Stretch 8 years ago
parent
commit
f5b2420b4b

+ 20 - 10
netbox/ipam/models.py

@@ -22,23 +22,33 @@ AF_CHOICES = (
     (6, 'IPv6'),
 )
 
+PREFIX_STATUS_CONTAINER = 0
+PREFIX_STATUS_ACTIVE = 1
+PREFIX_STATUS_RESERVED = 2
+PREFIX_STATUS_DEPRECATED = 3
 PREFIX_STATUS_CHOICES = (
-    (0, 'Container'),
-    (1, 'Active'),
-    (2, 'Reserved'),
-    (3, 'Deprecated')
+    (PREFIX_STATUS_CONTAINER, 'Container'),
+    (PREFIX_STATUS_ACTIVE, 'Active'),
+    (PREFIX_STATUS_RESERVED, 'Reserved'),
+    (PREFIX_STATUS_DEPRECATED, 'Deprecated')
 )
 
+IPADDRESS_STATUS_ACTIVE = 1
+IPADDRESS_STATUS_RESERVED = 2
+IPADDRESS_STATUS_DHCP = 5
 IPADDRESS_STATUS_CHOICES = (
-    (1, 'Active'),
-    (2, 'Reserved'),
-    (5, 'DHCP')
+    (IPADDRESS_STATUS_ACTIVE, 'Active'),
+    (IPADDRESS_STATUS_RESERVED, 'Reserved'),
+    (IPADDRESS_STATUS_DHCP, 'DHCP')
 )
 
+VLAN_STATUS_ACTIVE = 1
+VLAN_STATUS_RESERVED = 2
+VLAN_STATUS_DEPRECATED = 3
 VLAN_STATUS_CHOICES = (
-    (1, 'Active'),
-    (2, 'Reserved'),
-    (3, 'Deprecated')
+    (VLAN_STATUS_ACTIVE, 'Active'),
+    (VLAN_STATUS_RESERVED, 'Reserved'),
+    (VLAN_STATUS_DEPRECATED, 'Deprecated')
 )
 
 STATUS_CHOICE_CLASSES = {

+ 31 - 2
netbox/ipam/tables.py

@@ -6,6 +6,25 @@ from utilities.tables import BaseTable, ToggleColumn
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
 
+RIR_UTILIZATION = """
+<div class="progress">
+    {% if record.stats.total %}
+        <div class="progress-bar" role="progressbar" style="width: {{ record.stats.percentages.active }}%;">
+            <span class="sr-only">{{ record.stats.percentages.active }}%</span>
+        </div>
+        <div class="progress-bar progress-bar-info" role="progressbar" style="width: {{ record.stats.percentages.reserved }}%;">
+            <span class="sr-only">{{ record.stats.percentages.reserved }}%</span>
+        </div>
+        <div class="progress-bar progress-bar-danger" role="progressbar" style="width: {{ record.stats.percentages.deprecated }}%;">
+            <span class="sr-only">{{ record.stats.percentages.deprecated }}%</span>
+        </div>
+        <div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ record.stats.percentages.available }}%;">
+            <span class="sr-only">{{ record.stats.percentages.available }}%</span>
+        </div>
+    {% endif %}
+</div>
+"""
+
 RIR_ACTIONS = """
 {% if perms.ipam.change_rir %}
     <a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@@ -108,12 +127,22 @@ class RIRTable(BaseTable):
     pk = ToggleColumn()
     name = tables.LinkColumn(verbose_name='Name')
     aggregate_count = tables.Column(verbose_name='Aggregates')
-    slug = tables.Column(verbose_name='Slug')
+    stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
+                                footer=lambda table: sum(r.stats['total'] for r in table.data))
+    stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
+                                 footer=lambda table: sum(r.stats['active'] for r in table.data))
+    stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
+                                   footer=lambda table: sum(r.stats['reserved'] for r in table.data))
+    stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
+                                     footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
+    stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
+                                    footer=lambda table: sum(r.stats['available'] for r in table.data))
+    utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
     actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
 
     class Meta(BaseTable.Meta):
         model = RIR
-        fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
+        fields = ('pk', 'name', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions')
 
 
 #

+ 79 - 2
netbox/ipam/views.py

@@ -1,5 +1,6 @@
-import netaddr
+from collections import OrderedDict
 from django_tables2 import RequestConfig
+import netaddr
 
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -16,7 +17,7 @@ from utilities.views import (
 )
 
 from . import filters, forms, tables
-from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
+from .models import Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
 
 def add_available_prefixes(parent, prefix_list):
@@ -157,6 +158,82 @@ class RIRListView(ObjectListView):
     edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
     template_name = 'ipam/rir_list.html'
 
+    def alter_queryset(self, request):
+
+        if request.GET.get('family') == '6':
+            family = 6
+            denominator = 2 ** 64  # Count /64s for IPv6 rather than individual IPs
+        else:
+            family = 4
+            denominator = 1
+
+        rirs = []
+        for rir in self.queryset:
+
+            stats = {
+                'total': 0,
+                'active': 0,
+                'reserved': 0,
+                'deprecated': 0,
+                'available': 0,
+            }
+            aggregate_list = Aggregate.objects.filter(family=family, rir=rir)
+            for aggregate in aggregate_list:
+
+                queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
+
+                # Find all consumed space for each prefix status (we ignore containers for this purpose).
+                active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
+                reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
+                deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
+
+                # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
+                available_prefixes = (
+                    netaddr.IPSet([aggregate.prefix]) -
+                    netaddr.IPSet(active_prefixes) -
+                    netaddr.IPSet(reserved_prefixes) -
+                    netaddr.IPSet(deprecated_prefixes)
+                )
+
+                # Add the size of each metric to the RIR total.
+                stats['total'] += aggregate.prefix.size / denominator
+                stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
+                stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
+                stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
+                stats['available'] += available_prefixes.size / denominator
+
+            # Calculate the percentage of total space for each prefix status.
+            total = float(stats['total'])
+            stats['percentages'] = {
+                'active': float('{:.2f}'.format(stats['active'] / total * 100)) if total else 0,
+                'reserved': float('{:.2f}'.format(stats['reserved'] / total * 100)) if total else 0,
+                'deprecated': float('{:.2f}'.format(stats['deprecated'] / total * 100)) if total else 0,
+            }
+            stats['percentages']['available'] = (
+                100 -
+                stats['percentages']['active'] -
+                stats['percentages']['reserved'] -
+                stats['percentages']['deprecated']
+            )
+            rir.stats = stats
+            rirs.append(rir)
+
+        return rirs
+
+    def extra_context(self):
+
+        totals = {
+            'total': sum([rir.stats['total'] for rir in self.queryset]),
+            'active': sum([rir.stats['active'] for rir in self.queryset]),
+            'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
+            'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
+            'available': sum([rir.stats['available'] for rir in self.queryset]),
+        }
+
+        return {
+            'totals': totals,
+        }
+
 
 class RIREditView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'ipam.change_rir'

+ 3 - 0
netbox/project-static/css/base.css

@@ -85,6 +85,9 @@ label.required {
 th.pk, td.pk {
     width: 30px;
 }
+tfoot td {
+    font-weight: bold;
+}
 
 /* Paginator */
 nav ul.pagination {

+ 15 - 0
netbox/templates/ipam/rir_list.html

@@ -1,10 +1,22 @@
 {% extends '_base.html' %}
+{% load humanize %}
 {% load helpers %}
 
 {% block title %}RIRs{% endblock %}
 
 {% block content %}
 <div class="pull-right">
+    {% if request.GET.family == '6' %}
+        <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
+            <span class="fa fa-table" aria-hidden="true"></span>
+            IPv4 Stats
+        </a>
+    {% else %}
+        <a href="{% url 'ipam:rir_list' %}?family=6" class="btn btn-default">
+            <span class="fa fa-table" aria-hidden="true"></span>
+            IPv6 Stats
+        </a>
+    {% endif %}
     {% if perms.ipam.add_rir %}
         <a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
             <span class="fa fa-plus" aria-hidden="true"></span>
@@ -18,4 +30,7 @@
         {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
     </div>
 </div>
+{% if request.GET.family == '6' %}
+    <div class="pull-right text-muted"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
+{% endif %}
 {% endblock %}