jeremystretch 4 лет назад
Родитель
Сommit
fbf91dda7d

+ 2 - 3
netbox/dcim/api/views.py

@@ -1,7 +1,6 @@
 import socket
 from collections import OrderedDict
 
-from django.conf import settings
 from django.http import Http404, HttpResponse, HttpResponseForbidden
 from django.shortcuts import get_object_or_404
 from drf_yasg import openapi
@@ -21,7 +20,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.exceptions import ServiceUnavailable
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.views import ModelViewSet
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.api import get_serializer_for_model
 from utilities.utils import count_related, decode_dict
 from virtualization.models import VirtualMachine
@@ -459,7 +458,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
         napalm_methods = request.GET.getlist('method')
         response = OrderedDict([(m, None) for m in napalm_methods])
 
-        config = Config()
+        config = get_config()
         username = config.NAPALM_USERNAME
         password = config.NAPALM_PASSWORD
         timeout = config.NAPALM_TIMEOUT

+ 2 - 2
netbox/dcim/models/racks.py

@@ -14,7 +14,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.svg import RackElevationSVG
 from extras.utils import extras_features
-from netbox.config import Config
+from netbox.config import get_config
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
@@ -394,7 +394,7 @@ class Rack(PrimaryModel):
         """
         elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url)
         if unit_width is None or unit_height is None:
-            config = Config()
+            config = get_config()
             unit_width = unit_width or config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
             unit_height = unit_height or config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
 

+ 2 - 3
netbox/ipam/api/mixins.py

@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db import transaction
 from django.shortcuts import get_object_or_404
@@ -9,7 +8,7 @@ from rest_framework.decorators import action
 from rest_framework.response import Response
 
 from ipam.models import *
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.constants import ADVISORY_LOCK_KEYS
 from . import serializers
 
@@ -161,7 +160,7 @@ class AvailableIPsMixin:
 
         # Determine the maximum number of IPs to return
         else:
-            config = Config()
+            config = get_config()
             PAGINATE_COUNT = config.PAGINATE_COUNT
             MAX_PAGE_SIZE = config.MAX_PAGE_SIZE
             try:

+ 3 - 3
netbox/ipam/models/ip.py

@@ -16,7 +16,7 @@ from ipam.fields import IPNetworkField, IPAddressField
 from ipam.managers import IPAddressManager
 from ipam.querysets import PrefixQuerySet
 from ipam.validators import DNSValidator
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.querysets import RestrictedQuerySet
 from virtualization.models import VirtualMachine
 
@@ -316,7 +316,7 @@ class Prefix(PrimaryModel):
                 })
 
             # Enforce unique IP space (if applicable)
-            if (self.vrf is None and Config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
+            if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
                 duplicate_prefixes = self.get_duplicates()
                 if duplicate_prefixes:
                     raise ValidationError({
@@ -811,7 +811,7 @@ class IPAddress(PrimaryModel):
                 })
 
             # Enforce unique IP space (if applicable)
-            if (self.vrf is None and Config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
+            if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
                 duplicate_ips = self.get_duplicates()
                 if duplicate_ips and (
                         self.role not in IPADDRESS_ROLES_NONUNIQUE or

+ 3 - 4
netbox/netbox/api/pagination.py

@@ -1,8 +1,7 @@
-from django.conf import settings
 from django.db.models import QuerySet
 from rest_framework.pagination import LimitOffsetPagination
 
-from netbox.config import Config
+from netbox.config import get_config
 
 
 class OptionalLimitOffsetPagination(LimitOffsetPagination):
@@ -12,7 +11,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
     MAX_PAGE_SIZE has been set to 0 or None.
     """
     def __init__(self):
-        self.default_limit = Config().PAGINATE_COUNT
+        self.default_limit = get_config().PAGINATE_COUNT
 
     def paginate_queryset(self, queryset, request, view=None):
 
@@ -44,7 +43,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
                 if limit < 0:
                     raise ValueError()
                 # Enforce maximum page size, if defined
-                MAX_PAGE_SIZE = Config().MAX_PAGE_SIZE
+                MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
                 if MAX_PAGE_SIZE:
                     return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
                 return limit

+ 30 - 2
netbox/netbox/config/__init__.py

@@ -1,14 +1,41 @@
+import logging
+import threading
+
 from django.conf import settings
 from django.core.cache import cache
 
 from .parameters import PARAMS
 
 __all__ = (
-    'Config',
+    'clear_config',
     'ConfigItem',
+    'get_config',
     'PARAMS',
 )
 
+_thread_locals = threading.local()
+
+logger = logging.getLogger('netbox.config')
+
+
+def get_config():
+    """
+    Return the current NetBox configuration, pulling it from cache if not already loaded in memory.
+    """
+    if not hasattr(_thread_locals, 'config'):
+        _thread_locals.config = Config()
+        logger.debug("Initialized configuration")
+    return _thread_locals.config
+
+
+def clear_config():
+    """
+    Delete the currently loaded configuration, if any.
+    """
+    if hasattr(_thread_locals, 'config'):
+        del _thread_locals.config
+        logger.debug("Cleared configuration")
+
 
 class Config:
     """
@@ -19,6 +46,7 @@ class Config:
         self.config = cache.get('config')
         self.version = cache.get('config_version')
         self.defaults = {param.name: param.default for param in PARAMS}
+        logger.debug("Loaded configuration data from cache")
 
     def __getattr__(self, item):
 
@@ -46,5 +74,5 @@ class ConfigItem:
         self.item = item
 
     def __call__(self):
-        config = Config()
+        config = get_config()
         return getattr(config, self.item)

+ 2 - 2
netbox/netbox/context_processors.py

@@ -1,7 +1,7 @@
 from django.conf import settings as django_settings
 
 from extras.registry import registry
-from netbox.config import Config
+from netbox.config import get_config
 
 
 def settings_and_registry(request):
@@ -10,7 +10,7 @@ def settings_and_registry(request):
     """
     return {
         'settings': django_settings,
-        'config': Config(),
+        'config': get_config(),
         'registry': registry,
         'preferences': request.user.config if request.user.is_authenticated else {},
     }

+ 18 - 4
netbox/netbox/middleware.py

@@ -11,11 +11,12 @@ from django.http import Http404, HttpResponseRedirect
 from django.urls import reverse
 
 from extras.context_managers import change_logging
+from netbox.config import clear_config
 from netbox.views import server_error
 from utilities.api import is_api_request, rest_api_server_error
 
 
-class LoginRequiredMiddleware(object):
+class LoginRequiredMiddleware:
     """
     If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page.
     """
@@ -114,7 +115,7 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
         return groups
 
 
-class ObjectChangeMiddleware(object):
+class ObjectChangeMiddleware:
     """
     This middleware performs three functions in response to an object being created, updated, or deleted:
 
@@ -144,7 +145,7 @@ class ObjectChangeMiddleware(object):
         return response
 
 
-class APIVersionMiddleware(object):
+class APIVersionMiddleware:
     """
     If the request is for an API endpoint, include the API version as a response header.
     """
@@ -159,7 +160,20 @@ class APIVersionMiddleware(object):
         return response
 
 
-class ExceptionHandlingMiddleware(object):
+class DynamicConfigMiddleware:
+    """
+    Store the cached NetBox configuration in thread-local storage for the duration of the request.
+    """
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        response = self.get_response(request)
+        clear_config()
+        return response
+
+
+class ExceptionHandlingMiddleware:
     """
     Intercept certain exceptions which are likely indicative of installation issues and provide helpful instructions
     to the user.

+ 1 - 0
netbox/netbox/settings.py

