From 1f3f9781d9ed23917f98821dcc07ff2418910990 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 3 Nov 2016 15:13:15 -0400 Subject: [PATCH 01/15] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 27195c6d3..ce4eeffb7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.7.0' +VERSION = '1.7.1-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 13cdc44cafcf59954fab1b2be407764290f44e2b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 4 Nov 2016 16:04:29 -0400 Subject: [PATCH 02/15] #667: Initial work on RIR statistics --- netbox/ipam/models.py | 30 +++++++---- netbox/ipam/urls.py | 2 + netbox/ipam/views.py | 78 ++++++++++++++++++++++++++- netbox/templates/ipam/stats.html | 90 ++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 netbox/templates/ipam/stats.html 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/urls.py b/netbox/ipam/urls.py index dc5fcc964..71017be26 100644 --- a/netbox/ipam/urls.py +++ b/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[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), # Aggregates diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3262bbeb5..6beb22f3e 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): @@ -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, + }) diff --git a/netbox/templates/ipam/stats.html b/netbox/templates/ipam/stats.html new file mode 100644 index 000000000..e4056e224 --- /dev/null +++ b/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 %} +

RIR Statistics

+
+
+ + {% if family == 6 %} + + {% endif %} + {% for rir, counts in stats.items %} +

{{ rir }}

+
+ {% if counts.total %} +
+ {{ counts.percentages.active }}% +
+
+ {{ counts.percentages.reserved }}% +
+
+ {{ counts.percentages.deprecated }}% +
+
+ {{ counts.percentages.available }}% +
+ {% endif %} +
+
+
+

{{ counts.total|intcomma }}

+ Total +
+
+

{{ counts.active|intcomma }}

+ Active +
+
+

{{ counts.reserved|intcomma }}

+ Reserved +
+
+

{{ counts.deprecated|intcomma }}

+ Deprecated +
+
+

{{ counts.available|intcomma }}

+ Available +
+
+ {% endfor %} +
+
+
+

Totals

+
+
+

{{ totals.total|intcomma }}

+ All IPv{{ family }} space +
+
+

{{ totals.active|intcomma }}

+ Active +
+
+

{{ totals.reserved|intcomma }}

+ Reserved +
+
+

{{ totals.deprecated|intcomma }}

+ Deprecated +
+
+

{{ totals.available|intcomma }}

+ Available +
+
+
+
+{% endblock %} From e496dc710fd5ca3794e07cff530fe75a8fd5b436 Mon Sep 17 00:00:00 2001 From: Bruno Emanuel Date: Fri, 4 Nov 2016 18:01:07 -0300 Subject: [PATCH 03/15] Add Graphviz to Topology Maps --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 63562e2ea..c6dff9a86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /opt/netbox ARG BRANCH=master ARG URL=https://github.com/digitalocean/netbox.git RUN git clone --depth 1 $URL -b $BRANCH . && \ - apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev && \ + apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz python-pygraphviz && \ pip install gunicorn==17.5 && \ pip install django-auth-ldap && \ pip install -r requirements.txt From 9c7f55d8d07c10dda44d84ec7912ed837c1e6725 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 10 Nov 2016 15:01:05 -0500 Subject: [PATCH 04/15] Fixes #674: Correct status assignment on IP address import --- netbox/ipam/forms.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 958a99a3f..1a9e25db3 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -220,12 +220,11 @@ class PrefixFromCSVForm(forms.ModelForm): self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.") def save(self, *args, **kwargs): - m = super(PrefixFromCSVForm, self).save(commit=False) + # Assign Prefix status by name - m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] - if kwargs.get('commit'): - m.save() - return m + self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] + + return super(PrefixFromCSVForm, self).save(*args, **kwargs) class PrefixImportForm(BulkImportForm, BootstrapMixin): @@ -391,7 +390,10 @@ class IPAddressFromCSVForm(forms.ModelForm): if is_primary and not device: self.add_error('is_primary', "No device specified; cannot set as primary IP") - def save(self, commit=True): + def save(self, *args, **kwargs): + + # Assign status by name + self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']] # Set interface if self.cleaned_data['device'] and self.cleaned_data['interface_name']: @@ -404,7 +406,7 @@ class IPAddressFromCSVForm(forms.ModelForm): elif self.instance.address.version == 6: self.instance.primary_ip6_for = self.cleaned_data['device'] - return super(IPAddressFromCSVForm, self).save(commit=commit) + return super(IPAddressFromCSVForm, self).save(*args, **kwargs) class IPAddressImportForm(BulkImportForm, BootstrapMixin): From 7dfd32a5c4730738e2c2e2036ed9bca3ec0de096 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 10 Nov 2016 15:15:55 -0500 Subject: [PATCH 05/15] Fixes #676: Server error when bulk editing device types --- netbox/dcim/forms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6e95803f4..b57e31d1e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -268,6 +268,9 @@ class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin): manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) u_height = forms.IntegerField(min_value=1, required=False) + class Meta: + nullable_fields = [] + class DeviceTypeFilterForm(forms.Form, BootstrapMixin): manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')), From 07e34fbe842cf26ab5b6f425361d5267d388998d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 10 Nov 2016 15:30:45 -0500 Subject: [PATCH 06/15] Fixes #678: Server error on device import specifying an invalid device type --- netbox/dcim/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index b20f22940..93cdb95d5 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -852,8 +852,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): 'face': "Must specify rack face when defining rack position." }) - if self.device_type: - + try: # Child devices cannot be assigned to a rack face/unit if self.device_type.is_child_device and self.face is not None: raise ValidationError({ @@ -880,6 +879,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel): except Rack.DoesNotExist: pass + except DeviceType.DoesNotExist: + pass + def save(self, *args, **kwargs): is_new = not bool(self.pk) From d891c8c98121604fd7dffe8aafd996a8b9bc086d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Nov 2016 12:45:24 -0500 Subject: [PATCH 07/15] Incorporated stats into RIR list view --- netbox/ipam/tables.py | 28 +++++- netbox/ipam/urls.py | 2 - netbox/ipam/views.py | 146 ++++++++++++++-------------- netbox/templates/ipam/rir_list.html | 26 +++++ netbox/templates/ipam/stats.html | 90 ----------------- 5 files changed, 125 insertions(+), 167 deletions(-) delete mode 100644 netbox/templates/ipam/stats.html diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 6859472a6..922ce6a5c 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,17 @@ 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') + stats_active = tables.Column(accessor='stats.active', verbose_name='Active') + stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved') + stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated') + stats_available = tables.Column(accessor='stats.available', verbose_name='Available') + 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/urls.py b/netbox/ipam/urls.py index 71017be26..dc5fcc964 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -19,8 +19,6 @@ 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[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), # Aggregates diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6beb22f3e..f804e565e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -158,6 +158,79 @@ class RIRListView(ObjectListView): edit_permissions = ['ipam.change_rir', 'ipam.delete_rir'] template_name = 'ipam/rir_list.html' + def alter_queryset(self, request): + + # Count /64s for IPv6 rather than individual IPs + family = 4 + denominator = 2 ** 64 if family == 6 else 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' @@ -656,76 +729,3 @@ 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, - }) diff --git a/netbox/templates/ipam/rir_list.html b/netbox/templates/ipam/rir_list.html index 51d63f4c2..f0b38ac7f 100644 --- a/netbox/templates/ipam/rir_list.html +++ b/netbox/templates/ipam/rir_list.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load humanize %} {% load helpers %} {% block title %}RIRs{% endblock %} @@ -18,4 +19,29 @@ {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %} +
+
+

Totals

+
+
+

{{ totals.total|intcomma }}

+ All IPv4 space +
+
+

{{ totals.active|intcomma }}

+ Active +
+
+

{{ totals.reserved|intcomma }}

+ Reserved +
+
+

{{ totals.deprecated|intcomma }}

+ Deprecated +
+
+

{{ totals.available|intcomma }}

+ Available +
+
{% endblock %} diff --git a/netbox/templates/ipam/stats.html b/netbox/templates/ipam/stats.html deleted file mode 100644 index e4056e224..000000000 --- a/netbox/templates/ipam/stats.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends '_base.html' %} -{% load humanize %} -{% load render_table from django_tables2 %} - -{% block title %}RIR Statistics{% endblock %} - -{% block content %} -

RIR Statistics

-
-
- - {% if family == 6 %} - - {% endif %} - {% for rir, counts in stats.items %} -

{{ rir }}

-
- {% if counts.total %} -
- {{ counts.percentages.active }}% -
-
- {{ counts.percentages.reserved }}% -
-
- {{ counts.percentages.deprecated }}% -
-
- {{ counts.percentages.available }}% -
- {% endif %} -
-
-
-

{{ counts.total|intcomma }}

- Total -
-
-

{{ counts.active|intcomma }}

- Active -
-
-

{{ counts.reserved|intcomma }}

- Reserved -
-
-

{{ counts.deprecated|intcomma }}

- Deprecated -
-
-

{{ counts.available|intcomma }}

- Available -
-
- {% endfor %} -
-
-
-

Totals

-
-
-

{{ totals.total|intcomma }}

- All IPv{{ family }} space -
-
-

{{ totals.active|intcomma }}

- Active -
-
-

{{ totals.reserved|intcomma }}

- Reserved -
-
-

{{ totals.deprecated|intcomma }}

- Deprecated -
-
-

{{ totals.available|intcomma }}

