diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 79fb0e334..4a6dba734 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.0.10 + placeholder: v3.0.11 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 76944eecb..4c3ab0277 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.0.10 + placeholder: v3.0.11 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 184b5debb..f7fbe06ab 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,6 +1,10 @@ # NetBox v3.0 -## v3.0.11 (FUTURE) +## v3.0.12 (FUTURE) + +--- + +## v3.0.11 (2021-11-24) ### Enhancements @@ -14,6 +18,7 @@ ### Bug Fixes * [#7399](https://github.com/netbox-community/netbox/issues/7399) - Fix excessive CPU utilization when `AUTH_LDAP_FIND_GROUP_PERMS` is enabled +* [#7657](https://github.com/netbox-community/netbox/issues/7657) - Make change logging middleware thread-safe * [#7720](https://github.com/netbox-community/netbox/issues/7720) - Fix initialization of custom script MultiObjectVar field with multiple values * [#7729](https://github.com/netbox-community/netbox/issues/7729) - Fix permissions evaluation when displaying VLAN group VLANs table * [#7739](https://github.com/netbox-community/netbox/issues/7739) - Fix exception when tracing cable across circuit with no far end termination @@ -448,7 +453,7 @@ Note that NetBox's `rqworker` process will _not_ service custom queues by defaul * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths * [#6328](https://github.com/netbox-community/netbox/issues/6328) - Build and serve documentation locally -### Bug Fixes (from v3.2-beta2) +### Bug Fixes (from v3.0-beta2) * [#6977](https://github.com/netbox-community/netbox/issues/6977) - Truncate global search dropdown on small screens * [#6979](https://github.com/netbox-community/netbox/issues/6979) - Hide "create & add another" button for circuit terminations diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index b1ed5576d..cb378fba1 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -447,7 +447,7 @@ class PowerPortTypeChoices(ChoiceSet): )), ('International/ITA', ( (TYPE_ITA_C, 'ITA Type C (CEE 7/16)'), - (TYPE_ITA_E, 'ITA Type E (CEE 7/5)'), + (TYPE_ITA_E, 'ITA Type E (CEE 7/6)'), (TYPE_ITA_F, 'ITA Type F (CEE 7/4)'), (TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'), (TYPE_ITA_G, 'ITA Type G (BS 1363)'), @@ -659,8 +659,8 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_CS8464C, 'CS8464C'), )), ('ITA/International', ( - (TYPE_ITA_E, 'ITA Type E (CEE7/5)'), - (TYPE_ITA_F, 'ITA Type F (CEE7/3)'), + (TYPE_ITA_E, 'ITA Type E (CEE 7/5)'), + (TYPE_ITA_F, 'ITA Type F (CEE 7/3)'), (TYPE_ITA_G, 'ITA Type G (BS 1363)'), (TYPE_ITA_H, 'ITA Type H'), (TYPE_ITA_I, 'ITA Type I'), diff --git a/netbox/extras/context_managers.py b/netbox/extras/context_managers.py index 66b5ff94d..9f73fe9c3 100644 --- a/netbox/extras/context_managers.py +++ b/netbox/extras/context_managers.py @@ -2,8 +2,9 @@ from contextlib import contextmanager from django.db.models.signals import m2m_changed, pre_delete, post_save -from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object -from utilities.utils import curry +from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object +from netbox import thread_locals +from netbox.request_context import set_request from .webhooks import flush_webhooks @@ -15,12 +16,8 @@ def change_logging(request): :param request: WSGIRequest object with a unique `id` set """ - webhook_queue = [] - - # 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) + set_request(request) + thread_locals.webhook_queue = [] # Connect our receivers to the post_save and post_delete signals. post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object') @@ -38,5 +35,8 @@ def change_logging(request): clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue') # Flush queued webhooks to RQ - flush_webhooks(webhook_queue) - del webhook_queue + flush_webhooks(thread_locals.webhook_queue) + del thread_locals.webhook_queue + + # Clear the request from thread-local storage + set_request(None) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 77931f268..aff350cc4 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -7,13 +7,14 @@ from django.dispatch import receiver, Signal from django_prometheus.models import model_deletes, model_inserts, model_updates from extras.validators import CustomValidator +from netbox import thread_locals from netbox.config import get_config +from netbox.request_context import get_request from netbox.signals import post_clean from .choices import ObjectChangeActionChoices from .models import ConfigRevision, CustomField, ObjectChange from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook - # # Change logging/webhooks # @@ -22,10 +23,16 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook clear_webhooks = Signal() -def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): +def handle_changed_object(sender, instance, **kwargs): """ Fires when an object is created or updated. """ + if not hasattr(instance, 'to_objectchange'): + return + + request = get_request() + m2m_changed = False + def is_same_object(instance, webhook_data): return ( ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and @@ -33,11 +40,6 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): request.id == webhook_data['request_id'] ) - if not hasattr(instance, 'to_objectchange'): - return - - m2m_changed = False - # Determine the type of change being made if kwargs.get('created'): action = ObjectChangeActionChoices.ACTION_CREATE @@ -67,6 +69,7 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): objectchange.save() # If this is an M2M change, update the previously queued webhook (from post_save) + webhook_queue = thread_locals.webhook_queue if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]): instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments webhook_queue[-1]['data'] = serialize_for_webhook(instance) @@ -81,13 +84,15 @@ def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): model_updates.labels(instance._meta.model_name).inc() -def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs): +def handle_deleted_object(sender, instance, **kwargs): """ Fires when an object is deleted. """ if not hasattr(instance, 'to_objectchange'): return + request = get_request() + # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE) @@ -96,19 +101,21 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs): objectchange.save() # Enqueue webhooks + webhook_queue = thread_locals.webhook_queue enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE) # Increment metric counters model_deletes.labels(instance._meta.model_name).inc() -def _clear_webhook_queue(webhook_queue, sender, **kwargs): +def clear_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 = thread_locals.webhook_queue + logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})") webhook_queue.clear() diff --git a/netbox/netbox/__init__.py b/netbox/netbox/__init__.py index e69de29bb..5cf431025 100644 --- a/netbox/netbox/__init__.py +++ b/netbox/netbox/__init__.py @@ -0,0 +1,3 @@ +import threading + +thread_locals = threading.local() diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 959b6b525..cc768cbdc 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -1,10 +1,10 @@ +import logging import uuid from urllib import parse -import logging from django.conf import settings -from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_ from django.contrib import auth +from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_ from django.core.exceptions import ImproperlyConfigured from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect diff --git a/netbox/netbox/request_context.py b/netbox/netbox/request_context.py new file mode 100644 index 000000000..41e8283e8 --- /dev/null +++ b/netbox/netbox/request_context.py @@ -0,0 +1,9 @@ +from netbox import thread_locals + + +def set_request(request): + thread_locals.request = request + + +def get_request(): + return getattr(thread_locals, 'request', None) diff --git a/netbox/templates/extras/webhook.html b/netbox/templates/extras/webhook.html index 9aa103cb6..ab53c56a2 100644 --- a/netbox/templates/extras/webhook.html +++ b/netbox/templates/extras/webhook.html @@ -149,7 +149,11 @@ Additional Headers
-
{{ object.additional_headers }}
+ {% if object.additional_headers %} +
{{ object.additional_headers }}
+ {% else %} + None + {% endif %}
@@ -157,7 +161,11 @@ Body Template
-
{{ object.body_template }}
+ {% if object.body_template %} +
{{ object.body_template }}
+ {% else %} + None + {% endif %}
{% plugin_right_page object %} diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 1a81369fc..203c12b3f 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -327,13 +327,6 @@ def decode_dict(encoded_dict: Dict, *, decode_keys: bool = True) -> Dict: return {urllib.parse.unquote(k): decode_value(v, decode_keys) for k, v in encoded_dict.items()} -# Taken from django.utils.functional (<3.0) -def curry(_curried_func, *args, **kwargs): - def _curried(*moreargs, **morekwargs): - return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs}) - return _curried - - def array_to_string(array): """ Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField. diff --git a/requirements.txt b/requirements.txt index 43ea489b9..385370884 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ django-mptt==0.13.4 django-pglocks==1.0.4 django-prometheus==2.1.0 django-redis==5.0.0 -django-rq==2.4.1 +django-rq==2.5.1 django-tables2==2.4.1 django-taggit==1.5.1 django-timezone-field==4.2.1 @@ -16,7 +16,7 @@ drf-yasg[validation]==1.20.0 graphene_django==2.15.0 gunicorn==20.1.0 Jinja2==3.0.3 -Markdown==3.3.4 +Markdown==3.3.6 markdown-include==0.6.0 mkdocs-material==7.3.6 netaddr==0.8.0