Compare commits

...

53 Commits

Author SHA1 Message Date
Jeremy Stretch
7fc60cd667 Merge pull request #3387 from netbox-community/develop
Release v2.6.2
2019-08-02 10:31:34 -04:00
Jeremy Stretch
c90baaa807 Release v2.6.2 2019-08-02 10:29:10 -04:00
Jeremy Stretch
ea9492d4bd Fixes #3384: Maximum and allocated draw fields should be included on power port template creation form 2019-08-02 09:56:02 -04:00
Jeremy Stretch
025f77dcdc Fixes #3385: Fix power panels list when bulk editing power feeds 2019-08-02 09:43:46 -04:00
Jeremy Stretch
eb19b1a39e Changelog for #3367 2019-08-02 09:13:48 -04:00
Jeremy Stretch
25748efd54 Merge pull request #3383 from ragzilla/develop
Closes #3367: Add BNC Front/Rear port types and Coaxial cable type.
2019-08-02 09:10:40 -04:00
Matt Addison
2215a095c8 Closes #3367: Add BNC Front/Rear port types and Coaxial cable type. 2019-08-01 10:33:29 -04:00
Jeremy Stretch
a32d185ff0 Fixes #3018: Components connected via a cable must have an equal number of positions 2019-07-31 10:12:51 -04:00
Jeremy Stretch
ea32853ab3 Fixes #3289: Prevent position from being nullified when moving a device to a new rack 2019-07-30 17:07:58 -04:00
Jeremy Stretch
a6c41e0be5 Changelog for #3368 2019-07-30 16:26:52 -04:00
Jeremy Stretch
f223f9b9c1 Merge pull request #3369 from jlrgraham23/fix-changelog-wording
Indicate when changelog retention configured to be forever.
2019-07-30 16:20:39 -04:00
Daniel Sheppard
bcc7daeac7 Fixes #3370 - Add filter class to VirtualChassis API 2019-07-24 12:22:15 -05:00
Justin L R Graham
890ba3ea94 Indicate when changelog retention configured to be forever. 2019-07-23 13:46:55 -05:00
Jeremy Stretch
cab3c50ae6 Closes #3314: Paginate object changelog entries 2019-07-18 21:40:36 -04:00
Jeremy Stretch
86b6b9bf8b Fixes #3315: Enable filtering devices/interfaces by multiple MAC addresses 2019-07-18 21:21:56 -04:00
Jeremy Stretch
71551893b1 Fixes #3293: Enable filtering device components by multiple device IDs 2019-07-18 20:42:15 -04:00
Jeremy Stretch
0431b296c4 Merge pull request #3325 from lassebm/fix-3324
Fixes #3324: Doc incorrectly states child devices shown as non-racked
2019-07-17 16:27:55 -04:00
Jeremy Stretch
376eae748c Changelog for #3323 2019-07-17 16:25:19 -04:00
Jeremy Stretch
f2a45c5892 Merge pull request #3326 from lassebm/fix-3323
Fixes #3323: Interface Connections view inaccessible with "dcim.view_interface" permission
2019-07-17 16:24:14 -04:00
Jeremy Stretch
88b176ae15 Changelog for #3307 2019-07-17 16:22:30 -04:00
Jeremy Stretch
f1744ef4db Merge pull request #3308 from mmahacek/powerpanel-count
Add Powerpanel count to home page
2019-07-17 16:20:27 -04:00
Jeremy Stretch
892ff0c1ca Changelog for #3342 2019-07-16 11:31:36 -04:00
Jeremy Stretch
647163e2b2 Merge pull request #3337 from robellegate/fix/docs_digitalocean_repo_links
Replacing references to digitalocean org
2019-07-16 10:18:37 -04:00
Jeremy Stretch
8e043b00b8 Closes #3330: Remove .pyc file cleanup step from upgrade script 2019-07-16 10:11:39 -04:00
Jeremy Stretch
154b9e1faf Fixes #3342: Fix cluster delete button permissions reference 2019-07-16 10:07:38 -04:00
Robert Ellegate
30ef4b208c Replacing references to digitalocean org
s/(?<=:\/\/github.com\/)digitalocean(?=\/netbox)/netbox-community/g
2019-07-10 09:23:43 -04:00
Lasse Bang Mikkelsen
6276f5f7b9 Fixes #3323: Interface Connections view inaccessible with "dcim.view_interface" permission 2019-07-04 17:37:28 +02:00
Lasse Bang Mikkelsen
118ec358c0 Fixes #3324: Doc incorrectly states child devices shown as non-racked 2019-07-04 17:28:25 +02:00
Jeremy Stretch
3da9af5a9f Fixes #3317: Fix permissions for ConfigContextBulkDeleteView 2019-07-02 09:39:26 -04:00
mmahacek
ddced4fc2b Add stats.powerpanel_count 2019-06-28 17:04:42 -07:00
mmahacek
7a41b02fdd Add line for PowerPanel count 2019-06-28 17:03:06 -07:00
Jeremy Stretch
6c3c6fba62 Closes #984: Allow ordering circuits by A/Z side 2019-06-27 12:30:17 -04:00
Jeremy Stretch
2bb9464905 Merge pull request #3296 from michaelxniu/patch-1
Create NOTICE file
2019-06-25 14:10:14 -04:00
Michael Niu
821924f57f Create NOTICE file 2019-06-25 13:59:00 -04:00
Jeremy Stretch
74f14c5535 Post-release version bump 2019-06-25 09:44:00 -04:00
Jeremy Stretch
80c8c4c4b2 Merge pull request #3295 from digitalocean/develop
Release v2.6.1
2019-06-25 09:41:29 -04:00
Jeremy Stretch
d219c3ea88 Release v2.6.1 2019-06-25 09:39:30 -04:00
Jeremy Stretch
954ba91c86 Closes #3154: Add virtual_chassis_member device filter 2019-06-24 16:31:21 -04:00
Jeremy Stretch
7effb7e8d4 Fix for #3229 2019-06-24 15:48:49 -04:00
Jeremy Stretch
3fdb655a92 Fixes #3269: Raise validation error when specifying non-existent cable terminationss 2019-06-24 15:42:15 -04:00
Jeremy Stretch
cf770bf40c Closes #3277: Add cable trace buttons for console and power ports 2019-06-24 14:27:34 -04:00
Jeremy Stretch
9f50ced6fc Changelog for #3229 2019-06-24 14:22:03 -04:00
Jeremy Stretch
4dd97eab0c Merge pull request #3232 from hellerve/fix-3229
Filter group by site in rack filter
2019-06-24 14:19:50 -04:00
Jeremy Stretch
5de242fe53 Closes #3281: Hide custom links which render as empty text 2019-06-24 12:20:09 -04:00
Jeremy Stretch
251ba08e09 Fixes #3283: Fix rack group assignment on PowerFeed CSV import 2019-06-24 11:10:35 -04:00
Jeremy Stretch
653770ede9 Fixes #3292: Ignore empty URL query parameters 2019-06-24 11:00:18 -04:00
Jeremy Stretch
70ef6a69ee Fixes #3290: Fix server error when viewing cascaded PDUs 2019-06-24 10:05:21 -04:00
Jeremy Stretch
5a6c928a7c Fixes #3279: Reset the PostgreSQL sequence for Tag and TaggedItem IDs 2019-06-21 17:34:06 -04:00
John Anderson
bd9ef9951b Merge pull request #3284 from cimnine/fix_pwd_protected_redis_cache
Fixes Cacheops with a password protected redis
2019-06-21 16:35:50 -04:00
Jeremy Stretch
c067549f21 Fixes #3275: Fix error when adding power outlets to a device type 2019-06-21 16:24:12 -04:00
Christian Mäder
94ca3abefc Fixes Cacheops with a password protected redis
As per the [`README.rst`][1] of `django-cacheops`, if a password is
added to the connection string, it must be in the form
`redis://:password@host:port/db`. Notice the colon, which was missing
from the implementation in [`settings.py`][2].

