Sfoglia il codice sorgente

Merge pull request #3387 from netbox-community/develop

Release v2.6.2
Jeremy Stretch 6 anni fa
parent
commit
7fc60cd667

File diff suppressed because it is too large
+ 356 - 331
CHANGELOG.md


+ 3 - 3
CONTRIBUTING.md

@@ -16,11 +16,11 @@ For real-time discussion, you can join the #netbox Slack channel on [NetworkToCo
 
 ## Reporting Bugs
 
-* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
+* First, ensure that you've installed the [latest stable version](https://github.com/netbox-community/netbox/releases)
 of NetBox. If you're running an older version, it's possible that the bug has
 already been fixed.
 
-* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
+* Next, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
 to see if the bug you've found has already been reported. If you think you may
 be experiencing a reported issue that hasn't already been resolved, please
 click "add a reaction" in the top right corner of the issue and add a thumbs
@@ -51,7 +51,7 @@ your issue.
 
 ## Feature Requests
 
-* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
+* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
 to see if the feature you're requesting is already listed. (Be sure to search
 closed issues as well, since some feature requests have been rejected.) If the
 feature you'd like to see has already been requested and is open, click "add a

+ 1 - 0
NOTICE

@@ -0,0 +1 @@
+Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.

+ 2 - 2
README.md

@@ -7,7 +7,7 @@ to address the needs of network and infrastructure engineers.
 
 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).
+complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
 
 The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
 
@@ -32,7 +32,7 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
 # Installation
 
 Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
-instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases)
+instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
 and run `upgrade.sh`.
 
 ## Alternative Installations

+ 1 - 1
docs/core-functionality/devices.md

@@ -95,7 +95,7 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
 
 ### Device Bays
 
-Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
+Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or the "Non-Racked Devices" list within the rack view.
 
 Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent.  Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces.  You cannot create a LAG between interfaces in different child devices.
 

+ 2 - 2
docs/development/index.md

@@ -1,12 +1,12 @@
 # NetBox Development
 
