Optimize config queries

This commit is contained in:
jeremystretch 2021-10-26 13:41:56 -04:00
parent 41ff1d0fc9
commit fbf91dda7d
15 changed files with 77 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,41 @@
import logging
import threading
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from .parameters import PARAMS from .parameters import PARAMS
__all__ = ( __all__ = (
'Config', 'clear_config',
'ConfigItem', 'ConfigItem',
'get_config',
'PARAMS', '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: class Config:
""" """
@ -19,6 +46,7 @@ class Config:
self.config = cache.get('config') self.config = cache.get('config')
self.version = cache.get('config_version') self.version = cache.get('config_version')
self.defaults = {param.name: param.default for param in PARAMS} self.defaults = {param.name: param.default for param in PARAMS}
logger.debug("Loaded configuration data from cache")
def __getattr__(self, item): def __getattr__(self, item):
@ -46,5 +74,5 @@ class ConfigItem:
self.item = item self.item = item
def __call__(self): def __call__(self):
config = Config() config = get_config()
return getattr(config, self.item) return getattr(config, self.item)

View File

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

View File

@ -11,11 +11,12 @@ from django.http import Http404, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from extras.context_managers import change_logging from extras.context_managers import change_logging
from netbox.config import clear_config
from netbox.views import server_error from netbox.views import server_error
from utilities.api import is_api_request, rest_api_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. If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page.
""" """
@ -114,7 +115,7 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
return groups return groups
class ObjectChangeMiddleware(object): class ObjectChangeMiddleware:
""" """
This middleware performs three functions in response to an object being created, updated, or deleted: This middleware performs three functions in response to an object being created, updated, or deleted:
@ -144,7 +145,7 @@ class ObjectChangeMiddleware(object):
return response return response
class APIVersionMiddleware(object): class APIVersionMiddleware:
""" """
If the request is for an API endpoint, include the API version as a response header. 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 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 Intercept certain exceptions which are likely indicative of installation issues and provide helpful instructions
to the user. to the user.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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