Skip to content

Release v2.11.12 #7018

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 19 commits into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ffae2c5
Fixes #6632
Jul 22, 2021
1b12185
PRVB
jeremystretch Aug 12, 2021
fce4195
Closes #6748: Add site group filter to devices list
jeremystretch Aug 13, 2021
3feba29
Closes #6872: Add table configuration button to child prefixes view
jeremystretch Aug 13, 2021
5a8cedd
Add hardwired PowerOutlet
bluikko Aug 16, 2021
5b89cdc
Fixes #5968: Model forms should save empty custom field values as null
jeremystretch Aug 16, 2021
9b0258f
Fixes #6686: Force assignment of null custom field values to objects
jeremystretch Aug 16, 2021
10847e2
Optimize addition/removal of default custom field values
jeremystretch Aug 16, 2021
9baebfa
Merge pull request #6790 from WillIrvine/issue-6632
jeremystretch Aug 20, 2021
d850aa0
Changelog for #6790
jeremystretch Aug 20, 2021
53a5bc2
Fixes #6929: Introduce LOGIN_PERSISTENCE configuration parameter to p…
jeremystretch Aug 20, 2021
1fc3c6d
Fixes #6974: Show contextual label for IP address role
jeremystretch Aug 20, 2021
8131fea
Closes #7011: Add search field to VM interfaces filter form
jeremystretch Aug 23, 2021
cfa4f56
Fixes #7012: Fix hidden "add components" dropdown on devices list
jeremystretch Aug 23, 2021
aef8c5f
Merge pull request #6965 from bluikko/poweroutlet-hardwired
jeremystretch Aug 23, 2021
75c62ff
Print request index after webhook data dump
jeremystretch Aug 23, 2021
0b0ab92
Fixes #6776: Fix erroneous webhook dispatch on failure to save objects
jeremystretch Aug 23, 2021
8497965
Fixes #6326: Enable filtering assigned VLANs by group in interface ed…
jeremystretch Aug 23, 2021
6518d87
Release v2.11.12
jeremystretch Aug 23, 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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ body:
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: v2.11.11
placeholder: v2.11.12
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v2.11.11
placeholder: v2.11.12
validations:
required: true
- type: dropdown
Expand Down
10 changes: 10 additions & 0 deletions docs/configuration/optional-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,16 @@ LOGGING = {

---

## LOGIN_PERSISTENCE

Default: False

If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.

Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.

---

## LOGIN_REQUIRED

Default: False
Expand Down
21 changes: 21 additions & 0 deletions docs/release-notes/version-2.11.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# NetBox v2.11

## v2.11.12 (2021-08-23)

### Enhancements

* [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list
* [#6790](https://github.com/netbox-community/netbox/issues/6790) - Recognize a /32 IPv4 address as a child of a /32 IPv4 prefix
* [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view
* [#6929](https://github.com/netbox-community/netbox/issues/6929) - Introduce `LOGIN_PERSISTENCE` configuration parameter to persist user sessions
* [#7011](https://github.com/netbox-community/netbox/issues/7011) - Add search field to VM interfaces filter form

### Bug Fixes

* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
* [#6326](https://github.com/netbox-community/netbox/issues/6326) - Enable filtering assigned VLANs by group in interface edit form
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list

---

## v2.11.11 (2021-08-12)

### Enhancements
Expand Down
5 changes: 5 additions & 0 deletions netbox/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,8 @@ class PowerOutletTypeChoices(ChoiceSet):
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'

CHOICES = (
('IEC 60320', (
Expand Down Expand Up @@ -654,6 +656,9 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)),
)


Expand Down
29 changes: 23 additions & 6 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from extras.models import Tag
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from ipam.models import IPAddress, VLAN
from ipam.models import IPAddress, VLAN, VLANGroup
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
Expand Down Expand Up @@ -2421,8 +2421,8 @@ class Meta:
class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
model = Device
field_order = [
'q', 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id',
'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
]
q = forms.CharField(
required=False,
Expand All @@ -2433,11 +2433,17 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
Expand Down Expand Up @@ -3103,15 +3109,26 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
'type': 'lag',
}
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Untagged VLAN'
label='Untagged VLAN',
query_params={
'group_id': '$vlan_group',
}
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
label='Tagged VLANs'
label='Tagged VLANs',
query_params={
'group_id': '$vlan_group',
}
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/context_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.db.models.signals import m2m_changed, pre_delete, post_save

from extras.signals import _handle_changed_object, _handle_deleted_object
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
from utilities.utils import curry
from .webhooks import flush_webhooks

Expand All @@ -20,11 +20,13 @@ def change_logging(request):
# Curry signals receivers to pass the current request
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)

# Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')

yield

Expand All @@ -33,6 +35,7 @@ def change_logging(request):
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')

# Flush queued webhooks to RQ
flush_webhooks(webhook_queue)
Expand Down
6 changes: 5 additions & 1 deletion netbox/extras/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ def clean(self):

# Save custom field data on instance
for cf_name in self.custom_fields:
self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
key = cf_name[3:] # Strip "cf_" from field name
value = self.cleaned_data.get(cf_name)
empty_values = self.fields[cf_name].empty_values
# Convert "empty" values to null
self.instance.custom_field_data[key] = value if value not in empty_values else None

return super().clean()

Expand Down
9 changes: 5 additions & 4 deletions netbox/extras/management/commands/webhook_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ def do_ANY(self):
self.end_headers()
self.wfile.write(b'Webhook received!\n')

request_counter += 1

# Print the request headers to stdout
# Print the request headers
if self.show_headers:
for k, v in self.headers.items():
print('{}: {}'.format(k, v))
print(f'{k}: {v}')
print()

# Print the request body (if any)
Expand All @@ -55,8 +53,11 @@ def do_ANY(self):
else:
print('(No body)')

print(f'Completed request #{request_counter}')
print('------------')

request_counter += 1


class Command(BaseCommand):
help = "Start a simple listener to display received HTTP requests"
Expand Down
31 changes: 22 additions & 9 deletions netbox/extras/models/customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,16 @@ def __init__(self, *args, **kwargs):
# Cache instance's original name so we can check later whether it has changed
self._name = self.name

def rename_object_data(self, old_name, new_name):
def populate_initial_data(self, content_types):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
Populate initial custom field data upon either a) the creation of a new CustomField, or
b) the assignment of an existing CustomField to new object types.
"""
for ct in self.content_types.all():
for ct in content_types:
model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params)
instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
for instance in instances:
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
instance.custom_field_data[self.name] = self.default
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)

def remove_stale_data(self, content_types):
Expand All @@ -139,9 +139,22 @@ def remove_stale_data(self, content_types):
"""
for ct in content_types:
model = ct.model_class()
for obj in model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False}):
del(obj.custom_field_data[self.name])
obj.save()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
for instance in instances:
del(instance.custom_field_data[self.name])
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)

def rename_object_data(self, old_name, new_name):
"""
Called when a CustomField has been renamed. Updates all assigned object data.
"""
for ct in self.content_types.all():
model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params)
for instance in instances:
instance.custom_field_data[new_name] = instance.custom_field_data.pop(old_name)
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)

def clean(self):
super().clean()
Expand Down
27 changes: 26 additions & 1 deletion netbox/extras/signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import random
from datetime import timedelta

Expand All @@ -6,6 +7,7 @@
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import Signal
from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter
Expand All @@ -19,6 +21,10 @@
# Change logging/webhooks
#

# Define a custom signal that can be sent to clear any queued webhooks
clear_webhooks = Signal()


def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
"""
Fires when an object is created or updated.
Expand Down Expand Up @@ -104,10 +110,28 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
model_deletes.labels(instance._meta.model_name).inc()


def _clear_webhook_queue(webhook_queue, sender, **kwargs):
"""
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
"""
logger = logging.getLogger('webhooks')
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")

webhook_queue.clear()


#
# Custom fields
#

def handle_cf_added_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the population of default/null values when a CustomField is added to one or more ContentTypes.
"""
if action == 'post_add':
instance.populate_initial_data(ContentType.objects.filter(pk__in=pk_set))


def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
"""
Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
Expand All @@ -131,9 +155,10 @@ def handle_cf_deleted(instance, **kwargs):
instance.remove_stale_data(instance.content_types.all())


m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField)
m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through)
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)


#
Expand Down
10 changes: 8 additions & 2 deletions netbox/extras/tests/test_customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ def test_simple_fields(self):
cf.save()
cf.content_types.set([obj_type])

# Assign a value to the first Site
# Check that the field has a null initial value
site = Site.objects.first()
self.assertIsNone(site.custom_field_data[cf.name])

# Assign a value to the first Site
site.custom_field_data[cf.name] = data['field_value']
site.save()

Expand Down Expand Up @@ -73,8 +76,11 @@ def test_select_field(self):
cf.save()
cf.content_types.set([obj_type])

# Assign a value to the first Site
# Check that the field has a null initial value
site = Site.objects.first()
self.assertIsNone(site.custom_field_data[cf.name])

# Assign a value to the first Site
site.custom_field_data[cf.name] = 'Option A'
site.save()

Expand Down
Loading