Merge pull request #7815 from netbox-community/develop

Release v3.0.10
This commit is contained in:
Jeremy Stretch 2021-11-12 08:50:43 -05:00 committed by GitHub
commit b29a5511df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 155 additions and 78 deletions

View File

@ -13,11 +13,8 @@ body:
- type: input
attributes:
label: NetBox version
description: >
What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v3.0.9
description: What version of NetBox are you currently running?
placeholder: v3.0.10
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.0.9
placeholder: v3.0.10
validations:
required: true
- type: dropdown

View File

@ -76,14 +76,10 @@ free to add a comment with any additional justification for the feature.
(However, note that comments with no substance other than a "+1" will be
deleted. Please use GitHub's reactions feature to indicate your support.)
* Due to a large backlog of feature requests, we are not currently accepting
any proposals which substantially extend NetBox's functionality beyond its
current feature set. This includes the introduction of any new views or models
which have not already been proposed in an existing feature request.
* Before filing a new feature request, consider raising your idea on the
mailing list first. Feedback you receive there will help validate and shape the
proposed feature before filing a formal issue.
* Before filing a new feature request, consider raising your idea in a
[GitHub discussion](https://github.com/netbox-community/netbox/discussions)
first. Feedback you receive there will help validate and shape the proposed
feature before filing a formal issue.
* Good feature requests are very narrowly defined. Be sure to thoroughly
describe the functionality and data model(s) being proposed. The more effort

View File

@ -27,3 +27,13 @@ Device components represent discrete objects within a device which are used to t
---
{!models/dcim/cable.md!}
In the example below, three individual cables comprise a path between devices A and D:
![Cable path](../media/models/dcim_cable_trace.png)
Traced from Interface 1 on Device A, NetBox will show the following path:
* Cable 1: Interface 1 to Front Port 1
* Cable 2: Rear Port 1 to Rear Port 2
* Cable 3: Front Port 2 to Interface 2

View File

@ -21,9 +21,6 @@ This section entails the installation and configuration of a local PostgreSQL da
sudo postgresql-setup --initdb
```
!!! info
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
```no-highlight

View File

@ -17,8 +17,13 @@ Begin by installing all system packages required by NetBox and its dependencies.
=== "CentOS"
!!! warning
CentOS 8 does not provide Python 3.7 or later via its native package manager. You will need to install it via some other means. [Here is an example](https://tecadmin.net/install-python-3-7-on-centos-8/) of installing Python 3.7 from source.
Once you have Python 3.7 or later installed, install the remaining system packages:
```no-highlight
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
sudo yum install -y gcc libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
```
Before continuing with either platform, update pip (Python's package management tool) to its latest release:

View File

@ -1,6 +1,6 @@
# Installation
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.2. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
The following sections detail how to set up a new instance of NetBox:

View File

@ -22,13 +22,3 @@ Each cable may be assigned a type, label, length, and color. Each cable is also
## Tracing Cables
A cable may be traced from either of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user.
In the example below, three individual cables comprise a path between devices A and D:
![Cable path](../media/models/dcim_cable_trace.png)
Traced from Interface 1 on Device A, NetBox will show the following path:
* Cable 1: Interface 1 to Front Port 1
* Cable 2: Rear Port 1 to Rear Port 2
* Cable 3: Front Port 2 to Interface 2

View File

@ -1,5 +1,31 @@
# NetBox v3.0
## v3.0.10 (2021-11-12)
### Enhancements
* [#7740](https://github.com/netbox-community/netbox/issues/7740) - Add mini-DIN 8 console port type
* [#7760](https://github.com/netbox-community/netbox/issues/7760) - Add `vid` filter field to VLANs list
* [#7767](https://github.com/netbox-community/netbox/issues/7767) - Add visual aids to interfaces table for type, enabled status
### Bug Fixes
* [#7564](https://github.com/netbox-community/netbox/issues/7564) - Fix assignment of members to virtual chassis with initial position of zero
* [#7701](https://github.com/netbox-community/netbox/issues/7701) - Fix conflation of assigned IP status & role in interface tables
* [#7741](https://github.com/netbox-community/netbox/issues/7741) - Fix 404 when attaching multiple images in succession
* [#7752](https://github.com/netbox-community/netbox/issues/7752) - Fix minimum version check under Python v3.10
* [#7766](https://github.com/netbox-community/netbox/issues/7766) - Add missing outer dimension columns to rack table
* [#7780](https://github.com/netbox-community/netbox/issues/7780) - Preserve multi-line values during CSV file import
* [#7783](https://github.com/netbox-community/netbox/issues/7783) - Fix indentation of locations under site view
* [#7788](https://github.com/netbox-community/netbox/issues/7788) - Improve XSS mitigation in Markdown renderer
* [#7791](https://github.com/netbox-community/netbox/issues/7791) - Enable sorting device bays table by installed device status
* [#7802](https://github.com/netbox-community/netbox/issues/7802) - Differentiate ID and VID columns in VLANs table
* [#7808](https://github.com/netbox-community/netbox/issues/7808) - Fix reference values for content type under custom field import form
* [#7809](https://github.com/netbox-community/netbox/issues/7809) - Add missing export template support for various models
* [#7814](https://github.com/netbox-community/netbox/issues/7814) - Fix restriction of user & group objects in GraphQL API queries
---
## v3.0.9 (2021-11-03)
### Enhancements

View File

@ -185,6 +185,7 @@ class ConsolePortTypeChoices(ChoiceSet):
TYPE_RJ11 = 'rj-11'
TYPE_RJ12 = 'rj-12'
TYPE_RJ45 = 'rj-45'
TYPE_MINI_DIN_8 = 'mini-din-8'
TYPE_USB_A = 'usb-a'
TYPE_USB_B = 'usb-b'
TYPE_USB_C = 'usb-c'
@ -202,6 +203,7 @@ class ConsolePortTypeChoices(ChoiceSet):
(TYPE_RJ11, 'RJ-11'),
(TYPE_RJ12, 'RJ-12'),
(TYPE_RJ45, 'RJ-45'),
(TYPE_MINI_DIN_8, 'Mini-DIN 8'),
)),
('USB', (
(TYPE_USB_A, 'USB Type A'),

View File

@ -117,12 +117,18 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
]
def clean(self):
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({
'initial_position': "A position must be specified for the first VC member."
})
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Assign VC members
if instance.pk:
initial_position = self.cleaned_data.get('initial_position') or 1
if instance.pk and self.cleaned_data['members']:
initial_position = self.cleaned_data.get('initial_position', 1)
for i, member in enumerate(self.cleaned_data['members'], start=initial_position):
member.virtual_chassis = instance
member.vc_position = i

View File

@ -53,6 +53,14 @@ def get_cabletermination_row_class(record):
return ''
def get_interface_row_class(record):
if not record.enabled:
return 'danger'
elif record.is_virtual:
return 'primary'
return get_cabletermination_row_class(record)
def get_interface_state_attribute(record):
"""
Get interface enabled state as string to attach to <tr/> DOM element.
@ -501,8 +509,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable
class DeviceInterfaceTable(InterfaceTable):
name = tables.TemplateColumn(
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}reorder-horizontal'
'{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet'
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
@ -534,7 +542,7 @@ class DeviceInterfaceTable(InterfaceTable):
'cable', 'connection', 'actions',
)
row_attrs = {
'class': get_cabletermination_row_class,
'class': get_interface_row_class,
'data-name': lambda record: record.name,
'data-enabled': get_interface_state_attribute,
}
@ -653,7 +661,8 @@ class DeviceBayTable(DeviceComponentTable):
}
)
status = tables.TemplateColumn(
template_code=DEVICEBAY_STATUS
template_code=DEVICEBAY_STATUS,
order_by=Accessor('installed_device__status')
)
installed_device = tables.Column(
linkify=True

View File

@ -72,12 +72,20 @@ class RackTable(BaseTable):
tags = TagColumn(
url_name='dcim:rack_list'
)
outer_width = tables.TemplateColumn(
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
verbose_name='Outer Width'
)
outer_depth = tables.TemplateColumn(
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
verbose_name='Outer Depth'
)
class Meta(BaseTable.Meta):
model = Rack
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type',
'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',

View File

@ -40,17 +40,13 @@ DEVICEBAY_STATUS = """
INTERFACE_IPADDRESSES = """
<div class="table-badge-group">
{% for ip in record.ip_addresses.all %}
<a
class="table-badge{% if ip.status != 'active' %} badge bg-{{ ip.get_status_class }}{% elif ip.role %} badge bg-{{ ip.get_role_class }}{% endif %}"
href="{{ ip.get_absolute_url }}"
{% if ip.status != 'active'%}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}"
{% elif ip.role %}data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_role_display }}"
{% endif %}
>
{{ ip }}
</a>
{% endfor %}
{% for ip in record.ip_addresses.all %}
{% if ip.status != 'active' %}
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_class }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
{% else %}
<a href="{{ ip.get_absolute_url }}" class="table-badge">{{ ip }}</a>
{% endif %}
{% endfor %}
</div>
"""

View File

@ -70,7 +70,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links')
limit_choices_to=FeatureQuery('export_templates')
)
class Meta:

View File

@ -31,7 +31,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
return self.get_queryset().filter(content_types=content_type)
@extras_features('webhooks')
@extras_features('webhooks', 'export_templates')
class CustomField(ChangeLoggedModel):
content_types = models.ManyToManyField(
to=ContentType,

View File

@ -9,7 +9,7 @@ from django.db import models
from django.http import HttpResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format, time_format
from django.utils.formats import date_format
from rest_framework.utils.encoders import JSONEncoder
from extras.choices import *
@ -36,7 +36,7 @@ __all__ = (
# Webhooks
#
@extras_features('webhooks')
@extras_features('webhooks', 'export_templates')
class Webhook(ChangeLoggedModel):
"""
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
@ -175,7 +175,7 @@ class Webhook(ChangeLoggedModel):
# Custom links
#
@extras_features('webhooks')
@extras_features('webhooks', 'export_templates')
class CustomLink(ChangeLoggedModel):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
@ -234,7 +234,7 @@ class CustomLink(ChangeLoggedModel):
# Export templates
#
@extras_features('webhooks')
@extras_features('webhooks', 'export_templates')
class ExportTemplate(ChangeLoggedModel):
content_type = models.ForeignKey(
to=ContentType,
@ -357,6 +357,8 @@ class ImageAttachment(BigIDModel):
objects = RestrictedQuerySet.as_manager()
clone_fields = ('content_type', 'object_id')
class Meta:
ordering = ('name', 'pk') # name may be non-unique

View File

@ -14,7 +14,7 @@ from utilities.querysets import RestrictedQuerySet
# Tags
#
@extras_features('webhooks')
@extras_features('webhooks', 'export_templates')
class Tag(ChangeLoggedModel, TagBase):
color = ColorField(
default=ColorChoices.COLOR_GREY

View File

@ -475,11 +475,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
def alter_obj(self, instance, request, args, kwargs):
if not instance.pk:
# Assign the parent object based on URL kwargs
try:
app_label, model = request.GET.get('content_type').split('.')
except (AttributeError, ValueError):
raise Http404("Content type not specified")
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
return instance

View File

@ -1,3 +1,4 @@
import django_filters
from django import forms
from django.utils.translation import gettext as _
@ -409,7 +410,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
field_groups = [
['q', 'tag'],
['region_id', 'site_group_id', 'site_id'],
['group_id', 'status', 'role_id'],
['group_id', 'status', 'role_id', 'vid'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
@ -461,6 +462,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
label=_('Role'),
fetch_trigger='open'
)
vid = forms.IntegerField(
required=False,
label='VLAN ID'
)
tag = TagFilterField(model)

View File

@ -93,7 +93,7 @@ class VLANTable(BaseTable):
pk = ToggleColumn()
vid = tables.TemplateColumn(
template_code=VLAN_LINK,
verbose_name='ID'
verbose_name='VID'
)
site = tables.Column(
linkify=True

View File

@ -4,6 +4,7 @@ import os
import platform
import re
import socket
import sys
import warnings
from urllib.parse import urlsplit
@ -16,7 +17,7 @@ from django.core.validators import URLValidator
# Environment setup
#
VERSION = '3.0.9'
VERSION = '3.0.10'
# Hostname
HOSTNAME = platform.node()
@ -25,7 +26,7 @@ HOSTNAME = platform.node()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Validate Python version
if platform.python_version_tuple() < ('3', '7'):
if sys.version_info < (3, 7):
raise RuntimeError(
f"NetBox requires Python 3.7 or higher (current: Python {platform.python_version()})"
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -814,7 +814,7 @@ table .table-badge-group {
}
&.badge:not(:last-of-type):not(:only-child) {
margin-bottom: map.get($spacers, 2);
margin-bottom: map.get($spacers, 1);
}
}
}

View File

@ -219,8 +219,8 @@
</tr>
{% for location in locations %}
<tr>
<td style="padding-left: {{ location.level }}8px">
<i class="mdi mdi-folder-open"></i>
<td>
{% for i in location.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
<a href="{{ location.get_absolute_url }}">{{ location }}</a>
</td>
<td>

View File

@ -61,7 +61,7 @@
<a href="{{ vc_member.get_absolute_url }}">{{ vc_member }}</a>
</td>
<td>
{% badge vc_member.vc_position %}
{% badge vc_member.vc_position show_empty=True %}
</td>
<td>
{% if object.master == vc_member %}

View File

@ -44,7 +44,7 @@
</div>
{% if perms.extras.add_imageattachment %}
<div class="card-footer text-end noprint">
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
<a href="{% url 'extras:imageattachment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Attach an image
</a>
</div>

View File

@ -19,7 +19,7 @@ class GroupType(DjangoObjectType):
@classmethod
def get_queryset(cls, queryset, info):
return RestrictedQuerySet(model=Group)
return RestrictedQuerySet(model=Group).restrict(info.context.user, 'view')
class UserType(DjangoObjectType):
@ -34,4 +34,4 @@ class UserType(DjangoObjectType):
@classmethod
def get_queryset(cls, queryset, info):
return RestrictedQuerySet(model=User)
return RestrictedQuerySet(model=User).restrict(info.context.user, 'view')

View File

@ -224,7 +224,7 @@ class CSVFileField(forms.FileField):
return None
csv_str = file.read().decode('utf-8').strip()
reader = csv.reader(csv_str.splitlines())
reader = csv.reader(StringIO(csv_str))
headers, records = parse_csv(reader)
return headers, records
@ -304,7 +304,7 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
app_label, model = name.split('.')
ct_filter |= Q(app_label=app_label, model=model)
return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True))
return super().prepare_value(value)
return f'{value.app_label}.{value.model}'
#

View File

@ -6,6 +6,7 @@ from typing import Dict, Any
import yaml
from django import template
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import date
from django.urls import NoReverseMatch, reverse
from django.utils import timezone
@ -39,14 +40,19 @@ def render_markdown(value):
"""
Render text as Markdown
"""
schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
# Strip HTML tags
value = strip_tags(value)
# Sanitize Markdown links
schemes = '|'.join(settings.ALLOWED_URL_SCHEMES)
pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)'
pattern = fr'\[([^\]]+)\]\((?!({schemes})).*:(.+)\)'
value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
# Sanitize Markdown reference links
pattern = fr'\[(.+)\]:\w?(?!({schemes})).*:(.+)'
value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE)
# Render Markdown
html = markdown(value, extensions=['fenced_code', 'tables'])
@ -78,6 +84,25 @@ def meta(obj, attr):
return getattr(obj._meta, attr, '')
@register.filter()
def content_type(obj):
"""
Return the ContentType for the given object.
"""
return ContentType.objects.get_for_model(obj)
@register.filter()
def content_type_id(obj):
"""
Return the ContentType ID for the given object.
"""
content_type = ContentType.objects.get_for_model(obj)
if content_type:
return content_type.pk
return None
@register.filter()
def viewname(model, action):
"""

View File

@ -345,7 +345,7 @@ class APIViewTestCases:
obj_perm.users.add(self.user)
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
id_list = self._get_queryset().values_list('id', flat=True)[:3]
id_list = list(self._get_queryset().values_list('id', flat=True)[:3])
self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk update")
data = [
{'id': id, **self.bulk_update_data} for id in id_list
@ -416,7 +416,7 @@ class APIViewTestCases:
# Target the three most recently created objects to avoid triggering recursive deletions
# (e.g. with MPTT objects)
id_list = self._get_queryset().order_by('-id').values_list('id', flat=True)[:3]
id_list = list(self._get_queryset().order_by('-id').values_list('id', flat=True)[:3])
self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk deletion")
data = [{"id": id} for id in id_list]

View File

@ -15,13 +15,13 @@ djangorestframework==3.12.4
drf-yasg[validation]==1.20.0
graphene_django==2.15.0
gunicorn==20.1.0
Jinja2==3.0.2
Jinja2==3.0.3
Markdown==3.3.4
markdown-include==0.6.0
mkdocs-material==7.3.6
netaddr==0.8.0
Pillow==8.4.0
psycopg2-binary==2.9.1
psycopg2-binary==2.9.2
PyYAML==6.0
svgwrite==1.4.1
tablib==3.1.0

View File

@ -11,6 +11,7 @@ exec 1>&2
EXIT=0
RED='\033[0;31m'
YELLOW='\033[0;33m'
NOCOLOR='\033[0m'
if [ -d ./venv/ ]; then
@ -22,6 +23,11 @@ if [ -d ./venv/ ]; then
fi
fi
if [ ${NOVALIDATE} ]; then
echo "${YELLOW}Skipping validation checks${NOCOLOR}"
exit $EXIT
fi
echo "Validating PEP8 compliance..."
pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
if [ $? != 0 ]; then