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: 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 diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 0aa59a196..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. --- @@ -66,6 +79,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/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). 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/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') 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" 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/dcim/views.py b/netbox/dcim/views.py index e2b1d5ac9..92c8c029c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,4 +1,6 @@ import re +from natsort import natsorted +from operator import attrgetter from django.contrib import messages from django.contrib.auth.decorators import permission_required @@ -259,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 @@ -513,15 +524,26 @@ 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 = 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() 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])) 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 a5322d911..13bc4bda5 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' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: @@ -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 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] 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