From d9867423de9848a8548eeff7105c38ae33eeab1e Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 Jul 2016 22:47:08 -0700 Subject: [PATCH 01/14] Use the natsort library to provide a generic sorting option for better sorting of generic names for device bays. --- netbox/dcim/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4f0d7ec29..cb03a41d1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,4 +1,5 @@ import re +from operator import attrgetter from django.contrib import messages from django.contrib.auth.decorators import permission_required @@ -12,6 +13,8 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.http import urlencode from django.views.generic import View +from natsort import natsorted + from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit from extras.models import TopologyMap @@ -520,7 +523,10 @@ def device(request, pk): .select_related('connected_as_a', 'connected_as_b', 'circuit') mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ .select_related('connected_as_a', 'connected_as_b', 'circuit') - device_bays = DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer') + device_bays = natsorted( + DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), + key=attrgetter("name") + ) # Gather any secrets which belong to this device secrets = device.secrets.all() From 173a4cde8b51a379d157800bcf24ab73b5a56374 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 Jul 2016 23:27:00 -0700 Subject: [PATCH 02/14] Update the requirements file to include the natsort library requirement. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c1afc1b21..7b87785df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ py-gfm==0.1.3 pycrypto==2.6.1 sqlparse==0.1.19 xmltodict==0.10.2 +natsort>=5.0.0 From e81a2094df8369bc1f45a56bcb9940b179c830b2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Jul 2016 11:40:40 -0400 Subject: [PATCH 03/14] 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 a5322d911..917b2ee4d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.2.0' +VERSION = '1.2.1-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 18846cf40a83f1205fbdcc62744b97f1d2c7c8fb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Jul 2016 12:27:26 -0400 Subject: [PATCH 04/14] Fixes #271: Corrected select_related() in secrets API --- netbox/secrets/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 21a71e22f..629a28300 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -42,7 +42,7 @@ class SecretListView(generics.GenericAPIView): """ List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. """ - queryset = Secret.objects.select_related('device__primary_ip', 'role')\ + queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\ .prefetch_related('role__users', 'role__groups') serializer_class = serializers.SecretSerializer filter_class = SecretFilter @@ -87,7 +87,7 @@ class SecretDetailView(generics.GenericAPIView): """ Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret. """ - queryset = Secret.objects.select_related('device__primary_ip', 'role')\ + queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\ .prefetch_related('role__users', 'role__groups') serializer_class = serializers.SecretSerializer renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] From e1da3b8f103f9b67e071e20706a404ae2d9efd7e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Jul 2016 14:53:59 -0400 Subject: [PATCH 05/14] Related to #243: Implemented natsort on all Device and DeviceType objects (except interfaces) --- netbox/dcim/views.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 557b97019..92c8c029c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,4 +1,5 @@ import re +from natsort import natsorted from operator import attrgetter from django.contrib import messages @@ -13,8 +14,6 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.http import urlencode from django.views.generic import View -from natsort import natsorted - from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit from extras.models import TopologyMap @@ -262,13 +261,22 @@ def devicetype(request, pk): devicetype = get_object_or_404(DeviceType, pk=pk) # Component tables - consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype)) - consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects - .filter(device_type=devicetype)) - powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype)) - poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype)) + consoleport_table = tables.ConsolePortTemplateTable( + natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + consoleserverport_table = tables.ConsoleServerPortTemplateTable( + natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + powerport_table = tables.PowerPortTemplateTable( + natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + poweroutlet_table = tables.PowerOutletTemplateTable( + natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype)) - devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype)) + devicebay_table = tables.DeviceBayTemplateTable( + natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) if request.user.has_perm('dcim.change_devicetype'): consoleport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True @@ -516,17 +524,25 @@ class DeviceListView(ObjectListView): def device(request, pk): device = get_object_or_404(Device, pk=pk) - console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device') - cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console') - power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device') - power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port') + console_ports = natsorted( + ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') + ) + cs_ports = natsorted( + ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') + ) + power_ports = natsorted( + PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') + ) + power_outlets = natsorted( + PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') + ) interfaces = Interface.objects.filter(device=device, mgmt_only=False)\ .select_related('connected_as_a', 'connected_as_b', 'circuit') mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ .select_related('connected_as_a', 'connected_as_b', 'circuit') device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), - key=attrgetter("name") + key=attrgetter('name') ) # Gather any secrets which belong to this device From 2c23ca33a29c0d301f359a0913b0ae1284de29b8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Jul 2016 15:48:56 -0400 Subject: [PATCH 06/14] Fixes #274: Correct reference to old field --- netbox/dcim/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 775f046cd..09e16c348 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin): def get_queryset(self, request): qs = super(DeviceAdmin, self).get_queryset(request) - return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack') + return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack') From 8e7e02a6227f5e83ba81056ab644daa7884cae59 Mon Sep 17 00:00:00 2001 From: lukerussell Date: Wed, 13 Jul 2016 06:04:29 +1000 Subject: [PATCH 07/14] Added a link to docs in readme.md Adding a direct link for easy access. I couldn't find a link anywhere except digging in through the docs/ directory. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 00343a855..419f27b02 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ NetBox is an IP address management (IPAM) and data center infrastructure managem NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox). +The complete documentation for Netbox can be found at [Read the Docs](http://netbox.readthedocs.io/en/latest/). + Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net**! ### Build Status From b60f964835c4abda001e313ea846bd47de3f85f3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Jul 2016 16:57:00 -0400 Subject: [PATCH 08/14] Fixes #272: Added a step to copy the gunicorn config --- docs/installation/upgrading.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 55353b77a..087d9f198 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -4,42 +4,40 @@ As with the initial installation, you can upgrade NetBox by either downloading t ## Option A: Download a Release -Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. For this guide we are using 1.0.4 as the old version and 1.0.7 as the new version. +Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`. + +Download and extract the latest version: -Download & extract latest version: ``` # wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz # tar -xzf vX.Y.Z.tar.gz -C /opt # cd /opt/ -# ln -sf netbox-1.0.7/ netbox +# ln -sf netbox-X.Y.Z/ netbox ``` Copy the 'configuration.py' you created when first installing to the new version: + ``` -# cp /opt/netbox-1.0.4/configuration.py /opt/netbox/configuration.py +# cp /opt/netbox-X.Y.Z/configuration.py /opt/netbox/configuration.py +``` + +If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: + +``` +# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py ``` ## Option B: Clone the Git Repository (latest master release) -For this guide, we'll use `/opt/netbox`. +This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch: -Check that your git branch is up to date & is set to master: ``` # cd /opt/netbox -# git status -``` - -If not on branch master, set it and verify status: -``` # git checkout master +# git pull origin master # git status ``` -Pull down the set branch from git status above: -``` -# git pull -``` - # Run the Upgrade Script Once the new code is in place, run the upgrade script (which may need to be run as root depending on how your environment is configured). From 03542b400d885397b995c8bfe05e8c38ec39da96 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 09:25:13 -0400 Subject: [PATCH 09/14] Renamed IPAMConfig to DCIMConfig --- netbox/dcim/__init__.py | 2 +- netbox/dcim/apps.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/__init__.py b/netbox/dcim/__init__.py index 886472672..1f3214979 100644 --- a/netbox/dcim/__init__.py +++ b/netbox/dcim/__init__.py @@ -1 +1 @@ -default_app_config = 'dcim.apps.IPAMConfig' +default_app_config = 'dcim.apps.DCIMConfig' diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index 8dfb6e3e6..fdfcc1f57 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class IPAMConfig(AppConfig): +class DCIMConfig(AppConfig): name = "dcim" verbose_name = "DCIM" From e19ce043d6409f90b93b3ac710484fcb530c700d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 10:37:25 -0400 Subject: [PATCH 10/14] Fixes #275: Exclude self when checking for overlapping aggregates --- netbox/ipam/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 402e02330..e37e21bb3 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -123,6 +123,8 @@ class Aggregate(CreatedUpdatedModel): # Ensure that the aggregate being added does not cover an existing aggregate covered_aggregates = Aggregate.objects.filter(prefix__net_contained=str(self.prefix)) + if self.pk: + covered_aggregates = covered_aggregates.exclude(pk=self.pk) if covered_aggregates: raise ValidationError("{} is overlaps with an existing aggregate ({})" .format(self.prefix, covered_aggregates[0])) From f1b6f0cfee97b13e3f8a28a56f28cf3dae23b1c9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 11:15:37 -0400 Subject: [PATCH 11/14] Fixes #285: Added PREFER_IPV4 configuration setting --- docs/configuration/optional-settings.md | 8 ++++++++ netbox/dcim/models.py | 5 ++++- netbox/netbox/configuration.example.py | 4 ++++ netbox/netbox/settings.py | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 0aa59a196..6258a16fd 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -66,6 +66,14 @@ Determine how many objects to display per page within each list of objects. --- +## PREFER_IPV4 + +Default: False + +When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead. + +--- + ## TIME_ZONE Default: UTC diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index df07fa748..c51714a71 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from django.conf import settings from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core.validators import MinValueValidator @@ -713,7 +714,9 @@ class Device(CreatedUpdatedModel): @property def primary_ip(self): - if self.primary_ip6: + if settings.PREFER_IPV4 and self.primary_ip4: + return self.primary_ip4 + elif self.primary_ip6: return self.primary_ip6 elif self.primary_ip4: return self.primary_ip4 diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index aba0eb3f5..745bdfaaf 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -78,3 +78,7 @@ SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. BANNER_TOP = '' BANNER_BOTTOM = '' + +# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to +# prefer IPv4 instead. +PREFER_IPV4 = False diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 917b2ee4d..ac0eb6836 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -40,6 +40,7 @@ DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') BANNER_TOP = getattr(configuration, 'BANNER_TOP', False) BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False) +PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # Attempt to import LDAP configuration if it has been defined From 96b496ffa80dba032d47fd7e6f36bccca33bd56c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 11:24:34 -0400 Subject: [PATCH 12/14] Updated documentation to include banner settings --- docs/configuration/optional-settings.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 6258a16fd..a9f42394d 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -13,11 +13,24 @@ ADMINS = [ --- +## BANNER_TOP + +## BANNER_BOTTOM + +Setting these variables will display content in a banner at the top and/or bottom of the page, respectively. To replicate the content of the top banner in the bottom banner, set: + +``` +BANNER_TOP = 'Your banner text' +BANNER_BOTTOM = BANNER_TOP +``` + +--- + ## DEBUG Default: False -This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users. +This setting enables debugging. This should be done only during development or troubleshooting. Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users. --- From 6f1ed9fc16f62789672921bcfa5a73d75200fba7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 11:47:20 -0400 Subject: [PATCH 13/14] Clarified the process of voting on feature requests --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec5b48801..58de11608 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,9 +27,10 @@ IRC. ## Feature Requests -* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you'd like to see -has already been requested (and possibly rejected). If it is, be sure to comment with a "+1" and any additional -justification you have for the feature. +* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting +has already been requested (and possibly rejected). If it has, click "add a reaction" in the top right corner of the +issue and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel +free to add a comment with any additional justification for the feature. * While discussion of new features is welcome, it's important to limit the scope of NetBox's feature set to avoid feature creep. For example, the following features would be firmly out of scope for NetBox: From d9bf199e757cb8a14b4dadffd0cf9216908ac058 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Jul 2016 12:01:34 -0400 Subject: [PATCH 14/14] Version bump: v1.2.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 ac0eb6836..13bc4bda5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.2.1-dev' +VERSION = '1.2.1' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: