diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
index 163712d1e..1538251cf 100644
--- a/netbox/ipam/models.py
+++ b/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 = {
diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index 6859472a6..f58dc6673 100644
--- a/netbox/ipam/tables.py
+++ b/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 = """
+
+ {% if record.stats.total %}
+
+ {{ record.stats.percentages.active }}%
+
+
+ {{ record.stats.percentages.reserved }}%
+
+
+ {{ record.stats.percentages.deprecated }}%
+
+
+ {{ record.stats.percentages.available }}%
+
+ {% endif %}
+
+"""
+
RIR_ACTIONS = """
{% if perms.ipam.change_rir %}
@@ -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')
#
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 3262bbeb5..78bb1c148 100644
--- a/netbox/ipam/views.py
+++ b/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'
diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css
index 635745309..635aa7e94 100644
--- a/netbox/project-static/css/base.css
+++ b/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 {
diff --git a/netbox/templates/ipam/rir_list.html b/netbox/templates/ipam/rir_list.html
index 51d63f4c2..756e91d7f 100644
--- a/netbox/templates/ipam/rir_list.html
+++ b/netbox/templates/ipam/rir_list.html
@@ -1,10 +1,22 @@
{% extends '_base.html' %}
+{% load humanize %}
{% load helpers %}
{% block title %}RIRs{% endblock %}
{% block content %}
+{% if request.GET.family == '6' %}
+ Note: Numbers shown indicate /64 prefixes.
+{% endif %}
{% endblock %}