- Available -
-
-
-
-{% endblock %} From a0ee6b0d58cbc986702e81f44e1ff8f78a37ab3b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Nov 2016 15:02:53 -0500 Subject: [PATCH 08/15] Closes #667: Added stats to RIR list view --- netbox/ipam/tables.py | 15 +++++++---- netbox/ipam/views.py | 9 ++++--- netbox/project-static/css/base.css | 3 +++ netbox/templates/ipam/rir_list.html | 39 +++++++++++------------------ 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 922ce6a5c..1877c82fd 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -127,11 +127,16 @@ class RIRTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn(verbose_name='Name') aggregate_count = tables.Column(verbose_name='Aggregates') - stats_total = tables.Column(accessor='stats.total', verbose_name='Total') - stats_active = tables.Column(accessor='stats.active', verbose_name='Active') - stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved') - stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated') - stats_available = tables.Column(accessor='stats.available', verbose_name='Available') + 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='') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f804e565e..930c2de56 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -160,9 +160,12 @@ class RIRListView(ObjectListView): def alter_queryset(self, request): - # Count /64s for IPv6 rather than individual IPs - family = 4 - denominator = 2 ** 64 if family == 6 else 1 + 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: 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 f0b38ac7f..756e91d7f 100644 --- a/netbox/templates/ipam/rir_list.html +++ b/netbox/templates/ipam/rir_list.html @@ -6,6 +6,17 @@ {% block content %}
+ {% if request.GET.family == '6' %} + + + IPv4 Stats + + {% else %} + + + IPv6 Stats + + {% endif %} {% if perms.ipam.add_rir %} @@ -19,29 +30,7 @@ {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
-
-
-

Totals

-
-
-

{{ totals.total|intcomma }}

- All IPv4 space -
-
-

{{ totals.active|intcomma }}

- Active -
-
-

{{ totals.reserved|intcomma }}

- Reserved -
-
-

{{ totals.deprecated|intcomma }}

- Deprecated -
-
-

{{ totals.available|intcomma }}

- Available -
-
+{% if request.GET.family == '6' %} +
Note: Numbers shown indicate /64 prefixes.
+{% endif %} {% endblock %} From 99c2911a6699863f4a54318c61f552ba0b9e1a05 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Nov 2016 15:04:14 -0500 Subject: [PATCH 09/15] PEP8 fix --- netbox/ipam/tables.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 1877c82fd..f58dc6673 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -130,13 +130,13 @@ class RIRTable(BaseTable): 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)) + 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)) + 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)) + 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)) + 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='') From f5695619971e3ee8a483e79b99d551d479bb5aae Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Nov 2016 15:09:25 -0500 Subject: [PATCH 10/15] Another PEP8 fix --- netbox/ipam/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 930c2de56..78bb1c148 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -189,10 +189,10 @@ class RIRListView(ObjectListView): # 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) + 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. @@ -210,10 +210,10 @@ class RIRListView(ObjectListView): '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'] + 100 - + stats['percentages']['active'] - + stats['percentages']['reserved'] - + stats['percentages']['deprecated'] ) rir.stats = stats rirs.append(rir) From 18a516ee5377d0637529fb664620b87d8b5fc576 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Nov 2016 15:29:40 -0500 Subject: [PATCH 11/15] Closes #685: When assigning an IP to a device, automaitcally select the interface if only one exists --- netbox/dcim/forms.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b57e31d1e..44e30e964 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1252,10 +1252,15 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' - self.fields['interface'].queryset = device.interfaces.all() + interfaces = device.interfaces.all() + self.fields['interface'].queryset = interfaces self.fields['interface'].required = True - # If this device does not have any IP addresses assigned, default to setting the first IP as its primary + # If this device has only one interface, select it by default. + if len(interfaces) == 1: + self.fields['interface'].initial = interfaces[0] + + # If this device does not have any IP addresses assigned, default to setting the first IP as its primary. if not IPAddress.objects.filter(interface__device=device).count(): self.fields['set_as_primary'].initial = True From 8e71c0f2a86be6efa01fc7059503a3da7799031f Mon Sep 17 00:00:00 2001 From: Bruno Emanuel Date: Sun, 13 Nov 2016 13:29:11 -0300 Subject: [PATCH 12/15] Removed python-graphviz --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c6dff9a86..dc28bb20b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /opt/netbox ARG BRANCH=master ARG URL=https://github.com/digitalocean/netbox.git RUN git clone --depth 1 $URL -b $BRANCH . && \ - apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz python-pygraphviz && \ + apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev graphviz && \ pip install gunicorn==17.5 && \ pip install django-auth-ldap && \ pip install -r requirements.txt From 9eaf153673a2b6699fff56a871fa9f67d103defd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Nov 2016 11:13:27 -0500 Subject: [PATCH 13/15] Fixes #692: Form errors are not displayed on checkbox fields --- netbox/templates/utilities/render_field.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/netbox/templates/utilities/render_field.html b/netbox/templates/utilities/render_field.html index 09a91f752..21b5b905a 100644 --- a/netbox/templates/utilities/render_field.html +++ b/netbox/templates/utilities/render_field.html @@ -16,6 +16,13 @@ Set null {% endif %} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} {% elif field|widget_type == 'textarea' %}
From 955abcef218d13f72f83fba49a1045896bc80312 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Nov 2016 11:29:03 -0500 Subject: [PATCH 14/15] Fixes #691: Allow the assignment of power ports to PDUs --- netbox/templates/dcim/device.html | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 62683930b..14d5b0869 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -237,16 +237,14 @@ {% for pp in power_ports %} {% include 'dcim/inc/_powerport.html' %} {% empty %} - {% if not device.device_type.is_pdu %} - - - No power ports defined - {% if perms.dcim.add_powerport %} - - {% endif %} - - - {% endif %} + + + No power ports defined + {% if perms.dcim.add_powerport %} + + {% endif %} + + {% endfor %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} @@ -261,7 +259,7 @@ Add console port {% endif %} - {% if perms.dcim.add_powerport and not device.device_type.is_pdu %} + {% if perms.dcim.add_powerport %} Add power port From 1d509a8ff86bb99277dc965d451080ad4146980c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Nov 2016 12:13:42 -0500 Subject: [PATCH 15/15] Release v1.7.1 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ce4eeffb7..ccc94488b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.7.1-dev' +VERSION = '1.7.1' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: