Просмотр исходного кода

Merge pull request #286 from digitalocean/develop

Release v1.2.1
Jeremy Stretch 9 лет назад
Родитель
Сommit
300aff71bb

+ 4 - 3
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:

+ 2 - 0
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

+ 22 - 1
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

+ 14 - 16
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
 ```
 
-## Option B: Clone the Git Repository (latest master release)
-
-For this guide, we'll use `/opt/netbox`.
+If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
 
-Check that your git branch is up to date & is set to master:
 ```
-# cd /opt/netbox
-# git status
+# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
 ```
 
-If not on branch master, set it and verify status:
+## Option B: Clone the Git Repository (latest master release)
+
+This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
+
 ```
+# cd /opt/netbox
 # 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).

+ 1 - 1
netbox/dcim/__init__.py

@@ -1 +1 @@
-default_app_config = 'dcim.apps.IPAMConfig'
+default_app_config = 'dcim.apps.DCIMConfig'

+ 1 - 1
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')

+ 1 - 1
netbox/dcim/apps.py

@@ -1,6 +1,6 @@
 from django.apps import AppConfig
 
 
-class IPAMConfig(AppConfig):
+class DCIMConfig(AppConfig):
     name = "dcim"
     verbose_name = "DCIM"

+ 4 - 1
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

+ 33 - 11
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()

+ 2 - 0
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]))

+ 4 - 0
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

+ 2 - 1
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

+ 2 - 2
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]

+ 1 - 0
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