settings.py 32 KB

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