Coverage for netbox/config/__init__.py: 81%
68 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import logging
2import threading
4from django.conf import settings
5from django.core.cache import cache
6from django.db.utils import DatabaseError
7from django.utils.translation import gettext_lazy as _
9from .parameters import PARAMS
11__all__ = (
12 'PARAMS',
13 'Config',
14 'ConfigItem',
15 'clear_config',
16 'get_config',
17)
19_thread_locals = threading.local()
21logger = logging.getLogger('netbox.config')
23# Sentinel used to distinguish a cache miss from a cached "empty" config (an empty dict is a
24# legitimate cached value when no ConfigRevision exists).
25_MISSING = object()
28def get_config():
29 """
30 Return the current NetBox configuration, pulling it from cache if not already loaded in memory.
31 """
32 if not hasattr(_thread_locals, 'config'):
33 _thread_locals.config = Config()
34 logger.debug("Initialized configuration")
35 return _thread_locals.config
38def clear_config():
39 """
40 Delete the currently loaded configuration, if any.
41 """
42 if hasattr(_thread_locals, 'config'): 42 ↛ exitline 42 didn't return from function 'clear_config' because the condition on line 42 was always true
43 del _thread_locals.config
44 logger.debug("Cleared configuration")
47class Config:
48 """
49 Fetch and store in memory the current NetBox configuration. This class must be instantiated prior to access, and
50 must be re-instantiated each time it's necessary to check for updates to the cached config.
51 """
52 def __init__(self):
53 self._populate_from_cache()
54 # Only consult the database when the cache has genuinely never been populated. A cached
55 # empty config (no ConfigRevision) is authoritative and must not trigger a re-query.
56 if self._cache_miss:
57 self._populate_from_db()
58 self.defaults = {param.name: param.default for param in PARAMS}
60 def __getattr__(self, item):
62 # Check for hard-coded configuration in settings.py
63 if hasattr(settings, item):
64 return getattr(settings, item)
66 # Return config value from cache
67 if item in self.config: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 return self.config[item]
70 # Fall back to the parameter's default value
71 if item in self.defaults: 71 ↛ 74line 71 didn't jump to line 74 because the condition on line 71 was always true
72 return self.defaults[item]
74 raise AttributeError(_("Invalid configuration parameter: {item}").format(item=item))
76 def _populate_from_cache(self):
77 """Populate config data from Redis cache"""
78 cached_config = cache.get('config', _MISSING)
79 cached_version = cache.get('config_version', _MISSING)
81 # Treat the cache as warm only when both keys are present. A missing 'config_version'
82 # (e.g. evicted or never written) must re-query the database, even if 'config' is cached.
83 # The no-revision branch writes both keys (config={}, config_version=None), so the
84 # intentional empty state is still a cache hit.
85 self._cache_miss = cached_config is _MISSING or cached_version is _MISSING
87 # A cached value of None (ConfigRevision.data is nullable) or {} is a legitimate empty
88 # config and must not crash attribute access; normalize it to an empty dict.
89 self.config = {} if cached_config is _MISSING else (cached_config or {})
90 self.version = None if cached_version is _MISSING else cached_version
91 if self.config: 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true
92 logger.debug("Loaded configuration data from cache")
94 def _populate_from_db(self):
95 """Cache data from latest ConfigRevision, then populate from cache"""
96 from core.models import ConfigRevision
98 try:
99 # Enforce the creation date as the ordering parameter
100 revision = ConfigRevision.objects.get(active=True)
101 logger.debug(f"Loaded active configuration revision (#{revision.pk})")
102 except (ConfigRevision.DoesNotExist, ConfigRevision.MultipleObjectsReturned):
103 revision = ConfigRevision.objects.order_by('-created').first()
104 if revision is None: 104 ↛ 113line 104 didn't jump to line 113 because the condition on line 104 was always true
105 logger.debug("No configuration found in database; proceeding with default values")
106 # Cache the empty state so subsequent requests are served from the cache rather than
107 # re-querying the database on every request (#22158). Creating the first
108 # ConfigRevision overwrites this via the post_save handler.
109 cache.set('config', {}, None)
110 cache.set('config_version', None, None)
111 self._populate_from_cache()
112 return
113 logger.debug(f"No active configuration revision found; falling back to most recent (#{revision.pk})")
114 except DatabaseError:
115 # The database may not be available yet (e.g. when running a management command). Do NOT
116 # cache anything here, so the next instantiation re-queries once the database is reachable.
117 logger.warning("Skipping config initialization (database unavailable)")
118 return
120 revision.activate(update_db=False)
121 self._populate_from_cache()
122 logger.debug("Filled cache with data from latest ConfigRevision")
125class ConfigItem:
126 """
127 A callable to retrieve a configuration parameter from the cache. This can serve as a placeholder to defer
128 referencing a configuration parameter.
129 """
130 def __init__(self, item):
131 self.item = item
133 def __call__(self):
134 config = get_config()
135 return getattr(config, self.item)