[1]: 8ad970d55a/README.rst
[2]: 86d5b48007/netbox/netbox/settings.py (L349)
2019-06-21 22:23:10 +02:00
Jeremy Stretch
86d5b48007 Post-release version bump 2019-06-20 17:01:21 -04:00
hellerve
e89343e100 dcim: filter group by site in rack filter (fixes #3229) 2019-06-02 14:26:28 +02:00
37 changed files with 1307 additions and 1130 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(
@@ -527,6 +528,10 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
queryset=VirtualChassis.objects.all(),
label='Virtual chassis (ID)',
)
virtual_chassis_member = django_filters.BooleanFilter(
method='_virtual_chassis_member',
label='Is a virtual chassis member'
)
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
@@ -568,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(
@@ -590,6 +585,9 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
Q(primary_ip6__isnull=False)
)
def _virtual_chassis_member(self, queryset, name, value):
return queryset.exclude(virtual_chassis__isnull=value)
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleports__isnull=value)
@@ -617,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)',
)
@@ -698,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)',
)
@@ -717,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',
@@ -755,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:
@@ -781,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
#
@@ -601,12 +625,18 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
filter_for={
'group_id': 'site'
}
)
)
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site'),
group_id = ChainedModelChoiceField(
label='Rack group',
null_label='-- None --',
queryset=RackGroup.objects.select_related('site'),
chains=(
('site', 'site'),
),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/rack-groups/",
null_option=True,
@@ -948,13 +978,19 @@ 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):
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False
)
class Meta:
model = PowerOutletTemplate
@@ -965,6 +1001,21 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
'device_type': forms.HiddenInput(),
}
class PowerOutletTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(
label='Name'
)
power_port = forms.ModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False
)
feed_leg = forms.ChoiceField(
choices=add_blank_choice(POWERFEED_LEG_CHOICES),
required=False,
widget=StaticSelect2()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -975,12 +1026,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
)
class PowerOutletTemplateCreateForm(ComponentForm):
name_pattern = ExpandableNameField(
label='Name'
)
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
@@ -1233,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(
@@ -1739,6 +1784,13 @@ class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
virtual_chassis_member = forms.NullBooleanField(
required=False,
label='Virtual chassis member',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
@@ -3580,7 +3632,7 @@ class PowerFeedCSVForm(forms.ModelForm):
# Validate rack
if rack_name:
try:
self.instance.rack = Rack.objects.get(site=site, rack_group=rack_group, name=rack_name)
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
except Rack.DoesNotExist:
raise forms.ValidationError(
"Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
@@ -3596,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:
@@ -2747,55 +2747,79 @@ class Cable(ChangeLoggedModel):
def clean(self):
if self.termination_a and self.termination_b:
# Validate that termination A exists
try:
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
except ObjectDoesNotExist:
raise ValidationError({
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
})
type_a = self.termination_a_type.model
type_b = self.termination_b_type.model
# Validate that termination B exists
try:
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
except ObjectDoesNotExist:
raise ValidationError({
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
})
# Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
self.termination_a_type, self.termination_b_type
))
type_a = self.termination_a_type.model
type_b = self.termination_b_type.model
# 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))
# Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
self.termination_a_type, self.termination_b_type
))
# A front port cannot be connected to its corresponding rear port
if (
type_a in ['frontport', 'rearport'] and
type_b in ['frontport', 'rearport'] and
(
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
getattr(self.termination_b, 'rear_port', None) == self.termination_a
# 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
)
):
raise ValidationError("A front port cannot be connected to it corresponding rear port")
)
# Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_a, self.termination_a.cable_id
))
if self.termination_b.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_b, self.termination_b.cable_id
))
# 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))
# Virtual interfaces cannot be connected
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
if (
(
isinstance(endpoint_a, Interface) and
endpoint_a.type == IFACE_TYPE_VIRTUAL
) or
(
isinstance(endpoint_b, Interface) and
endpoint_b.type == IFACE_TYPE_VIRTUAL
)
):
raise ValidationError("Cannot connect to a virtual interface")
# A front port cannot be connected to its corresponding rear port
if (
type_a in ['frontport', 'rearport'] and
type_b in ['frontport', 'rearport'] and
(
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
getattr(self.termination_b, 'rear_port', None) == self.termination_a
)
):
raise ValidationError("A front port cannot be connected to it corresponding rear port")
# Check for an existing Cable connected to either termination object
if self.termination_a.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_a, self.termination_a.cable_id
))
if self.termination_b.cable not in (None, self):
raise ValidationError("{} already has a cable attached (#{})".format(
self.termination_b, self.termination_b.cable_id
))
# Virtual interfaces cannot be connected
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
if (
(
isinstance(endpoint_a, Interface) and
endpoint_a.type == IFACE_TYPE_VIRTUAL
) or
(
isinstance(endpoint_b, Interface) and
endpoint_b.type == IFACE_TYPE_VIRTUAL
)
):
raise ValidationError("Cannot connect to a virtual interface")
# Validate length and length_unit
if self.length is not None and self.length_unit is None:

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"
@@ -433,7 +433,7 @@ class PowerOutletTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerOutletTemplate
fields = ('pk', 'name')
fields = ('pk', 'name', 'power_port', 'feed_leg')
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

@@ -87,7 +87,8 @@ class CustomLinkForm(forms.ModelForm):
model = CustomLink
exclude = []
help_texts = {
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>.',
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. Links '
'which render as empty text will not be displayed.',
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
}

View File

@@ -0,0 +1,14 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('extras', '0022_custom_links'),
]
operations = [
# Update the last_value for tag Tag and TaggedItem ID sequences
migrations.RunSQL("SELECT setval('extras_tag_id_seq', (SELECT id FROM extras_tag ORDER BY id DESC LIMIT 1) + 1)"),
migrations.RunSQL("SELECT setval('extras_taggeditem_id_seq', (SELECT id FROM extras_taggeditem ORDER BY id DESC LIMIT 1) + 1)"),
]

View File

@@ -15,7 +15,8 @@ GROUP_BUTTON = '<div class="btn-group">\n' \
'<button type="button" class="btn btn-sm btn-{} dropdown-toggle" data-toggle="dropdown">\n' \
'{} <span class="caret"></span>\n' \
'</button>\n' \
'<ul class="dropdown-menu pull-right">\n'
'<ul class="dropdown-menu pull-right">\n' \
'{}</ul></div>'
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
@@ -35,32 +36,40 @@ def custom_links(obj):
template_code = ''
group_names = OrderedDict()
# Organize custom links by group
for cl in custom_links:
# Organize custom links by group
if cl.group_name and cl.group_name in group_names:
group_names[cl.group_name].append(cl)
elif cl.group_name:
group_names[cl.group_name] = [cl]
# Add non-grouped links
for cl in custom_links:
if not cl.group_name:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
cl.url, link_target, cl.button_class, cl.text
)
# Add non-grouped links
else:
text_rendered = Environment().from_string(source=cl.text).render(**context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
cl.url, link_target, cl.button_class, text_rendered
)
# Add grouped links to template
for group, links in group_names.items():
template_code += GROUP_BUTTON.format(
links[0].button_class, group
)
links_rendered = []
for cl in links:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += GROUP_LINK.format(
cl.url, link_target, cl.text
text_rendered = Environment().from_string(source=cl.text).render(**context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
links_rendered.append(
GROUP_LINK.format(cl.url, link_target, cl.text)
)
if links_rendered:
template_code += GROUP_BUTTON.format(
links[0].button_class, group, ''.join(links_rendered)
)
template_code += '</ul>\n</div>\n'
# Render template
rendered = Environment().from_string(source=template_code).render(**context)

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.0'
VERSION = '2.6.2'
# Hostname
HOSTNAME = platform.node()
@@ -346,7 +346,7 @@ else:
REDIS_CACHE_CON_STRING = 'redis://'
if REDIS_PASSWORD:
REDIS_CACHE_CON_STRING = '{}{}@'.format(REDIS_CACHE_CON_STRING, REDIS_PASSWORD)
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, REDIS_PASSWORD)
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_CACHE_DATABASE)

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

@@ -359,7 +359,7 @@
<td>{{ pp }}</td>
<td>{{ utilization.outlet_count }}</td>
<td>{{ utilization.allocated }}VA</td>
{% if powerfeed %}
{% if powerfeed.available_power %}
<td>{{ powerfeed.available_power }}VA</td>
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
{% else %}

View File

@@ -15,6 +15,9 @@
<td>
{% if cp.cable %}
<a href="{{ cp.cable.get_absolute_url }}">{{ cp.cable }}</a>
<a href="{% url 'dcim:consoleport_trace' pk=cp.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
{% else %}
&mdash;
{% endif %}

View File

@@ -23,6 +23,9 @@
<td>
{% if pp.cable %}
<a href="{{ pp.cable.get_absolute_url }}">{{ pp.cable }}</a>
<a href="{% url 'dcim:powerport_trace' pk=pp.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
{% else %}
&mdash;
{% endif %}

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,13 +3,14 @@ from django import forms
from django.conf import settings
from django.db import models
from dcim.forms import MACAddressField
from extras.models import Tag
def multivalue_field_factory(field_class):
"""
Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
filter values while maintaining the field's built-in vlaidation. Example: GET /api/dcim/devices/?name=foo&name=bar
filter values while maintaining the field's built-in validation. Example: GET /api/dcim/devices/?name=foo&name=bar
"""
class NewField(field_class):
widget = forms.SelectMultiple
@@ -17,7 +18,10 @@ def multivalue_field_factory(field_class):
def to_python(self, value):
if not value:
return []
return [super(field_class, self).to_python(v) for v in value]
return [
# Only append non-empty values (this avoids e.g. trying to cast '' as an integer)
super(field_class, self).to_python(v) for v in value if v
]
return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
@@ -46,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)..."