Merge pull request #3387 from netbox-community/develop

Release v2.6.2
This commit is contained in:
Jeremy Stretch 2019-08-02 10:31:34 -04:00 committed by GitHub
commit 7fc60cd667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1147 additions and 1062 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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
NOTICE Normal file
View File

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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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`

View File

@ -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.

View File

@ -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

View File

@ -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'

View File

@ -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') a_side = tables.Column(
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side') 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')

View File

@ -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

View File

@ -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
# #

View File

@ -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'),
), ),
), ),
( (

View File

@ -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( mac_address = MultiValueMACAddressFilter(
method='_mac_address', 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( device_id = MultiValueNumberFilter(
method='filter_device', 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( mac_address = MultiValueMACAddressFilter()
method='_mac_address',
label='MAC address',
)
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(

View File

@ -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',
} }

View File

@ -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))

View File

@ -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"

View File

@ -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(

View File

@ -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',
}) })

View File

@ -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()

View File

@ -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"),
), ),

View File

@ -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

View File

@ -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

View File

@ -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-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/digitalocean/netbox/wiki">Help</a> <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>

View File

@ -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"> <div class="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 %}
{% endblock %} {% endblock %}

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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>]

View File

@ -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)..."