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' %} + + + IPv4 Stats + + {% else %} + + + IPv6 Stats + + {% endif %} {% if perms.ipam.add_rir %} @@ -18,4 +30,7 @@ {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
+{% if request.GET.family == '6' %} +
Note: Numbers shown indicate /64 prefixes.
+{% endif %} {% endblock %}