@@ -335,6 +335,7 @@ MIDDLEWARE = [
     'netbox.middleware.ExceptionHandlingMiddleware',
     'netbox.middleware.RemoteUserMiddleware',
     'netbox.middleware.LoginRequiredMiddleware',
+    'netbox.middleware.DynamicConfigMiddleware',
     'netbox.middleware.APIVersionMiddleware',
     'netbox.middleware.ObjectChangeMiddleware',
     'django_prometheus.middleware.PrometheusAfterMiddleware',

+ 2 - 2
netbox/users/views.py

@@ -13,7 +13,7 @@ from django.utils.http import is_safe_url
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.generic import View
 
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.forms import ConfirmationForm
 from .forms import LoginForm, PasswordChangeForm, TokenForm
 from .models import Token
@@ -53,7 +53,7 @@ class LoginView(View):
 
             # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
             # last_login time upon authentication.
-            if Config().MAINTENANCE_MODE:
+            if get_config().MAINTENANCE_MODE:
                 logger.warning("Maintenance mode enabled: disabling update of most recent login time")
                 user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
 

+ 4 - 4
netbox/utilities/paginator.py

@@ -1,6 +1,6 @@
 from django.core.paginator import Paginator, Page
 
-from netbox.config import Config
+from netbox.config import get_config
 
 
 class EnhancedPaginator(Paginator):
@@ -14,9 +14,9 @@ class EnhancedPaginator(Paginator):
         try:
             per_page = int(per_page)
             if per_page < 1:
-                per_page = Config().PAGINATE_COUNT
+                per_page = get_config().PAGINATE_COUNT
         except ValueError:
-            per_page = Config().PAGINATE_COUNT
+            per_page = get_config().PAGINATE_COUNT
 
         # Set orphans count based on page size
         if orphans is None and per_page <= 50:
@@ -66,7 +66,7 @@ def get_paginate_count(request):
 
     Return the lesser of the calculated value and MAX_PAGE_SIZE.
     """
-    config = Config()
+    config = get_config()
 
     if 'per_page' in request.GET:
         try:

+ 2 - 2
netbox/utilities/templatetags/helpers.py

@@ -14,7 +14,7 @@ from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from markdown import markdown
 
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.forms import get_selected_values, TableConfigForm
 from utilities.utils import foreground_color
 
@@ -45,7 +45,7 @@ def render_markdown(value):
     value = strip_tags(value)
 
     # Sanitize Markdown links
-    schemes = '|'.join(Config().ALLOWED_URL_SCHEMES)
+    schemes = '|'.join(get_config().ALLOWED_URL_SCHEMES)
     pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)'
     value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
 

+ 2 - 2
netbox/utilities/tests/test_api.py

@@ -9,7 +9,7 @@ from dcim.models import Region, Site
 from extras.choices import CustomFieldTypeChoices
 from extras.models import CustomField
 from ipam.models import VLAN
-from netbox.config import Config
+from netbox.config import get_config
 from utilities.testing import APITestCase, disable_warnings
 
 
@@ -137,7 +137,7 @@ class APIPaginationTestCase(APITestCase):
 
     def test_default_page_size(self):
         response = self.client.get(self.url, format='json', **self.header)
-        page_size = Config().PAGINATE_COUNT
+        page_size = get_config().PAGINATE_COUNT
         self.assertLess(page_size, 100, "Default page size not sufficient for data set")
 
         self.assertHttpStatus(response, status.HTTP_200_OK)

+ 2 - 2
netbox/utilities/validators.py

@@ -3,7 +3,7 @@ import re
 from django.core.exceptions import ValidationError
 from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
 
-from netbox.config import Config
+from netbox.config import get_config
 
 
 class EnhancedURLValidator(URLValidator):
@@ -24,7 +24,7 @@ class EnhancedURLValidator(URLValidator):
     def __init__(self, schemes=None, **kwargs):
         super().__init__(**kwargs)
         if schemes is not None:
-            self.schemes = Config().ALLOWED_URL_SCHEMES
+            self.schemes = get_config().ALLOWED_URL_SCHEMES
 
 
 class ExclusionValidator(BaseValidator):

+ 2 - 2
netbox/virtualization/models.py

@@ -8,7 +8,7 @@ from dcim.models import BaseInterface, Device
 from extras.models import ConfigContextModel
 from extras.querysets import ConfigContextModelQuerySet
 from extras.utils import extras_features
-from netbox.config import Config
+from netbox.config import get_config
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.fields import NaturalOrderingField
 from utilities.ordering import naturalize_interface
@@ -340,7 +340,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
 
     @property
     def primary_ip(self):
-        if Config().PREFER_IPV4 and self.primary_ip4:
+        if get_config().PREFER_IPV4 and self.primary_ip4:
             return self.primary_ip4
         elif self.primary_ip6:
             return self.primary_ip6