From 753c6ebd20332314995107d3c24dd4b21b303b31 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 93d4671e48adb4dca19a52d57bcde8a52d3b78b4 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 d150866952732221eecdde7bb7768791a0a2d64c 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 977578ca2886cb99fe7b5942e74dd77d78846a9d 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 c862487929893a74cadc1cc0e3658a0ed817029b 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 1cffda312cf52643fcfd23812c0434ce2200a3ef 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 2e8a128b6e920ad906918f88aac7cd0488cbc67e 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 3cfd60e290e2bdc30c30114bb30859948cc41bb9 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 b27e74c03d49b3cbf2b30741093e300ca443a3a6 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 0093d614271a501059f27e27b0adcb135d00de15 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 43e4446e7559411cef66563edadc3a23bb0e95d1 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 222bbf89cb728310a0155d981738236eeaf88f3e 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 ccee310eb0fdb8ab95e7b00814891f974f44458e 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 ae9d7e4bacd056d324c9ba15e11ced4b01bc8b41 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']: