Skip to content

Release v2.10.9 #6143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c8eae3a
PRVB
jeremystretch Mar 26, 2021
6ec8ac7
Fixes #6073: Permit users to manage their own REST API tokens without…
jeremystretch Mar 31, 2021
b7309d5
Closes #6054: Display NAPALM-enabled device tabs only when relevant
jeremystretch Mar 31, 2021
f2f0ea8
Closes #5526: Add MAC address search field to VM interfaces list
jeremystretch Mar 31, 2021
6242e19
Closes #5756: Omit child devices from non-racked devices list under r…
jeremystretch Mar 31, 2021
9df2130
Closes #5840: Add column to cable termination objects to display cabl…
jeremystretch Mar 31, 2021
861a52d
Closes #5965: Mention cf property on CustomFieldModel in docs
jeremystretch Mar 31, 2021
7bd853e
Fixes #5805: Fix missing custom field filters for cables, rack reserv…
jeremystretch Mar 31, 2021
d42b069
Fix 'select all' widget
jeremystretch Apr 5, 2021
85d0270
Fixes #6099: Correct example permission description
jeremystretch Apr 7, 2021
ae3527d
Fixes #6081: Fix interface connections REST API endpoint
jeremystretch Apr 7, 2021
03b3f59
Fixes #6108: Do not infer tenant assignment from parent objects for p…
jeremystretch Apr 8, 2021
f096c4a
#6081: Tweak queryset filtering
jeremystretch Apr 8, 2021
e69251b
Fixes #6070: Add missing 'count_ipaddresses' attribute to VMInterface…
jeremystretch Apr 8, 2021
4dfba3a
Update export-templates.md (#6091)
tcaiazza Apr 9, 2021
2cc088c
Fixes #6131: Correct handling of boolean fields when cloning objects
jeremystretch Apr 9, 2021
701ad8a
Allow skipping TLS cert verification on Redis connection (#6084)
mraerino Apr 9, 2021
cc9b750
Changelog & docs for #6083
jeremystretch Apr 9, 2021
348fca7
Fixes #6117: Handle exception when attempting to assign an MPTT-enabl…
jeremystretch Apr 11, 2021
7ddcec3
Fixes #6082: Support colons in webhook header values
jeremystretch Apr 12, 2021
7c29fb4
Release v2.10.9
jeremystretch Apr 12, 2021
92fb43a
Merge branch 'master' into develop
jeremystretch Apr 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/additional-features/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Each custom selection field must have at least two choices. These are specified

If a default value is specified for a selection field, it must exactly match one of the provided choices.

## Custom Fields in Templates

Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).

For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`.

## Custom Fields and the REST API

When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined:
Expand Down
8 changes: 8 additions & 0 deletions docs/additional-features/export-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ Height: {{ rack.u_height }}U

To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.

If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
```
{% for server in queryset %}
{% set data = server.get_config_context() %}
{{ data.syslog }}
{% endfor %}
```

A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.

## Example
Expand Down
7 changes: 7 additions & 0 deletions docs/additional-features/napalm.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.

The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:

* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission

!!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.

Expand Down
2 changes: 1 addition & 1 deletion docs/administration/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
| ----------- | ----------- |
| `{"status": "active"}` | Status is active |
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing |
| `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing |
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/required-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
* `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis
* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended)

An example configuration is provided below:

Expand Down
23 changes: 23 additions & 0 deletions docs/release-notes/version-2.10.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# NetBox v2.10

## v2.10.9 (2021-04-12)

### Enhancements

* [#5526](https://github.com/netbox-community/netbox/issues/5526) - Add MAC address search field to VM interfaces list
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis

### Bug Fixes

* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint
* [#6082](https://github.com/netbox-community/netbox/issues/6082) - Support colons in webhook header values
* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses
* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent
* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects

---

## v2.10.8 (2021-03-26)

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ def get_path(self, obj):

class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
interface_b = NestedInterfaceSerializer(source='_path.destination')
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)

class Meta:
Expand Down
3 changes: 3 additions & 0 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import OrderedDict

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import F
from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404
Expand Down Expand Up @@ -580,6 +581,8 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.prefetch_related('device', '_path').filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
_path__destination_type__app_label='dcim',
_path__destination_type__model='interface',
_path__destination_id__isnull=False,
pk__lt=F('_path__destination_id')
)
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ class Meta:
nullable_fields = []


class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = RackReservation
field_order = ['q', 'region', 'site', 'group_id', 'user_id', 'tenant_group', 'tenant']
q = forms.CharField(
Expand Down Expand Up @@ -3966,7 +3966,7 @@ def clean(self):
})


class CableFilterForm(BootstrapMixin, forms.Form):
class CableFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cable
q = forms.CharField(
required=False,
Expand Down
8 changes: 4 additions & 4 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ def save(self, *args, **kwargs):

return super().save(*args, **kwargs)

@property
def count_ipaddresses(self):
return self.ip_addresses.count()


@extras_features('export_templates', 'webhooks', 'custom_links')
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
Expand Down Expand Up @@ -615,10 +619,6 @@ def is_wireless(self):
def is_lag(self):
return self.type == InterfaceTypeChoices.TYPE_LAG

@property
def count_ipaddresses(self):
return self.ip_addresses.count()


#
# Pass-through ports
Expand Down
6 changes: 6 additions & 0 deletions netbox/dcim/models/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ def to_objectchange(self, action):
def clean(self):
super().clean()

# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})

# Parent RackGroup (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
Expand Down
10 changes: 10 additions & 0 deletions netbox/dcim/models/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from dcim.choices import *
from dcim.constants import *
from django.core.exceptions import ValidationError
from dcim.fields import ASNField
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
Expand Down Expand Up @@ -87,6 +88,15 @@ def to_objectchange(self, action):
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
)

def clean(self):
super().clean()

# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})


#
# Sites
Expand Down
53 changes: 35 additions & 18 deletions netbox/dcim/tables/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ class CableTerminationTable(BaseTable):
cable = tables.Column(
linkify=True
)
cable_color = ColorColumn(
accessor='cable.color',
orderable=False,
verbose_name='Cable Color'
)
cable_peer = tables.TemplateColumn(
accessor='_cable_peer',
template_code=CABLETERMINATION,
Expand All @@ -255,7 +260,8 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')

Expand All @@ -274,7 +280,8 @@ class DeviceConsolePortTable(ConsolePortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsolePort
fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions'
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
row_attrs = {
Expand All @@ -289,7 +296,10 @@ class ConsoleServerPortTable(DeviceComponentTable, PathEndpointTable):

class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags')
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')


Expand All @@ -308,7 +318,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class Meta(DeviceComponentTable.Meta):
model = ConsoleServerPort
fields = (
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'connection', 'tags', 'actions'
'pk', 'name', 'label', 'type', 'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags',
'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'description', 'cable', 'connection', 'actions')
row_attrs = {
Expand All @@ -325,7 +336,7 @@ class Meta(DeviceComponentTable.Meta):
model = PowerPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable',
'cable_peer', 'connection', 'tags',
'cable_color', 'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')

Expand All @@ -345,8 +356,8 @@ class DevicePowerPortTable(PowerPortTable):
class Meta(DeviceComponentTable.Meta):
model = PowerPort
fields = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_peer',
'connection', 'tags', 'actions',
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
Expand All @@ -368,8 +379,8 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class Meta(DeviceComponentTable.Meta):
model = PowerOutlet
fields = (
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_peer',
'connection', 'tags',
'pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')

Expand All @@ -388,8 +399,8 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(DeviceComponentTable.Meta):
model = PowerOutlet
fields = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_peer', 'connection',
'tags', 'actions',
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_color',
'cable_peer', 'connection', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions',
Expand Down Expand Up @@ -424,7 +435,8 @@ class Meta(DeviceComponentTable.Meta):
model = Interface
fields = (
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
'description', 'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
'description', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan',
'tagged_vlans',
)
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')

Expand All @@ -450,7 +462,8 @@ class Meta(DeviceComponentTable.Meta):
model = Interface
fields = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'description',
'cable', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions',
'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable',
Expand All @@ -477,7 +490,7 @@ class Meta(DeviceComponentTable.Meta):
model = FrontPort
fields = (
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
'cable_peer', 'tags',
'cable_color', 'cable_peer', 'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')

Expand All @@ -497,8 +510,8 @@ class DeviceFrontPortTable(FrontPortTable):
class Meta(DeviceComponentTable.Meta):
model = FrontPort
fields = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
'tags', 'actions',
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_color',
'cable_peer', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'cable_peer',
Expand All @@ -516,7 +529,10 @@ class RearPortTable(DeviceComponentTable, CableTerminationTable):

class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags')
fields = (
'pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer',
'tags',
)
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')


Expand All @@ -535,7 +551,8 @@ class DeviceRearPortTable(RearPortTable):
class Meta(DeviceComponentTable.Meta):
model = RearPort
fields = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'tags', 'actions',
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_color', 'cable_peer', 'tags',
'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'actions',
Expand Down
3 changes: 1 addition & 2 deletions netbox/dcim/tables/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from dcim.models import PowerFeed, PowerPanel
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn
from .devices import CableTerminationTable
from .template_code import POWERFEED_CABLE, POWERFEED_CABLETERMINATION

__all__ = (
'PowerFeedTable',
Expand Down Expand Up @@ -69,7 +68,7 @@ class Meta(BaseTable.Meta):
model = PowerFeed
fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'cable', 'cable_peer', 'connection', 'available_power', 'tags',
'max_utilization', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power', 'tags',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
Expand Down
5 changes: 3 additions & 2 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,11 @@ class RackView(generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')

def get_extra_context(self, request, instance):
# Get 0U and child devices located within the rack
# Get 0U devices located within the rack
nonracked_devices = Device.objects.filter(
rack=instance,
position__isnull=True
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer')

peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def render_headers(self, context):
ret = {}
data = render_jinja2(self.additional_headers, context)
for line in data.splitlines():
header, value = line.split(':')
header, value = line.split(':', 1)
ret[header.strip()] = value.strip()
return ret

Expand Down
Loading