Coverage for netbox/settings.py: 63%
406 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 hashlib
2import importlib
3import importlib.util
4import os
5import platform
6import sys
7import warnings
9import storages.utils # type: ignore
10from django.contrib.messages import constants as messages
11from django.core.exceptions import ImproperlyConfigured, ValidationError
12from django.core.validators import URLValidator
13from django.utils.module_loading import import_string
14from django.utils.translation import gettext_lazy as _
16from core.exceptions import IncompatiblePluginError
17from netbox.config import PARAMS as CONFIG_PARAMS
18from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
19from netbox.plugins import PluginConfig
20from netbox.registry import registry
21from netbox.settings_utils import (
22 get_configuration_dir,
23 load_configuration,
24 parse_job_timeout,
25 resolve_install_paths,
26 secret_key_hint,
27 validate_webhook_default_timeout,
28)
29from utilities.release import load_release_data
30from utilities.security import validate_peppers
31from utilities.string import trailing_slash
33from .monkey import get_unique_validators
35#
36# Environment setup
37#
39RELEASE = load_release_data()
40VERSION = RELEASE.full_version # Retained for backward compatibility
41# Settings package directory (settings.py lives here in both checkout & wheel).
42_SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__))
44# All wheel-vs-checkout path branching is centralized in resolve_install_paths(): a wheel
45# bundles package data under netbox/_data and keeps mutable instance files under an external
46# instance root (NETBOX_ROOT, default /opt/netbox); a checkout keeps both roots as the project
47# directory, so archive/git behavior is unchanged.
48_PATHS = resolve_install_paths(_SETTINGS_DIR, os.environ)
49NETBOX_INSTALL_MODE = _PATHS.install_mode
50BASE_DIR = _PATHS.base_dir
51# Instance root for wheel installs (holds conf/, media/, reports/, scripts/, static/, units).
52NETBOX_ROOT = _PATHS.netbox_root
54# Validate the Python version
55if sys.version_info < (3, 12): # noqa: UP036 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 raise RuntimeError(
57 f"NetBox requires Python 3.12 or later. (Currently installed: Python {platform.python_version()})"
58 )
60#
61# Configuration import
62#
64# Import the configuration module (wheel mode prefers NETBOX_ROOT/conf/configuration.py).
65configuration = load_configuration(
66 install_mode=NETBOX_INSTALL_MODE,
67 install_root=NETBOX_ROOT,
68 environ=os.environ,
69)
71# The directory holding the active configuration.py; ldap_config.py lives beside it.
72CONFIGURATION_DIR = get_configuration_dir(configuration)
74# Check for missing/conflicting required configuration parameters
75for parameter in ('ALLOWED_HOSTS', 'SECRET_KEY', 'REDIS'):
76 if not hasattr(configuration, parameter): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 raise ImproperlyConfigured(f"Required parameter {parameter} is missing from configuration.")
78if not hasattr(configuration, 'DATABASE') and not hasattr(configuration, 'DATABASES'): 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 raise ImproperlyConfigured("The database configuration must be defined using DATABASE or DATABASES.")
80elif hasattr(configuration, 'DATABASE') and hasattr(configuration, 'DATABASES'): 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true
81 raise ImproperlyConfigured("DATABASE and DATABASES may not be set together. The use of DATABASES is encouraged.")
83# Set static config parameters
84ADMINS = getattr(configuration, 'ADMINS', [])
85ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS') # Required
86API_TOKEN_PEPPERS = getattr(configuration, 'API_TOKEN_PEPPERS', {})
87AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [
88 {
89 "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
90 "OPTIONS": {
91 "min_length": 12,
92 },
93 },
94 {
95 "NAME": "utilities.password_validation.AlphanumericPasswordValidator",
96 },
97])
98BASE_PATH = trailing_slash(getattr(configuration, 'BASE_PATH', ''))
99BULK_UPDATE_CHUNK_SIZE = getattr(configuration, 'BULK_UPDATE_CHUNK_SIZE', 5000)
100if BULK_UPDATE_CHUNK_SIZE is not None and (type(BULK_UPDATE_CHUNK_SIZE) is not int or BULK_UPDATE_CHUNK_SIZE < 1): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 raise ImproperlyConfigured(
102 f"BULK_UPDATE_CHUNK_SIZE must be a positive integer or None (found {BULK_UPDATE_CHUNK_SIZE!r})"
103 )
104CHANGELOG_SKIP_EMPTY_CHANGES = getattr(configuration, 'CHANGELOG_SKIP_EMPTY_CHANGES', True)
105CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True)
106CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
107CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
108CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
109CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
110CSRF_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}'
111CSRF_COOKIE_HTTPONLY = True
112CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
113CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
114DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440)
115DATABASE = getattr(configuration, 'DATABASE', None) # Legacy DB definition
116DATABASE_ROUTERS = getattr(configuration, 'DATABASE_ROUTERS', [])
117DATABASES = getattr(configuration, 'DATABASES', {'default': DATABASE})
118DEBUG = getattr(configuration, 'DEBUG', False)
119DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None)
120DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
121 # Permit users to manage their own bookmarks
122 'extras.view_bookmark': ({'user': '$user'},),
123 'extras.add_bookmark': ({'user': '$user'},),
124 'extras.change_bookmark': ({'user': '$user'},),
125 'extras.delete_bookmark': ({'user': '$user'},),
126 # Permit users to manage their own notifications
127 'extras.view_notification': ({'user': '$user'},),
128 'extras.add_notification': ({'user': '$user'},),
129 'extras.change_notification': ({'user': '$user'},),
130 'extras.delete_notification': ({'user': '$user'},),
131 # Permit users to manage their own subscriptions
132 'extras.view_subscription': ({'user': '$user'},),
133 'extras.add_subscription': ({'user': '$user'},),
134 'extras.change_subscription': ({'user': '$user'},),
135 'extras.delete_subscription': ({'user': '$user'},),
136 # Permit users to manage their own API tokens
137 'users.view_token': ({'user': '$user'},),
138 'users.add_token': ({'user': '$user'},),
139 'users.change_token': ({'user': '$user'},),
140 'users.delete_token': ({'user': '$user'},),
141})
142DEVELOPER = getattr(configuration, 'DEVELOPER', False)
143DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', _PATHS.docs_root)
144EMAIL = getattr(configuration, 'EMAIL', {})
145STREAMING_EXPORTS = getattr(configuration, 'STREAMING_EXPORTS', False)
146EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', [
147 'extras.events.process_event_queue',
148])
149EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
150FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
151FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
152GRAPHQL_DEFAULT_VERSION = getattr(configuration, 'GRAPHQL_DEFAULT_VERSION', 1)
153GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
154GRAPHQL_MAX_QUERY_DEPTH = getattr(configuration, 'GRAPHQL_MAX_QUERY_DEPTH', None)
155HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node())
156HTTP_CLIENT_IP_HEADERS = getattr(configuration, 'HTTP_CLIENT_IP_HEADERS', (
157 'HTTP_X_REAL_IP',
158 'HTTP_X_FORWARDED_FOR',
159 'REMOTE_ADDR',
160))
161HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
162INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
163ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)
164JINJA_ENVIRONMENT_PARAMS = getattr(configuration, 'JINJA_ENVIRONMENT_PARAMS', [])
165JINJA_FILTERS = getattr(configuration, 'JINJA_FILTERS', getattr(configuration, 'JINJA2_FILTERS', {}))
166LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us')
167LANGUAGE_COOKIE_PATH = CSRF_COOKIE_PATH
168LOGGING = getattr(configuration, 'LOGGING', {})
169LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
170LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', True)
171LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
172LOGIN_FORM_HIDDEN = getattr(configuration, 'LOGIN_FORM_HIDDEN', False)
173LOGOUT_REDIRECT_URL = getattr(configuration, 'LOGOUT_REDIRECT_URL', 'home')
174MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(NETBOX_ROOT, 'media')).rstrip('/')
175METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
176PLUGINS = getattr(configuration, 'PLUGINS', [])
177PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
178PLUGINS_CATALOG_CONFIG = getattr(configuration, 'PLUGINS_CATALOG_CONFIG', {})
179PROXY_ROUTERS = getattr(configuration, 'PROXY_ROUTERS', ['utilities.proxy.DefaultProxyRouter'])
180QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
181REDIS = getattr(configuration, 'REDIS') # Required
182RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
183REMOTE_AUTH_AUTO_CREATE_GROUPS = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_GROUPS', False)
184REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
185REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
186REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
187REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
188REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False)
189REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP')
190REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
191REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False)
192REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
193REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', [])
194REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', [])
195REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL')
196REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME')
197REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME')
198# Required by extras/migrations/0109_script_models.py
199REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(NETBOX_ROOT, 'reports')).rstrip('/')
200RQ = getattr(configuration, 'RQ', {})
201if 'WORKER_CLASS' in RQ and RQ['WORKER_CLASS'] != 'utilities.rqworker.NetBoxRQWorker': 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true
202 warnings.warn(
203 f"RQ['WORKER_CLASS'] is set to {RQ['WORKER_CLASS']!r}; NetBoxRQWorker's self-healing heartbeat "
204 f"logic will not be applied. Workers may not automatically recover from a Redis outage."
205 )
206else:
207 RQ.setdefault('WORKER_CLASS', 'utilities.rqworker.NetBoxRQWorker')
208RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
209RQ_RETRY_INTERVAL = getattr(configuration, 'RQ_RETRY_INTERVAL', 60)
210RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
211SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(NETBOX_ROOT, 'scripts')).rstrip('/')
212SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
213SECRET_KEY = getattr(configuration, 'SECRET_KEY') # Required
214SECURE_HSTS_INCLUDE_SUBDOMAINS = getattr(configuration, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', False)
215SECURE_HSTS_PRELOAD = getattr(configuration, 'SECURE_HSTS_PRELOAD', False)
216SECURE_HSTS_SECONDS = getattr(configuration, 'SECURE_HSTS_SECONDS', 0)
217SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
218SENTRY_CONFIG = getattr(configuration, 'SENTRY_CONFIG', {})
219SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
220SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
221SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
222SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
223SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
224SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
225STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
226STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', None)
227STORAGES = getattr(configuration, 'STORAGES', {})
228TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
229TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
230WEBHOOK_DEFAULT_TIMEOUT = getattr(configuration, 'WEBHOOK_DEFAULT_TIMEOUT', 60)
231validate_webhook_default_timeout(WEBHOOK_DEFAULT_TIMEOUT, parse_job_timeout(RQ_DEFAULT_TIMEOUT))
232DISK_BASE_UNIT = getattr(configuration, 'DISK_BASE_UNIT', 1000)
233if DISK_BASE_UNIT not in [1000, 1024]: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true
234 raise ImproperlyConfigured(f"DISK_BASE_UNIT must be 1000 or 1024 (found {DISK_BASE_UNIT})")
235RAM_BASE_UNIT = getattr(configuration, 'RAM_BASE_UNIT', 1000)
236if RAM_BASE_UNIT not in [1000, 1024]: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true
237 raise ImproperlyConfigured(f"RAM_BASE_UNIT must be 1000 or 1024 (found {RAM_BASE_UNIT})")
239# Load any dynamic configuration parameters which have been hard-coded in the configuration file
240for param in CONFIG_PARAMS:
241 if hasattr(configuration, param.name):
242 globals()[param.name] = getattr(configuration, param.name)
244# Enforce minimum length for SECRET_KEY
245if type(SECRET_KEY) is not str: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 raise ImproperlyConfigured(f"SECRET_KEY must be a string (found {type(SECRET_KEY).__name__})")
247if len(SECRET_KEY) < 50: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true
248 raise ImproperlyConfigured(
249 f"SECRET_KEY must be at least 50 characters in length. To generate a suitable key, run the following command:\n"
250 f" {secret_key_hint(NETBOX_INSTALL_MODE, BASE_DIR)}"
251 )
253# Validate API token peppers
254if API_TOKEN_PEPPERS: 254 ↛ 257line 254 didn't jump to line 257 because the condition on line 254 was always true
255 validate_peppers(API_TOKEN_PEPPERS)
256else:
257 warnings.warn("API_TOKEN_PEPPERS is not defined. v2 API tokens cannot be used.")
259# Validate update repo URL and timeout
260if RELEASE_CHECK_URL: 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true
261 try:
262 URLValidator()(RELEASE_CHECK_URL)
263 except ValidationError:
264 raise ImproperlyConfigured(
265 "RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
266 )
268# Validate configured proxy routers
269for path in PROXY_ROUTERS:
270 if type(path) is str: 270 ↛ 269line 270 didn't jump to line 269 because the condition on line 270 was always true
271 try:
272 import_string(path)
273 except ImportError:
274 raise ImproperlyConfigured(f"Invalid path in PROXY_ROUTERS: {path}")
276# Warn on the presence of deprecated configuration parameters
277if not LOGIN_REQUIRED: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 warnings.warn(
279 "LOGIN_REQUIRED is deprecated and will be removed in NetBox v5.0. Unauthenticated access to the application "
280 "will no longer be supported. Please plan to require authentication for all users before upgrading.",
281 FutureWarning,
282 )
283elif hasattr(configuration, 'LOGIN_REQUIRED'): 283 ↛ 289line 283 didn't jump to line 289 because the condition on line 283 was always true
284 warnings.warn(
285 "LOGIN_REQUIRED is deprecated and will be removed in NetBox v5.0. This parameter can be removed from your "
286 "configuration file.",
287 FutureWarning,
288 )
289if hasattr(configuration, 'JINJA2_FILTERS'): 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true
290 warnings.warn(
291 "JINJA2_FILTERS has been renamed to JINJA_FILTERS and the old name will be removed in NetBox v5.0. Please "
292 "update your configuration file to use JINJA_FILTERS instead.",
293 DeprecationWarning,
294 )
297#
298# Database
299#
301# Verify that a default database has been configured
302if 'default' not in DATABASES: 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true
303 raise ImproperlyConfigured("No default database has been configured.")
305# Set the database engine
306if 'ENGINE' not in DATABASES['default']: 306 ↛ 316line 306 didn't jump to line 316 because the condition on line 306 was always true
307 DATABASES['default'].update({
308 'ENGINE': 'django_prometheus.db.backends.postgresql' if METRICS_ENABLED else 'django.db.backends.postgresql'
309 })
312#
313# Storage backend
314#
316if STORAGE_BACKEND is not None: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true
317 if not STORAGES:
318 raise ImproperlyConfigured(
319 "STORAGE_BACKEND and STORAGES are both set, remove the deprecated STORAGE_BACKEND setting."
320 )
321 else:
322 warnings.warn(
323 "STORAGE_BACKEND is deprecated, use the new STORAGES setting instead.",
324 FutureWarning,
325 )
327if STORAGE_CONFIG is not None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true
328 warnings.warn(
329 "STORAGE_CONFIG is deprecated, use the new STORAGES setting instead.",
330 FutureWarning,
331 )
333# Default STORAGES for Django
334DEFAULT_STORAGES = {
335 "default": {
336 "BACKEND": "django.core.files.storage.FileSystemStorage",
337 },
338 "staticfiles": {
339 "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
340 },
341 "scripts": {
342 "BACKEND": "extras.storage.ScriptFileSystemStorage",
343 "OPTIONS": {
344 "allow_overwrite": True,
345 },
346 },
347}
348STORAGES = DEFAULT_STORAGES | STORAGES
350# TODO: This code is deprecated and needs to be removed in the future
351if STORAGE_BACKEND is not None: 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 STORAGES['default']['BACKEND'] = STORAGE_BACKEND
354# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
355if STORAGE_CONFIG is not None: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true
356 def _setting(name, default=None):
357 if name in STORAGE_CONFIG:
358 return STORAGE_CONFIG[name]
359 return globals().get(name, default)
360 storages.utils.setting = _setting
362# django-storage-swift
363if STORAGE_BACKEND == 'swift.storage.SwiftStorage': 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true
364 try:
365 import swift.utils # noqa: F401
366 except ModuleNotFoundError as e:
367 if getattr(e, 'name') == 'swift':
368 raise ImproperlyConfigured(
369 f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
370 "It can be installed by running 'pip install django-storage-swift'."
371 )
372 raise e
374 # Load all SWIFT_* settings from the user configuration
375 for param, value in STORAGE_CONFIG.items():
376 if param.startswith('SWIFT_'):
377 globals()[param] = value
378# TODO: End of deprecated code
380#
381# Redis
382#
384# Background task queuing
385if 'tasks' not in REDIS: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true
386 raise ImproperlyConfigured("REDIS section in configuration.py is missing the 'tasks' subsection.")
387TASKS_REDIS = REDIS['tasks']
388TASKS_REDIS_HOST = TASKS_REDIS.get('HOST', 'localhost')
389TASKS_REDIS_PORT = TASKS_REDIS.get('PORT', 6379)
390TASKS_REDIS_URL = TASKS_REDIS.get('URL')
391TASKS_REDIS_SENTINELS = TASKS_REDIS.get('SENTINELS', [])
392TASKS_REDIS_USING_SENTINEL = all([
393 isinstance(TASKS_REDIS_SENTINELS, (list, tuple)),
394 len(TASKS_REDIS_SENTINELS) > 0
395])
396TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
397TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
398TASKS_REDIS_USERNAME = TASKS_REDIS.get('USERNAME', '')
399TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
400TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
401TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
402TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
403TASKS_REDIS_CA_CERT_PATH = TASKS_REDIS.get('CA_CERT_PATH', False)
405# Caching
406if 'caching' not in REDIS: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true
407 raise ImproperlyConfigured("REDIS section in configuration.py is missing caching subsection.")
408CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
409CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
410CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
411CACHING_REDIS_USERNAME = REDIS['caching'].get('USERNAME', '')
412CACHING_REDIS_USERNAME_HOST = '@'.join(filter(None, [CACHING_REDIS_USERNAME, CACHING_REDIS_HOST]))
413CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
414CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
415CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
416CACHING_REDIS_PROTO = 'rediss' if REDIS['caching'].get('SSL', False) else 'redis'
417CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY', False)
418CACHING_REDIS_CA_CERT_PATH = REDIS['caching'].get('CA_CERT_PATH', False)
419CACHING_REDIS_URL = REDIS['caching'].get('URL', f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}')
421# Configure Django's default cache to use Redis
422CACHES = {
423 'default': {
424 'BACKEND': 'django_redis.cache.RedisCache',
425 'LOCATION': CACHING_REDIS_URL,
426 'OPTIONS': {
427 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
428 'USERNAME': CACHING_REDIS_USERNAME,
429 'PASSWORD': CACHING_REDIS_PASSWORD,
430 }
431 }
432}
434if CACHING_REDIS_SENTINELS: 434 ↛ 435line 434 didn't jump to line 435 because the condition on line 434 was never true
435 DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory'
436 CACHES['default']['LOCATION'] = f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_SENTINEL_SERVICE}/{CACHING_REDIS_DATABASE}'
437 CACHES['default']['OPTIONS']['CLIENT_CLASS'] = 'django_redis.client.SentinelClient'
438 CACHES['default']['OPTIONS']['SENTINELS'] = CACHING_REDIS_SENTINELS
439if CACHING_REDIS_SKIP_TLS_VERIFY: 439 ↛ 440line 439 didn't jump to line 440 because the condition on line 439 was never true
440 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
441 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_cert_reqs'] = False
442if CACHING_REDIS_CA_CERT_PATH: 442 ↛ 443line 442 didn't jump to line 443 because the condition on line 442 was never true
443 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
444 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_ca_certs'] = CACHING_REDIS_CA_CERT_PATH
446# Merge in KWARGS for additional parameters
447if caching_redis_kwargs := REDIS['caching'].get('KWARGS'): 447 ↛ 448line 447 didn't jump to line 448 because the condition on line 447 was never true
448 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
449 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS'].update(caching_redis_kwargs)
452#
453# Sessions
454#
456if LOGIN_TIMEOUT is not None: 456 ↛ 459line 456 didn't jump to line 459 because the condition on line 456 was always true
457 # Django default is 1209600 seconds (14 days)
458 SESSION_COOKIE_AGE = LOGIN_TIMEOUT
459SESSION_SAVE_EVERY_REQUEST = bool(LOGIN_PERSISTENCE)
460if SESSION_FILE_PATH is not None: 460 ↛ 461line 460 didn't jump to line 461 because the condition on line 460 was never true
461 SESSION_ENGINE = 'django.contrib.sessions.backends.file'
464#
465# Email
466#
468MAILERS = {
469 'default': {
470 'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
471 'OPTIONS': {
472 'host': EMAIL.get('SERVER'),
473 'port': EMAIL.get('PORT', 25),
474 'username': EMAIL.get('USERNAME'),
475 'password': EMAIL.get('PASSWORD'),
476 'use_ssl': EMAIL.get('USE_SSL', False),
477 'use_tls': EMAIL.get('USE_TLS', False),
478 'ssl_certfile': EMAIL.get('SSL_CERTFILE'),
479 'ssl_keyfile': EMAIL.get('SSL_KEYFILE'),
480 'timeout': EMAIL.get('TIMEOUT', 10),
481 },
482 },
483}
484EMAIL_SUBJECT_PREFIX = '[NetBox] '
485SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
488#
489# Django core settings
490#
492INSTALLED_APPS = [
493 'django.contrib.auth',
494 'django.contrib.contenttypes',
495 'django.contrib.sessions',
496 'django.contrib.messages',
497 'django.contrib.staticfiles',
498 'django.contrib.humanize',
499 'django.contrib.postgres',
500 'django.forms',
501 'corsheaders',
502 'debug_toolbar',
503 'django_filters',
504 'django_htmx',
505 'django_tables2',
506 'django_prometheus',
507 'strawberry_django',
508 'mptt',
509 'rest_framework',
510 'social_django',
511 'sorl.thumbnail',
512 'taggit',
513 'timezone_field',
514 'core',
515 'account',
516 'circuits',
517 'dcim',
518 'ipam',
519 'extras',
520 'tenancy',
521 'users',
522 'utilities',
523 'virtualization',
524 'vpn',
525 'wireless',
526 'django_rq', # Must come after extras to allow overriding management commands
527 'drf_spectacular',
528 'drf_spectacular_sidecar',
529]
530if not DEBUG and 'collectstatic' not in sys.argv: 530 ↛ 534line 530 didn't jump to line 534 because the condition on line 530 was always true
531 INSTALLED_APPS.remove('debug_toolbar')
533# Middleware
534MIDDLEWARE = [
535 'corsheaders.middleware.CorsMiddleware',
536 'django.contrib.sessions.middleware.SessionMiddleware',
537 'django.middleware.locale.LocaleMiddleware',
538 'netbox.middleware.CommonMiddleware', # Replaces django.middleware.common.CommonMiddleware
539 'django.middleware.csrf.CsrfViewMiddleware',
540 'django.contrib.auth.middleware.AuthenticationMiddleware',
541 'django.contrib.messages.middleware.MessageMiddleware',
542 'django.middleware.clickjacking.XFrameOptionsMiddleware',
543 'django.middleware.security.SecurityMiddleware',
544 'django_htmx.middleware.HtmxMiddleware',
545 'netbox.middleware.RemoteUserMiddleware',
546 'netbox.middleware.CoreMiddleware',
547 'netbox.middleware.MaintenanceModeMiddleware',
548 'netbox.middleware.SocialAuthExceptionMiddleware',
549]
551if DEBUG: 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true
552 MIDDLEWARE = [
553 "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
554 *MIDDLEWARE,
555 ]
557if METRICS_ENABLED: 557 ↛ 559line 557 didn't jump to line 559 because the condition on line 557 was never true
558 # If metrics are enabled, add the before & after Prometheus middleware
559 MIDDLEWARE = [
560 'netbox.middleware.PrometheusBeforeMiddleware',
561 *MIDDLEWARE,
562 'netbox.middleware.PrometheusAfterMiddleware',
563 ]
565# URLs
566ROOT_URLCONF = 'netbox.urls'
568# Templates
569TEMPLATES_DIR = BASE_DIR + '/templates'
570TEMPLATES = [
571 {
572 'BACKEND': 'django.template.backends.django.DjangoTemplates',
573 'DIRS': [TEMPLATES_DIR],
574 'APP_DIRS': True,
575 'OPTIONS': {
576 'builtins': [
577 'utilities.templatetags.builtins.filters',
578 'utilities.templatetags.builtins.tags',
579 ],
580 'context_processors': [
581 'django.template.context_processors.debug',
582 'django.template.context_processors.request',
583 'django.template.context_processors.media',
584 'django.contrib.auth.context_processors.auth',
585 'django.contrib.messages.context_processors.messages',
586 'netbox.context_processors.settings',
587 'netbox.context_processors.config',
588 'netbox.context_processors.registry',
589 'netbox.context_processors.preferences',
590 ],
591 },
592 },
593]
595# This allows us to override Django's stock form widget templates
596FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
598# Set up authentication backends
599if type(REMOTE_AUTH_BACKEND) not in (list, tuple): 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true
600 REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
601AUTHENTICATION_BACKENDS = [
602 *REMOTE_AUTH_BACKEND,
603 'netbox.authentication.ObjectPermissionBackend',
604]
606# Use our custom User model
607AUTH_USER_MODEL = 'users.User'
609# Authentication URLs
610LOGIN_URL = f'/{BASE_PATH}login/'
611LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
613# Use timezone-aware datetime objects
614USE_TZ = True
616# Toggle language translation support
617USE_I18N = TRANSLATION_ENABLED
619# WSGI
620WSGI_APPLICATION = 'netbox.wsgi.application'
621SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
622USE_X_FORWARDED_HOST = True
623X_FRAME_OPTIONS = 'SAMEORIGIN'
625# Static files (CSS, JavaScript, Images)
626# STATIC_ROOT is deliberately not a configuration parameter; static files are collected to <NETBOX_ROOT>/static.
627STATIC_ROOT = os.path.join(NETBOX_ROOT, 'static')
628STATIC_URL = f'/{BASE_PATH}static/'
629STATICFILES_DIRS = (
630 os.path.join(BASE_DIR, 'project-static', 'dist'),
631 os.path.join(BASE_DIR, 'project-static', 'img'),
632 os.path.join(BASE_DIR, 'project-static', 'js'),
633 # May not exist on a checkout until `manage.py upgrade --build-docs` runs (wheels bundle
634 # the pre-rendered site); collectstatic tolerates that.
635 ('docs', _PATHS.static_docs_root), # Prefix with /docs
636)
638# Media URL
639MEDIA_URL = f'/{BASE_PATH}media/'
641# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
642DATA_UPLOAD_MAX_NUMBER_FIELDS = None
644# Messages
645MESSAGE_TAGS = {
646 messages.ERROR: 'danger',
647}
649DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
651SERIALIZATION_MODULES = {
652 'json': 'utilities.serializers.json',
653}
655DEBUG_TOOLBAR_CONFIG = {
656 'SHOW_TOOLBAR_CALLBACK': 'utilities.debug.show_toolbar',
657}
660#
661# Permissions & authentication
662#
664# Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
665# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
666EXEMPT_EXCLUDE_MODELS = (
667 ('extras', 'configrevision'),
668 ('users', 'group'),
669 ('users', 'objectpermission'),
670 ('users', 'token'),
671 ('users', 'user'),
672)
674# All URLs starting with a string listed here are exempt from maintenance mode enforcement
675MAINTENANCE_EXEMPT_PATHS = (
676 f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
677 LOGIN_URL,
678 LOGIN_REDIRECT_URL,
679 LOGOUT_REDIRECT_URL
680)
683#
684# Sentry
685#
687if SENTRY_ENABLED: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 try:
689 import sentry_sdk
690 except ModuleNotFoundError:
691 raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.")
693 # Build the Sentry initialization parameters
694 sentry_config = {
695 'sample_rate': 1.0,
696 'send_default_pii': False,
697 'traces_sample_rate': 0,
698 # TODO: Support proxy routing
699 'http_proxy': HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
700 'https_proxy': HTTP_PROXIES.get('https') if HTTP_PROXIES else None,
701 }
702 sentry_config.update(SENTRY_CONFIG)
703 # Check for a DSN
704 if not sentry_config.get('dsn'):
705 raise ImproperlyConfigured(
706 "Sentry is enabled but a DSN has not been specified. Set one under the SENTRY_CONFIG parameter."
707 )
709 # Initialize the SDK
710 sentry_sdk.init(
711 release=RELEASE.full_version,
712 **sentry_config
713 )
714 # Assign any configured tags
715 for k, v in SENTRY_TAGS.items():
716 sentry_sdk.set_tag(k, v)
719#
720# Census collection
721#
723# Calculate a unique deployment ID from the secret key
724DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
725CENSUS_URL = 'https://census.netbox.oss.netboxlabs.com/api/v1/'
728#
729# NetBox Copilot
730#
732NETBOX_COPILOT_URL = 'https://static.copilot.netboxlabs.ai/load.js'
735#
736# Django social auth
737#
739SOCIAL_AUTH_PIPELINE = (
740 'social_core.pipeline.social_auth.social_details',
741 'social_core.pipeline.social_auth.social_uid',
742 'social_core.pipeline.social_auth.social_user',
743 'social_core.pipeline.user.get_username',
744 'social_core.pipeline.user.create_user',
745 'social_core.pipeline.social_auth.associate_user',
746 'netbox.authentication.user_default_groups_handler',
747 'social_core.pipeline.social_auth.load_extra_data',
748 'social_core.pipeline.user.user_details',
749)
751# Redirect users back to the login page (surfacing the error via the messages framework) when an
752# SSO/SAML authentication failure occurs, rather than raising an HTTP 500. Full exceptions are still
753# raised when DEBUG is enabled. LOGIN_URL is an absolute path which respects BASE_PATH; the social
754# auth middleware passes this value directly to an HttpResponseRedirect without reversing it.
755SOCIAL_AUTH_LOGIN_ERROR_URL = LOGIN_URL
756SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG
758# Load all SOCIAL_AUTH_* settings from the user configuration
759for param in dir(configuration):
760 if param.startswith('SOCIAL_AUTH_'):
761 globals()[param] = getattr(configuration, param)
763# Force usage of PostgreSQL's JSONB field for extra data
764SOCIAL_AUTH_JSONFIELD_ENABLED = True
765SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'
767SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL
769#
770# Django Prometheus
771#
773PROMETHEUS_EXPORT_MIGRATIONS = False
776#
777# Django filters
778#
780FILTERS_NULL_CHOICE_LABEL = 'None'
781FILTERS_NULL_CHOICE_VALUE = 'null'
784#
785# Django REST framework (API)
786#
788REST_FRAMEWORK_VERSION = '.'.join(RELEASE.version.split('-')[0].split('.')[:2]) # Use major.minor as API version
789REST_FRAMEWORK = {
790 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
791 'COERCE_DECIMAL_TO_STRING': False,
792 'DEFAULT_AUTHENTICATION_CLASSES': (
793 'rest_framework.authentication.SessionAuthentication',
794 'netbox.api.authentication.TokenAuthentication',
795 ),
796 'DEFAULT_FILTER_BACKENDS': (
797 'django_filters.rest_framework.DjangoFilterBackend',
798 'rest_framework.filters.OrderingFilter',
799 ),
800 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata',
801 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.NetBoxPagination',
802 'DEFAULT_PARSER_CLASSES': (
803 'rest_framework.parsers.JSONParser',
804 'rest_framework.parsers.MultiPartParser',
805 ),
806 'DEFAULT_PERMISSION_CLASSES': (
807 'netbox.api.authentication.TokenPermissions',
808 ),
809 'DEFAULT_RENDERER_CLASSES': (
810 'rest_framework.renderers.JSONRenderer',
811 'netbox.api.renderers.FormlessBrowsableAPIRenderer',
812 ),
813 'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema',
814 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
815 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
816 # Align REST framework's key for errors which pertain to no particular field with Django's
817 # (django.core.exceptions.NON_FIELD_ERRORS), so that the API reports such an error under one key
818 # rather than two. Model validation errors reach a response by way of full_clean(), and so are
819 # keyed by Django; errors raised by a serializer or field are keyed by REST framework. Without
820 # this, which of the two a client must read depends on the layer which rejected the request.
821 'NON_FIELD_ERRORS_KEY': '__all__',
822 'SCHEMA_COERCE_METHOD_NAMES': {
823 # Default mappings
824 'retrieve': 'read',
825 'destroy': 'delete',
826 # Custom operations
827 'bulk_destroy': 'bulk_delete',
828 },
829 'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
830}
832#
833# DRF Spectacular
834#
836SPECTACULAR_SETTINGS = {
837 'TITLE': 'NetBox REST API',
838 'LICENSE': {'name': 'Apache v2 License'},
839 'VERSION': RELEASE.full_version,
840 'COMPONENT_SPLIT_REQUEST': True,
841 'REDOC_DIST': 'SIDECAR',
842 'SERVERS': [{
843 'url': '',
844 'description': 'NetBox',
845 }],
846 'SWAGGER_UI_DIST': 'SIDECAR',
847 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
848 'POSTPROCESSING_HOOKS': [],
849}
851#
852# Django RQ (events backend)
853#
855if TASKS_REDIS_USING_SENTINEL: 855 ↛ 856line 855 didn't jump to line 856 because the condition on line 855 was never true
856 RQ_PARAMS = {
857 'SENTINELS': TASKS_REDIS_SENTINELS,
858 'MASTER_NAME': TASKS_REDIS_SENTINEL_SERVICE,
859 'SOCKET_TIMEOUT': None,
860 'CONNECTION_KWARGS': {
861 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
862 },
863 }
864elif TASKS_REDIS_URL: 864 ↛ 865line 864 didn't jump to line 865 because the condition on line 864 was never true
865 RQ_PARAMS = {
866 'URL': TASKS_REDIS_URL,
867 'SSL': TASKS_REDIS_SSL,
868 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
869 }
870else:
871 RQ_PARAMS = {
872 'HOST': TASKS_REDIS_HOST,
873 'PORT': TASKS_REDIS_PORT,
874 'SSL': TASKS_REDIS_SSL,
875 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
876 }
877RQ_PARAMS.update({
878 'DB': TASKS_REDIS_DATABASE,
879 'USERNAME': TASKS_REDIS_USERNAME,
880 'PASSWORD': TASKS_REDIS_PASSWORD,
881 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
882})
883if TASKS_REDIS_CA_CERT_PATH: 883 ↛ 884line 883 didn't jump to line 884 because the condition on line 883 was never true
884 RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
885 RQ_PARAMS['REDIS_CLIENT_KWARGS']['ssl_ca_certs'] = TASKS_REDIS_CA_CERT_PATH
887# Merge in KWARGS for additional parameters
888if tasks_redis_kwargs := TASKS_REDIS.get('KWARGS'): 888 ↛ 889line 888 didn't jump to line 889 because the condition on line 888 was never true
889 RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
890 RQ_PARAMS['REDIS_CLIENT_KWARGS'].update(tasks_redis_kwargs)
892# Define named RQ queues
893RQ_QUEUES = {
894 RQ_QUEUE_HIGH: RQ_PARAMS,
895 RQ_QUEUE_DEFAULT: RQ_PARAMS,
896 RQ_QUEUE_LOW: RQ_PARAMS,
897}
898# Add any queues defined in QUEUE_MAPPINGS
899RQ_QUEUES.update({
900 queue: RQ_PARAMS for queue in set(QUEUE_MAPPINGS.values()) if queue not in RQ_QUEUES
901})
903#
904# Localization
905#
907# Supported translation languages
908LANGUAGES = (
909 ('cs', _('Czech')),
910 ('da', _('Danish')),
911 ('de', _('German')),
912 ('en', _('English')),
913 ('es', _('Spanish')),
914 ('fr', _('French')),
915 ('it', _('Italian')),
916 ('ja', _('Japanese')),
917 ('ko', _('Korean')),
918 ('lv', _('Latvian')),
919 ('nl', _('Dutch')),
920 ('pl', _('Polish')),
921 ('pt', _('Portuguese')),
922 ('ru', _('Russian')),
923 ('tr', _('Turkish')),
924 ('uk', _('Ukrainian')),
925 ('zh', _('Chinese')),
926)
927LOCALE_PATHS = (
928 BASE_DIR + '/translations',
929)
931#
932# Strawberry (GraphQL)
933#
934STRAWBERRY_DJANGO = {
935 "DEFAULT_PK_FIELD_NAME": "id",
936 "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
937 "PAGINATION_DEFAULT_LIMIT": 100,
938 # Disable the library's max-limit cap (introduced in strawberry-graphql-django 0.85); NetBox enforces its own
939 # page-size ceiling via MAX_PAGE_SIZE in netbox.graphql.pagination.apply_pagination().
940 "PAGINATION_MAX_LIMIT": None,
941}
943#
944# Plugins
945#
947PLUGIN_CATALOG_URL = 'https://api.netbox.oss.netboxlabs.com/v1/plugins'
949EVENTS_PIPELINE = list(EVENTS_PIPELINE)
950if 'extras.events.process_event_queue' not in EVENTS_PIPELINE: 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 EVENTS_PIPELINE.insert(0, 'extras.events.process_event_queue')
953# Register any configured plugins
954for plugin_name in PLUGINS: 954 ↛ 955line 954 didn't jump to line 955 because the loop on line 954 never started
955 try:
956 # Import the plugin module
957 plugin = importlib.import_module(plugin_name)
958 except ModuleNotFoundError as e:
959 if getattr(e, 'name') == plugin_name:
960 raise ImproperlyConfigured(
961 f"Unable to import plugin {plugin_name}: Module not found. Check that the plugin module has been "
962 f"installed within the correct Python environment."
963 )
964 raise e
966 try:
967 # Load the PluginConfig
968 plugin_config: PluginConfig = plugin.config
969 except AttributeError:
970 raise ImproperlyConfigured(
971 f"Plugin {plugin_name} does not provide a 'config' variable. This should be defined in the plugin's "
972 f"__init__.py file and point to the PluginConfig subclass."
973 )
975 # Validate version compatibility and user-provided configuration settings and assign defaults
976 if plugin_name not in PLUGINS_CONFIG:
977 PLUGINS_CONFIG[plugin_name] = {}
978 try:
979 plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
980 except IncompatiblePluginError as e:
981 warnings.warn(f'Unable to load plugin {plugin_name}: {e}')
982 continue
984 # Register the plugin as installed successfully
985 registry['plugins']['installed'].append(plugin_name)
987 plugin_module = "{}.{}".format(plugin_config.__module__, plugin_config.__name__) # type: ignore
989 # Gather additional apps to load alongside this plugin
990 django_apps = plugin_config.django_apps
991 if plugin_name in django_apps:
992 django_apps.pop(plugin_name)
993 if plugin_module not in django_apps:
994 django_apps.append(plugin_module)
996 # Test if we can import all modules (or its parent, for PluginConfigs and AppConfigs)
997 for app in django_apps:
998 if "." in app:
999 parts = app.split(".")
1000 spec = importlib.util.find_spec(".".join(parts[:-1]))
1001 else:
1002 spec = importlib.util.find_spec(app)
1003 if spec is None:
1004 raise ImproperlyConfigured(
1005 f"Failed to load django_apps specified by plugin {plugin_name}: {django_apps} "
1006 f"The module {app} cannot be imported. Check that the necessary package has been "
1007 f"installed within the correct Python environment."
1008 )
1010 INSTALLED_APPS.extend(django_apps)
1012 # Preserve uniqueness of the INSTALLED_APPS list, we keep the last occurrence
1013 sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
1014 INSTALLED_APPS = list(sorted_apps)
1016 # Add middleware
1017 plugin_middleware = plugin_config.middleware
1018 if plugin_middleware and type(plugin_middleware) in (list, tuple):
1019 MIDDLEWARE.extend(plugin_middleware)
1021 # Create RQ queues dedicated to the plugin
1022 # we use the plugin name as a prefix for queue name's defined in the plugin config
1023 # ex: mysuperplugin.mysuperqueue1
1024 if type(plugin_config.queues) is not list:
1025 raise ImproperlyConfigured(f"Plugin {plugin_name} queues must be a list.")
1026 RQ_QUEUES.update({
1027 f"{plugin_name}.{queue}": RQ_PARAMS for queue in plugin_config.queues
1028 })
1030 events_pipeline = plugin_config.events_pipeline
1031 if events_pipeline:
1032 if type(events_pipeline) in (list, tuple):
1033 EVENTS_PIPELINE.extend(events_pipeline)
1034 else:
1035 raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
1037# GraphQL assembly must run after every plugin has initialized.
1038INSTALLED_APPS.append('netbox.graphql.apps.GraphQLConfig')
1041#
1042# Monkey-patching
1043#
1045from rest_framework.utils import field_mapping # noqa: E402
1046from strawberry_django import pagination # noqa: E402
1047from strawberry_django.fields.field import StrawberryDjangoField # noqa: E402
1049from netbox.graphql.pagination import OffsetPaginationInput, apply_pagination # noqa: E402
1051# TODO: Remove this once #20547 has been implemented
1052# Override DRF's get_unique_validators() function with our own (see bug #19302)
1053field_mapping.get_unique_validators = get_unique_validators
1055# Override strawberry-django's OffsetPaginationInput class to add the `start` parameter
1056pagination.OffsetPaginationInput = OffsetPaginationInput
1058# Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination
1059StrawberryDjangoField.apply_pagination = apply_pagination
1062# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
1063try:
1064 from .local_settings import *
1065 _UNSUPPORTED_SETTINGS = True
1066except ImportError:
1067 pass