settings.py 38 KB

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