Browse Source

#667: Initial work on RIR statistics

Jeremy Stretch 8 years ago
parent
commit
13cdc44caf
4 changed files with 188 additions and 12 deletions
  1. 20 10
      netbox/ipam/models.py
  2. 2 0
      netbox/ipam/urls.py
  3. 76 2
      netbox/ipam/views.py
  4. 90 0
      netbox/templates/ipam/stats.html

+ 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 = {

+ 2 - 0
netbox/ipam/urls.py

@@ -19,6 +19,8 @@ urlpatterns = [
     url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
     url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
     url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
+    url(r'^rirs/stats/$', views.rir_stats, name='rir_stats'),
+    url(r'^rirs/stats/ipv6/$', views.rir_stats, kwargs={'family': 6}, name='rir_stats_ipv6'),
     url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
 
     # Aggregates

+ 76 - 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):
@@ -655,3 +656,76 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'ipam.delete_vlan'
     cls = VLAN
     default_redirect_url = 'ipam:vlan_list'
+
+
+#
+# Miscellaneous
+#
+
+def rir_stats(request, family=4):
+
+    denominator = 2 ** 64 if family == 6 else 1
+
+    stats = OrderedDict()
+    for rir in RIR.objects.all():
+
+        stats[rir] = {
+            '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[rir]['total'] += aggregate.prefix.size / denominator
+            stats[rir]['active'] += netaddr.IPSet(active_prefixes).size / denominator
+            stats[rir]['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
+            stats[rir]['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
+            stats[rir]['available'] += available_prefixes.size / denominator
+
+        # Calculate the percentage of total space for each prefix status.
+        total = float(stats[rir]['total'])
+        stats[rir]['percentages'] = {
+            'active': float('{:.2f}'.format(stats[rir]['active'] / total * 100)) if total else 0,
+            'reserved': float('{:.2f}'.format(stats[rir]['reserved'] / total * 100)) if total else 0,
+            'deprecated': float('{:.2f}'.format(stats[rir]['deprecated'] / total * 100)) if total else 0,
+            'available': float('{:.2f}'.format(stats[rir]['available'] / total * 100)) if total else 0,
+        }
+        stats[rir]['percentages']['available'] = (
+            100
+            - stats[rir]['percentages']['active']
+            - stats[rir]['percentages']['reserved']
+            - stats[rir]['percentages']['deprecated']
+        )
+
+    totals = {
+        'total': sum([counts['total'] for rir, counts in stats.items()]),
+        'active': sum([counts['active'] for rir, counts in stats.items()]),
+        'reserved': sum([counts['reserved'] for rir, counts in stats.items()]),
+        'deprecated': sum([counts['deprecated'] for rir, counts in stats.items()]),
+        'available': sum([counts['available'] for rir, counts in stats.items()]),
+    }
+
+    return render(request, 'ipam/stats.html', {
+        'stats': stats,
+        'totals': totals,
+        'family': family,
+    })

+ 90 - 0
netbox/templates/ipam/stats.html

@@ -0,0 +1,90 @@
+{% extends '_base.html' %}
+{% load humanize %}
+{% load render_table from django_tables2 %}
+
+{% block title %}RIR Statistics{% endblock %}
+
+{% block content %}
+<h1>RIR Statistics</h1>
+<div class="row">
+    <div class="col-md-9">
+        <ul class="nav nav-tabs" style="margin-bottom: 20px">
+            <li role="presentation"{% if family == 4 %} class="active"{% endif %}><a href="{% url 'ipam:rir_stats' %}">IPv4</a></li>
+            <li role="presentation"{% if family == 6 %} class="active"{% endif %}><a href="{% url 'ipam:rir_stats_ipv6' %}">IPv6</a></li>
+        </ul>
+        {% if family == 6 %}
+            <div class="alert alert-info alert-dismissible" role="alert">
+                <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <strong>Note:</strong> Numbers shown here indicate equivalent /64 prefixes, not individual IP addresses.
+            </div>
+        {% endif %}
+        {% for rir, counts in stats.items %}
+            <h3>{{ rir }}</h3>
+            <div class="progress">
+                {% if counts.total %}
+                    <div class="progress-bar" role="progressbar" style="width: {{ counts.percentages.active }}%;">
+                        <span class="sr-only">{{ counts.percentages.active }}%</span>
+                    </div>
+                    <div class="progress-bar progress-bar-info" role="progressbar" style="width: {{ counts.percentages.reserved }}%;">
+                        <span class="sr-only">{{ counts.percentages.reserved }}%</span>
+                    </div>
+                    <div class="progress-bar progress-bar-danger" role="progressbar" style="width: {{ counts.percentages.deprecated }}%;">
+                        <span class="sr-only">{{ counts.percentages.deprecated }}%</span>
+                    </div>
+                    <div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ counts.percentages.available }}%;">
+                        <span class="sr-only">{{ counts.percentages.available }}%</span>
+                    </div>
+                {% endif %}
+            </div>
+            <div class="row">
+                <div class="col-md-2 col-md-offset-2 text-center">
+                    <h4><span class="label label-default">{{ counts.total|intcomma }}</span></h4>
+                    Total
+                </div>
+                <div class="col-md-2 text-center">
+                    <h4><span class="label label-primary">{{ counts.active|intcomma }}</span></h4>
+                    Active
+                </div>
+                <div class="col-md-2 text-center">
+                    <h4><span class="label label-info">{{ counts.reserved|intcomma }}</span></h4>
+                    Reserved
+                </div>
+                <div class="col-md-2 text-center">
+                    <h4><span class="label label-danger">{{ counts.deprecated|intcomma }}</span></h4>
+                    Deprecated
+                </div>
+                <div class="col-md-2 text-center">
+                    <h4><span class="label label-success">{{ counts.available|intcomma }}</span></h4>
+                    Available
+                </div>
+            </div>
+        {% endfor %}
+        <hr />
+        <div class="row">
+            <div class="col-md-2">
+                <h3>Totals</h3>
+            </div>
+            <div class="col-md-2 text-center">
+                <h3>{{ totals.total|intcomma }}</h3>
+                All IPv{{ family }} space
+            </div>
+            <div class="col-md-2 text-center">
+                <h3>{{ totals.active|intcomma }}</h3>
+                Active
+            </div>
+            <div class="col-md-2 text-center">
+                <h3>{{ totals.reserved|intcomma }}</h3>
+                Reserved
+            </div>
+            <div class="col-md-2 text-center">
+                <h3>{{ totals.deprecated|intcomma }}</h3>
+                Deprecated
+            </div>
+            <div class="col-md-2 text-center">
+                <h3>{{ totals.available|intcomma }}</h3>
+                Available
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}