-NetBox is maintained as a [GitHub project](https://github.com/digitalocean/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
+NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
 
 ## Communication
 
 Communication among developers should always occur via public channels:
 
-* [GitHub issues](https://github.com/digitalocean/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
+* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
 * [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
 * [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
 

+ 1 - 1
docs/development/release-checklist.md

@@ -61,7 +61,7 @@ Once CI has completed on the PR, merge it.
 
 ## Create a New Release
 
-Draft a [new release](https://github.com/digitalocean/netbox/releases/new) with the following parameters.
+Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters.
 
 * **Tag:** Current version (e.g. `v2.3.4`)
 * **Target:** `master`

+ 3 - 3
docs/installation/2-netbox.md

@@ -21,10 +21,10 @@ You may opt to install NetBox either from a numbered release or by cloning the m
 
 ## Option A: Download a Release
 
-Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
+Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
 
 ```no-highlight
-# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
+# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
 # tar -xzf vX.Y.Z.tar.gz -C /opt
 # cd /opt/
 # ln -s netbox-X.Y.Z/ netbox
@@ -56,7 +56,7 @@ If `git` is not already installed, install it:
 Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
 
 ```no-highlight
-# git clone -b master https://github.com/digitalocean/netbox.git .
+# git clone -b master https://github.com/netbox-community/netbox.git .
 Cloning into '.'...
 remote: Counting objects: 1994, done.
 remote: Compressing objects: 100% (150/150), done.

+ 2 - 2
docs/installation/upgrading.md

@@ -4,12 +4,12 @@ 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`.
+Download the [latest stable release](https://github.com/netbox-community/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:
 
 ```no-highlight
-# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
+# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
 # tar -xzf vX.Y.Z.tar.gz -C /opt
 # cd /opt/
 # ln -sfn netbox-X.Y.Z/ netbox

+ 1 - 1
mkdocs.yml

@@ -1,6 +1,6 @@
 site_name: NetBox
 theme: readthedocs
-repo_url: https://github.com/digitalocean/netbox
+repo_url: https://github.com/netbox-community/netbox
 
 pages:
     - Introduction: 'index.md'

+ 7 - 12
netbox/circuits/tables.py

@@ -20,15 +20,6 @@ STATUS_LABEL = """
 """
 
 
-class CircuitTerminationColumn(tables.Column):
-
-    def render(self, value):
-        return mark_safe('<a href="{}">{}</a>'.format(
-            value.site.get_absolute_url(),
-            value.site
-        ))
-
-
 #
 # Providers
 #
@@ -77,9 +68,13 @@ class CircuitTable(BaseTable):
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     tenant = tables.TemplateColumn(template_code=COL_TENANT)
-    termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
-    termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
+    a_side = tables.Column(
+        verbose_name='A Side'
+    )
+    z_side = tables.Column(
+        verbose_name='Z Side'
+    )
 
     class Meta(BaseTable.Meta):
         model = Circuit
-        fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')
+        fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')

+ 5 - 3
netbox/circuits/views.py

@@ -1,11 +1,9 @@
-from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
-from django.db.models import Count
+from django.db.models import Count, OuterRef, Subquery
 from django.shortcuts import get_object_or_404, redirect, render
-from django.utils.decorators import method_decorator
 from django.views.generic import View
 
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -135,10 +133,14 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 class CircuitListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_circuit'
+    _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
     queryset = Circuit.objects.select_related(
         'provider', 'type', 'tenant'
     ).prefetch_related(
         'terminations__site'
+    ).annotate(
+        a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
+        z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
     )
     filter = filters.CircuitFilter
     filter_form = forms.CircuitFilterForm

+ 1 - 0
netbox/dcim/api/views.py

@@ -579,6 +579,7 @@ class VirtualChassisViewSet(ModelViewSet):
         member_count=Count('members')
     )
     serializer_class = serializers.VirtualChassisSerializer
+    filterset_class = filters.VirtualChassisFilter
 
 
 #

+ 4 - 0
netbox/dcim/constants.py

@@ -280,6 +280,7 @@ IFACE_MODE_CHOICES = [
 # Pass-through port types
 PORT_TYPE_8P8C = 1000
 PORT_TYPE_110_PUNCH = 1100
+PORT_TYPE_BNC = 1200
 PORT_TYPE_ST = 2000
 PORT_TYPE_SC = 2100
 PORT_TYPE_SC_APC = 2110
@@ -296,6 +297,7 @@ PORT_TYPE_CHOICES = [
         [
             [PORT_TYPE_8P8C, '8P8C'],
             [PORT_TYPE_110_PUNCH, '110 Punch'],
+            [PORT_TYPE_BNC, 'BNC'],
         ],
     ],
     [
@@ -376,6 +378,7 @@ CABLE_TYPE_CAT6A = 1610
 CABLE_TYPE_CAT7 = 1700
 CABLE_TYPE_DAC_ACTIVE = 1800
 CABLE_TYPE_DAC_PASSIVE = 1810
+CABLE_TYPE_COAXIAL = 1900
 CABLE_TYPE_MMF = 3000
 CABLE_TYPE_MMF_OM1 = 3010
 CABLE_TYPE_MMF_OM2 = 3020
@@ -397,6 +400,7 @@ CABLE_TYPE_CHOICES = (
             (CABLE_TYPE_CAT7, 'CAT7'),
             (CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
             (CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
+            (CABLE_TYPE_COAXIAL, 'Coaxial'),
         ),
     ),
     (

+ 21 - 32
netbox/dcim/filters.py

@@ -2,14 +2,15 @@ import django_filters
 from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
-from netaddr import EUI
-from netaddr.core import AddrFormatError
 
 from extras.filters import CustomFieldFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
-from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
+from utilities.filters import (
+    MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
+    TreeNodeMultipleChoiceFilter,
+)
 from virtualization.models import Cluster
 from .constants import *
 from .models import (
@@ -514,8 +515,8 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
         field_name='device_type__is_full_depth',
         label='Is full depth',
     )
-    mac_address = django_filters.CharFilter(
-        method='_mac_address',
+    mac_address = MultiValueMACAddressFilter(
+        field_name='interfaces__mac_address',
         label='MAC address',
     )
     has_primary_ip = django_filters.BooleanFilter(
@@ -572,16 +573,6 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
             Q(comments__icontains=value)
         ).distinct()
 
-    def _mac_address(self, queryset, name, value):
-        value = value.strip()
-        if not value:
-            return queryset
-        try:
-            mac = EUI(value.strip())
-            return queryset.filter(interfaces__mac_address=mac).distinct()
-        except AddrFormatError:
-            return queryset.none()
-
     def _has_primary_ip(self, queryset, name, value):
         if value:
             return queryset.filter(
@@ -624,7 +615,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    device_id = django_filters.ModelChoiceFilter(
+    device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         label='Device (ID)',
     )
@@ -705,8 +696,8 @@ class InterfaceFilter(django_filters.FilterSet):
         field_name='name',
         label='Device',
     )
-    device_id = django_filters.NumberFilter(
-        method='filter_device',
+    device_id = MultiValueNumberFilter(
+        method='filter_device_id',
         field_name='pk',
         label='Device (ID)',
     )
@@ -724,10 +715,7 @@ class InterfaceFilter(django_filters.FilterSet):
         queryset=Interface.objects.all(),
         label='LAG interface (ID)',
     )
-    mac_address = django_filters.CharFilter(
-        method='_mac_address',
-        label='MAC address',
-    )
+    mac_address = MultiValueMACAddressFilter()
     tag = TagFilter()
     vlan_id = django_filters.CharFilter(
         method='filter_vlan_id',
@@ -762,6 +750,17 @@ class InterfaceFilter(django_filters.FilterSet):
         except Device.DoesNotExist:
             return queryset.none()
 
+    def filter_device_id(self, queryset, name, id_list):
+        # Include interfaces belonging to peer virtual chassis members
+        vc_interface_ids = []
+        try:
+            devices = Device.objects.filter(pk__in=id_list)
+            for device in devices:
+                vc_interface_ids += device.vc_interfaces.values_list('id', flat=True)
+            return queryset.filter(pk__in=vc_interface_ids)
+        except Device.DoesNotExist:
+            return queryset.none()
+
     def filter_vlan_id(self, queryset, name, value):
         value = value.strip()
         if not value:
@@ -788,16 +787,6 @@ class InterfaceFilter(django_filters.FilterSet):
             'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
         }.get(value, queryset.none())
 
-    def _mac_address(self, queryset, name, value):
-        value = value.strip()
-        if not value:
-            return queryset
-        try:
-            mac = EUI(value.strip())
-            return queryset.filter(mac_address=mac)
-        except AddrFormatError:
-            return queryset.none()
-
 
 class FrontPortFilter(DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(

+ 36 - 2
netbox/dcim/forms.py

@@ -7,6 +7,8 @@ from django.contrib.postgres.forms.array import SimpleArrayField
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from mptt.forms import TreeNodeChoiceField
+from netaddr import EUI
+from netaddr.core import AddrFormatError
 from taggit.forms import TagField
 from timezone_field import TimeZoneFormField
 
@@ -76,6 +78,28 @@ class BulkRenameForm(forms.Form):
                 })
 
 
+#
+# Fields
+#
+
+class MACAddressField(forms.Field):
+    widget = forms.CharField
+    default_error_messages = {
+        'invalid': 'MAC address must be in EUI-48 format',
+    }
+
+    def to_python(self, value):
+        value = super().to_python(value)
+
+        # Validate MAC address format
+        try:
+            value = EUI(value.strip())
+        except AddrFormatError:
+            raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
+
+        return value
+
+
 #
 # Regions
 #
@@ -954,6 +978,16 @@ class PowerPortTemplateCreateForm(ComponentForm):
     name_pattern = ExpandableNameField(
         label='Name'
     )
+    maximum_draw = forms.IntegerField(
+        min_value=1,
+        required=False,
+        help_text="Maximum current draw (watts)"
+    )
+    allocated_draw = forms.IntegerField(
+        min_value=1,
+        required=False,
+        help_text="Allocated current draw (watts)"
+    )
 
 
 class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
@@ -1244,7 +1278,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         required=False,
         widget=APISelect(
             api_url='/api/dcim/racks/',
-            display_field='display_name',
+            display_field='display_name'
         )
     )
     position = forms.TypedChoiceField(
@@ -3614,7 +3648,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=PowerPanel.objects.all(),
         required=False,
         widget=APISelect(
-            api_url="/api/dcim/sites",
+            api_url="/api/dcim/power-panels/",
             filter_for={
                 'rackgroup': 'site_id',
             }

+ 11 - 1
netbox/dcim/models.py

@@ -2271,7 +2271,7 @@ class Interface(CableTermination, ComponentModel):
 
         # It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
         # the component parent will raise DoesNotExist. For more discussion, see
-        # https://github.com/digitalocean/netbox/issues/2323
+        # https://github.com/netbox-community/netbox/issues/2323
         try:
             parent_obj = self.device or self.virtual_machine
         except ObjectDoesNotExist:
@@ -2772,6 +2772,16 @@ class Cable(ChangeLoggedModel):
                 self.termination_a_type, self.termination_b_type
             ))
 
+        # A component with multiple positions must be connected to a component with an equal number of positions
+        term_a_positions = getattr(self.termination_a, 'positions', 1)
+        term_b_positions = getattr(self.termination_b, 'positions', 1)
+        if term_a_positions != term_b_positions:
+            raise ValidationError(
+                "{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format(
+                    self.termination_a, term_a_positions, self.termination_b, term_b_positions
+                )
+            )
+
         # A termination point cannot be connected to itself
         if self.termination_a == self.termination_b:
             raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))

+ 1 - 1
netbox/dcim/tables.py

@@ -424,7 +424,7 @@ class PowerPortTemplateTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = PowerPortTemplate
-        fields = ('pk', 'name')
+        fields = ('pk', 'name', 'maximum_draw', 'allocated_draw')
         empty_text = "None"
 
 

+ 1 - 1
netbox/dcim/views.py

@@ -1903,7 +1903,7 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
 
 
 class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
-    permission_required = 'dcim.interface'
+    permission_required = 'dcim.view_interface'
     queryset = Interface.objects.select_related(
         'device', 'cable', '_connected_interface__device'
     ).filter(

+ 9 - 2
netbox/extras/views.py

@@ -146,7 +146,7 @@ class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 
 class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
-    permission_required = 'extras.delete_cconfigcontext'
+    permission_required = 'extras.delete_configcontext'
     queryset = ConfigContext.objects.all()
     table = ConfigContextTable
     default_return_url = 'extras:configcontext_list'
@@ -228,6 +228,13 @@ class ObjectChangeLogView(View):
             orderable=False
         )
 
+        # Apply the request context
+        paginate = {
+            'paginator_class': EnhancedPaginator,
+            'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+        }
+        RequestConfig(request, paginate).configure(objectchanges_table)
+
         # Check whether a header template exists for this model
         base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
         try:
@@ -239,7 +246,7 @@ class ObjectChangeLogView(View):
 
         return render(request, 'extras/object_changelog.html', {
             object_var: obj,
-            'objectchanges_table': objectchanges_table,
+            'table': objectchanges_table,
             'base_template': base_template,
             'active_tab': 'changelog',
         })

+ 1 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
 # Environment setup
 #
 
-VERSION = '2.6.1'
+VERSION = '2.6.2'
 
 # Hostname
 HOSTNAME = platform.node()

+ 1 - 1
netbox/netbox/urls.py

@@ -14,7 +14,7 @@ schema_view = get_schema_view(
         title="NetBox API",
         default_version='v2',
         description="API to access NetBox",
-        terms_of_service="https://github.com/digitalocean/netbox",
+        terms_of_service="https://github.com/netbox-community/netbox",
         contact=openapi.Contact(email="netbox@digitalocean.com"),
         license=openapi.License(name="Apache v2 License"),
     ),

+ 2 - 1
netbox/netbox/views.py

@@ -15,7 +15,7 @@ from dcim.filters import (
     VirtualChassisFilter,
 )
 from dcim.models import (
-    Cable, ConsolePort, Device, DeviceType, Interface, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
+    Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
 )
 from dcim.tables import (
     CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
@@ -196,6 +196,7 @@ class HomeView(View):
             'cable_count': cables.count(),
             'console_connections_count': connected_consoleports.count(),
             'power_connections_count': connected_powerports.count(),
+            'powerpanel_count': PowerPanel.objects.count(),
             'powerfeed_count': PowerFeed.objects.count(),
 
             # IPAM

+ 4 - 2
netbox/project-static/js/forms.js

@@ -183,7 +183,7 @@ $(document).ready(function() {
                 // Additional query params
                 $.each(element.attributes, function(index, attr){
                     if (attr.name.includes("data-additional-query-param-")){
-                        var param_name = attr.name.split("data-additional-query-param-")[1]
+                        var param_name = attr.name.split("data-additional-query-param-")[1];
                         parameters[param_name] = attr.value;
                     }
                 });
@@ -194,6 +194,8 @@ $(document).ready(function() {
 
             processResults: function (data) {
                 var element = this.$element[0];
+                // Clear any disabled options
+                $(element).children('option').attr('disabled', false);
                 var results = $.map(data.results, function (obj) {
                     obj.text = obj[element.getAttribute('display-field')] || obj.name;
                     obj.id = obj[element.getAttribute('value-field')] || obj.id;
@@ -207,7 +209,7 @@ $(document).ready(function() {
 
                 // Handle the null option, but only add it once
                 if (element.getAttribute('data-null-option') && data.previous === null) {
-                    var null_option = $(element).children()[0]
+                    var null_option = $(element).children()[0];
                     results.unshift({
                         id: null_option.value,
                         text: null_option.text

+ 2 - 2
netbox/templates/_base.html

@@ -58,8 +58,8 @@
                     <p class="text-muted">
                         <i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> &middot;
                         <i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> &middot;
-                        <i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> &middot;
-                        <i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
+                        <i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/netbox-community/netbox">Code</a> &middot;
+                        <i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/netbox-community/netbox/wiki">Help</a>
                     </p>
                 </div>
             </div>

+ 4 - 3
netbox/templates/extras/object_changelog.html

@@ -4,10 +4,11 @@
 
 {% block content %}
     {% if obj %}<h1>{{ obj }}</h1>{% endif %}
-    {% include 'panel_table.html' with table=objectchanges_table %}
+    {% include 'panel_table.html' %}
+        {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
     {% if settings.CHANGELOG_RETENTION %}
-        <div class="pull-right text-muted">
-            Changelog retention: {{ settings.CHANGELOG_RETENTION }} days
+        <div class="text-muted">
+            Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
         </div>
     {% endif %}
 {% endblock %}

+ 1 - 1
netbox/templates/extras/objectchange_list.html

@@ -11,7 +11,7 @@
         {% include 'utilities/obj_table.html' %}
         {% if settings.CHANGELOG_RETENTION %}
             <div class="pull-right text-muted">
-                Changelog retention: {{ settings.CHANGELOG_RETENTION }} days
+                Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
             </div>
         {% endif %}
     </div>

+ 10 - 0
netbox/templates/home.html

@@ -115,6 +115,16 @@
                     {% endif %}
                     <p class="list-group-item-text text-muted">Electrical circuits delivering power from panels</p>
                 </div>
+                <div class="list-group-item">
+                    {% if perms.dcim.view_powerpanel %}
+                        <span class="badge pull-right">{{ stats.powerpanel_count }}</span>
+                        <h4 class="list-group-item-heading"><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></h4>
+                    {% else %}
+                        <span class="badge pull-right"><i class="fa fa-lock"></i></span>
+                        <h4 class="list-group-item-heading">Power Panels</h4>
+                    {% endif %}
+                    <p class="list-group-item-text text-muted">Electrical panels receiving utility power</p>
+                </div>
             </div>
         </div>
         <div class="panel panel-default">

+ 1 - 1
netbox/templates/virtualization/cluster.html

@@ -33,7 +33,7 @@
                 Edit this cluster
             </a>
         {% endif %}
-        {% if perms.dcim.delete_cluster %}
+        {% if perms.virtualization.delete_cluster %}
             <a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
                 <span class="fa fa-trash" aria-hidden="true"></span>
                 Delete this cluster

+ 9 - 0
netbox/utilities/filters.py

@@ -3,6 +3,7 @@ from django import forms
 from django.conf import settings
 from django.db import models
 
+from dcim.forms import MACAddressField
 from extras.models import Tag
 
 
@@ -49,6 +50,14 @@ class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
     field_class = multivalue_field_factory(forms.TimeField)
 
 
+class MACAddressFilter(django_filters.CharFilter):
+    field_class = MACAddressField
+
+
+class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(MACAddressField)
+
+
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]

+ 0 - 6
upgrade.sh

@@ -10,12 +10,6 @@ cd "$(dirname "$0")"
 PYTHON="python3"
 PIP="pip3"
 
-# TODO: Remove this in v2.6 as it is no longer needed under Python 3
-# Delete stale bytecode
-COMMAND="find . -name \"*.pyc\" -delete"
-echo "Cleaning up stale Python bytecode ($COMMAND)..."
-eval $COMMAND
-
 # Uninstall any Python packages which are no longer needed
 COMMAND="${PIP} uninstall -r old_requirements.txt -y"
 echo "Removing old Python packages ($COMMAND)..."

Some files were not shown because too many files changed in this diff