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

Merge pull request #3387 from netbox-community/develop

Release v2.6.2
Jeremy Stretch 6 лет назад
Родитель
Сommit
7fc60cd667

Разница между файлами не показана из-за своего большого размера
+ 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
 ## 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
 of NetBox. If you're running an older version, it's possible that the bug has
 already been fixed.
 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
 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
 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
 click "add a reaction" in the top right corner of the issue and add a thumbs
@@ -51,7 +51,7 @@ your issue.
 
 
 ## Feature Requests
 ## 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
 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
 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
 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/)
 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
 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/).
 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
 # Installation
 
 
 Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
 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`.
 and run `upgrade.sh`.
 
 
 ## Alternative Installations
 ## 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
 
 
-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.
 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 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
 
 
 Communication among developers should always occur via public channels:
 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.
 * [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.
 * [#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
 ## 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`)
 * **Tag:** Current version (e.g. `v2.3.4`)
 * **Target:** `master`
 * **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
 ## 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
 ```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
 # tar -xzf vX.Y.Z.tar.gz -C /opt
 # cd /opt/
 # cd /opt/
 # ln -s netbox-X.Y.Z/ netbox
 # 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:
 Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
 
 
 ```no-highlight
 ```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 '.'...
 Cloning into '.'...
 remote: Counting objects: 1994, done.
 remote: Counting objects: 1994, done.
 remote: Compressing objects: 100% (150/150), 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
 ## 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:
 Download and extract the latest version:
 
 
 ```no-highlight
 ```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
 # tar -xzf vX.Y.Z.tar.gz -C /opt
 # cd /opt/
 # cd /opt/
 # ln -sfn netbox-X.Y.Z/ netbox
 # ln -sfn netbox-X.Y.Z/ netbox

+ 1 - 1
mkdocs.yml

@@ -1,6 +1,6 @@
 site_name: NetBox
 site_name: NetBox
 theme: readthedocs
 theme: readthedocs
-repo_url: https://github.com/digitalocean/netbox
+repo_url: https://github.com/netbox-community/netbox
 
 
 pages:
 pages:
     - Introduction: 'index.md'
     - 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
 # Providers
 #
 #
@@ -77,9 +68,13 @@ class CircuitTable(BaseTable):
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
     tenant = tables.TemplateColumn(template_code=COL_TENANT)
     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):
     class Meta(BaseTable.Meta):
         model = Circuit
         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 import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 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.shortcuts import get_object_or_404, redirect, render
-from django.utils.decorators import method_decorator
 from django.views.generic import View
 from django.views.generic import View
 
 
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -135,10 +133,14 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 
 class CircuitListView(PermissionRequiredMixin, ObjectListView):
 class CircuitListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_circuit'
     permission_required = 'circuits.view_circuit'
+    _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
     queryset = Circuit.objects.select_related(
     queryset = Circuit.objects.select_related(
         'provider', 'type', 'tenant'
         'provider', 'type', 'tenant'
     ).prefetch_related(
     ).prefetch_related(
         'terminations__site'
         '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 = filters.CircuitFilter
     filter_form = forms.CircuitFilterForm
     filter_form = forms.CircuitFilterForm

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

@@ -579,6 +579,7 @@ class VirtualChassisViewSet(ModelViewSet):
         member_count=Count('members')
         member_count=Count('members')
     )
     )
     serializer_class = serializers.VirtualChassisSerializer
     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
 # Pass-through port types
 PORT_TYPE_8P8C = 1000
 PORT_TYPE_8P8C = 1000
 PORT_TYPE_110_PUNCH = 1100
 PORT_TYPE_110_PUNCH = 1100
+PORT_TYPE_BNC = 1200
 PORT_TYPE_ST = 2000
 PORT_TYPE_ST = 2000
 PORT_TYPE_SC = 2100
 PORT_TYPE_SC = 2100
 PORT_TYPE_SC_APC = 2110
 PORT_TYPE_SC_APC = 2110
@@ -296,6 +297,7 @@ PORT_TYPE_CHOICES = [
         [
         [
             [PORT_TYPE_8P8C, '8P8C'],
             [PORT_TYPE_8P8C, '8P8C'],
             [PORT_TYPE_110_PUNCH, '110 Punch'],
             [PORT_TYPE_110_PUNCH, '110 Punch'],
+            [PORT_TYPE_BNC, 'BNC'],
         ],
         ],
     ],
     ],
     [
     [
@@ -376,6 +378,7 @@ CABLE_TYPE_CAT6A = 1610
 CABLE_TYPE_CAT7 = 1700
 CABLE_TYPE_CAT7 = 1700
 CABLE_TYPE_DAC_ACTIVE = 1800
 CABLE_TYPE_DAC_ACTIVE = 1800
 CABLE_TYPE_DAC_PASSIVE = 1810
 CABLE_TYPE_DAC_PASSIVE = 1810
+CABLE_TYPE_COAXIAL = 1900
 CABLE_TYPE_MMF = 3000
 CABLE_TYPE_MMF = 3000
 CABLE_TYPE_MMF_OM1 = 3010
 CABLE_TYPE_MMF_OM1 = 3010
 CABLE_TYPE_MMF_OM2 = 3020
 CABLE_TYPE_MMF_OM2 = 3020
@@ -397,6 +400,7 @@ CABLE_TYPE_CHOICES = (
             (CABLE_TYPE_CAT7, 'CAT7'),
             (CABLE_TYPE_CAT7, 'CAT7'),
             (CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
             (CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
             (CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
             (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.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from django.db.models import Q
-from netaddr import EUI
-from netaddr.core import AddrFormatError
 
 
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 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 virtualization.models import Cluster
 from .constants import *
 from .constants import *
 from .models import (
 from .models import (
@@ -514,8 +515,8 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
         field_name='device_type__is_full_depth',
         field_name='device_type__is_full_depth',
         label='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',
         label='MAC address',
     )
     )
     has_primary_ip = django_filters.BooleanFilter(
     has_primary_ip = django_filters.BooleanFilter(
@@ -572,16 +573,6 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
             Q(comments__icontains=value)
             Q(comments__icontains=value)
         ).distinct()
         ).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):
     def _has_primary_ip(self, queryset, name, value):
         if value:
         if value:
             return queryset.filter(
             return queryset.filter(
@@ -624,7 +615,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
-    device_id = django_filters.ModelChoiceFilter(
+    device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         label='Device (ID)',
         label='Device (ID)',
     )
     )
@@ -705,8 +696,8 @@ class InterfaceFilter(django_filters.FilterSet):
         field_name='name',
         field_name='name',
         label='Device',
         label='Device',
     )
     )
-    device_id = django_filters.NumberFilter(
-        method='filter_device',
+    device_id = MultiValueNumberFilter(
+        method='filter_device_id',
         field_name='pk',
         field_name='pk',
         label='Device (ID)',
         label='Device (ID)',
     )
     )
@@ -724,10 +715,7 @@ class InterfaceFilter(django_filters.FilterSet):
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         label='LAG interface (ID)',
         label='LAG interface (ID)',
     )
     )
-    mac_address = django_filters.CharFilter(
-        method='_mac_address',
-        label='MAC address',
-    )
+    mac_address = MultiValueMACAddressFilter()
     tag = TagFilter()
     tag = TagFilter()
     vlan_id = django_filters.CharFilter(
     vlan_id = django_filters.CharFilter(
         method='filter_vlan_id',
         method='filter_vlan_id',
@@ -762,6 +750,17 @@ class InterfaceFilter(django_filters.FilterSet):
         except Device.DoesNotExist:
         except Device.DoesNotExist:
             return queryset.none()
             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):
     def filter_vlan_id(self, queryset, name, value):
         value = value.strip()
         value = value.strip()
         if not value:
         if not value:
@@ -788,16 +787,6 @@ class InterfaceFilter(django_filters.FilterSet):
             'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
             'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
         }.get(value, queryset.none())
         }.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):
 class FrontPortFilter(DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(
     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.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from django.db.models import Q
 from mptt.forms import TreeNodeChoiceField
 from mptt.forms import TreeNodeChoiceField
+from netaddr import EUI
+from netaddr.core import AddrFormatError
 from taggit.forms import TagField
 from taggit.forms import TagField
 from timezone_field import TimeZoneFormField
 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
 # Regions
 #
 #
@@ -954,6 +978,16 @@ class PowerPortTemplateCreateForm(ComponentForm):
     name_pattern = ExpandableNameField(
     name_pattern = ExpandableNameField(
         label='Name'
         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):
 class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
@@ -1244,7 +1278,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
             api_url='/api/dcim/racks/',
             api_url='/api/dcim/racks/',
-            display_field='display_name',
+            display_field='display_name'
         )
         )
     )
     )
     position = forms.TypedChoiceField(
     position = forms.TypedChoiceField(
@@ -3614,7 +3648,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
         queryset=PowerPanel.objects.all(),
         queryset=PowerPanel.objects.all(),
         required=False,
         required=False,
         widget=APISelect(
         widget=APISelect(
-            api_url="/api/dcim/sites",
+            api_url="/api/dcim/power-panels/",
             filter_for={
             filter_for={
                 'rackgroup': 'site_id',
                 '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
         # 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
         # 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:
         try:
             parent_obj = self.device or self.virtual_machine
             parent_obj = self.device or self.virtual_machine
         except ObjectDoesNotExist:
         except ObjectDoesNotExist:
@@ -2772,6 +2772,16 @@ class Cable(ChangeLoggedModel):
                 self.termination_a_type, self.termination_b_type
                 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
         # A termination point cannot be connected to itself
         if self.termination_a == self.termination_b:
         if self.termination_a == self.termination_b:
             raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
             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):
     class Meta(BaseTable.Meta):
         model = PowerPortTemplate
         model = PowerPortTemplate
-        fields = ('pk', 'name')
+        fields = ('pk', 'name', 'maximum_draw', 'allocated_draw')
         empty_text = "None"
         empty_text = "None"
 
 
 
 

+ 1 - 1
netbox/dcim/views.py

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

+ 9 - 2
netbox/extras/views.py

@@ -146,7 +146,7 @@ class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 
 
 
 
 class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
-    permission_required = 'extras.delete_cconfigcontext'
+    permission_required = 'extras.delete_configcontext'
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     table = ConfigContextTable
     table = ConfigContextTable
     default_return_url = 'extras:configcontext_list'
     default_return_url = 'extras:configcontext_list'
@@ -228,6 +228,13 @@ class ObjectChangeLogView(View):
             orderable=False
             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
         # Check whether a header template exists for this model
         base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
         base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
         try:
         try:
@@ -239,7 +246,7 @@ class ObjectChangeLogView(View):
 
 
         return render(request, 'extras/object_changelog.html', {
         return render(request, 'extras/object_changelog.html', {
             object_var: obj,
             object_var: obj,
-            'objectchanges_table': objectchanges_table,
+            'table': objectchanges_table,
             'base_template': base_template,
             'base_template': base_template,
             'active_tab': 'changelog',
             'active_tab': 'changelog',
         })
         })

+ 1 - 1
netbox/netbox/settings.py

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

+ 1 - 1
netbox/netbox/urls.py

@@ -14,7 +14,7 @@ schema_view = get_schema_view(
         title="NetBox API",
         title="NetBox API",
         default_version='v2',
         default_version='v2',
         description="API to access NetBox",
         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"),
         contact=openapi.Contact(email="netbox@digitalocean.com"),
         license=openapi.License(name="Apache v2 License"),
         license=openapi.License(name="Apache v2 License"),
     ),
     ),

+ 2 - 1
netbox/netbox/views.py

@@ -15,7 +15,7 @@ from dcim.filters import (
     VirtualChassisFilter,
     VirtualChassisFilter,
 )
 )
 from dcim.models import (
 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 (
 from dcim.tables import (
     CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
     CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
@@ -196,6 +196,7 @@ class HomeView(View):
             'cable_count': cables.count(),
             'cable_count': cables.count(),
             'console_connections_count': connected_consoleports.count(),
             'console_connections_count': connected_consoleports.count(),
             'power_connections_count': connected_powerports.count(),
             'power_connections_count': connected_powerports.count(),
+            'powerpanel_count': PowerPanel.objects.count(),
             'powerfeed_count': PowerFeed.objects.count(),
             'powerfeed_count': PowerFeed.objects.count(),
 
 
             # IPAM
             # IPAM

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

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

+ 2 - 2
netbox/templates/_base.html

@@ -58,8 +58,8 @@
                     <p class="text-muted">
                     <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-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-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>
                     </p>
                 </div>
                 </div>
             </div>
             </div>

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

@@ -4,10 +4,11 @@
 
 
 {% block content %}
 {% block content %}
     {% if obj %}<h1>{{ obj }}</h1>{% endif %}
     {% 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 %}
     {% 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>
         </div>
     {% endif %}
     {% endif %}
 {% endblock %}
 {% endblock %}

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

@@ -11,7 +11,7 @@
         {% include 'utilities/obj_table.html' %}
         {% include 'utilities/obj_table.html' %}
         {% if settings.CHANGELOG_RETENTION %}
         {% if settings.CHANGELOG_RETENTION %}
             <div class="pull-right text-muted">
             <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>
             </div>
         {% endif %}
         {% endif %}
     </div>
     </div>

+ 10 - 0
netbox/templates/home.html

@@ -115,6 +115,16 @@
                     {% endif %}
                     {% endif %}
                     <p class="list-group-item-text text-muted">Electrical circuits delivering power from panels</p>
                     <p class="list-group-item-text text-muted">Electrical circuits delivering power from panels</p>
                 </div>
                 </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>
         </div>
         <div class="panel panel-default">
         <div class="panel panel-default">

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

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

+ 9 - 0
netbox/utilities/filters.py

@@ -3,6 +3,7 @@ from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.db import models
 from django.db import models
 
 
+from dcim.forms import MACAddressField
 from extras.models import Tag
 from extras.models import Tag
 
 
 
 
@@ -49,6 +50,14 @@ class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
     field_class = multivalue_field_factory(forms.TimeField)
     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):
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]
     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"
 PYTHON="python3"
 PIP="pip3"
 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
 # Uninstall any Python packages which are no longer needed
 COMMAND="${PIP} uninstall -r old_requirements.txt -y"
 COMMAND="${PIP} uninstall -r old_requirements.txt -y"
 echo "Removing old Python packages ($COMMAND)..."
 echo "Removing old Python packages ($COMMAND)..."

Некоторые файлы не были показаны из-за большого количества измененных файлов