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
* 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
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/)
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

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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