settings.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. import hashlib
  2. import importlib
  3. import logging
  4. import os
  5. import platform
  6. import re
  7. import socket
  8. import sys
  9. import warnings
  10. from urllib.parse import urlsplit
  11. import sentry_sdk
  12. from django.contrib.messages import constants as messages
  13. from django.core.exceptions import ImproperlyConfigured, ValidationError
  14. from django.core.validators import URLValidator
  15. from sentry_sdk.integrations.django import DjangoIntegration
  16. from netbox.config import PARAMS
  17. # Monkey patch to fix Django 4.0 support for graphene-django (see
  18. # https://github.com/graphql-python/graphene-django/issues/1284)
  19. # TODO: Remove this when graphene-django 2.16 becomes available
  20. import django
  21. from django.utils.encoding import force_str
  22. django.utils.encoding.force_text = force_str
  23. #
  24. # Environment setup
  25. #
  26. VERSION = '3.3.2'
  27. # Hostname
  28. HOSTNAME = platform.node()
  29. # Set the base directory two levels up
  30. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  31. # Validate Python version
  32. if sys.version_info < (3, 8):
  33. raise RuntimeError(
  34. f"NetBox requires Python 3.8 or later. (Currently installed: Python {platform.python_version()})"
  35. )
  36. DEFAULT_SENTRY_DSN = 'https://198cf560b29d4054ab8e583a1d10ea58@o1242133.ingest.sentry.io/6396485'
  37. #
  38. # Configuration import
  39. #
  40. # Import configuration parameters
  41. config_path = os.getenv('NETBOX_CONFIGURATION', 'netbox.configuration')
  42. try:
  43. configuration = importlib.import_module(config_path)
  44. except ModuleNotFoundError as e:
  45. if getattr(e, 'name') == config_path:
  46. raise ImproperlyConfigured(
  47. f"Specified configuration module ({config_path}) not found. Please define netbox/netbox/configuration.py "
  48. f"per the documentation, or specify an alternate module in the NETBOX_CONFIGURATION environment variable."
  49. )
  50. raise
  51. # Enforce required configuration parameters
  52. for parameter in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS']:
  53. if not hasattr(configuration, parameter):
  54. raise ImproperlyConfigured(f"Required parameter {parameter} is missing from configuration.")
  55. # Set required parameters
  56. ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS')
  57. DATABASE = getattr(configuration, 'DATABASE')
  58. REDIS = getattr(configuration, 'REDIS')
  59. SECRET_KEY = getattr(configuration, 'SECRET_KEY')
  60. # Calculate a unique deployment ID from the secret key
  61. DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
  62. # Set static config parameters
  63. ADMINS = getattr(configuration, 'ADMINS', [])
  64. AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [])
  65. BASE_PATH = getattr(configuration, 'BASE_PATH', '')
  66. if BASE_PATH:
  67. BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
  68. CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
  69. CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
  70. CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
  71. CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
  72. CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
  73. DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
  74. DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
  75. DEBUG = getattr(configuration, 'DEBUG', False)
  76. DEVELOPER = getattr(configuration, 'DEVELOPER', False)
  77. DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
  78. EMAIL = getattr(configuration, 'EMAIL', {})
  79. EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
  80. FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
  81. HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
  82. INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
  83. JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
  84. LOGGING = getattr(configuration, 'LOGGING', {})
  85. LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
  86. LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
  87. LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
  88. MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
  89. METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
  90. PLUGINS = getattr(configuration, 'PLUGINS', [])
  91. PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
  92. RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
  93. REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
  94. REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
  95. REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
  96. REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
  97. REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False)
  98. REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
  99. REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP')
  100. REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False)
  101. REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', [])
  102. REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', [])
  103. REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', [])
  104. REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
  105. REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
  106. REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
  107. RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
  108. SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
  109. SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
  110. SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
  111. SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
  112. SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
  113. SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
  114. SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
  115. SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
  116. SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
  117. SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
  118. SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
  119. STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
  120. STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
  121. TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
  122. TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
  123. # Check for hard-coded dynamic config parameters
  124. for param in PARAMS:
  125. if hasattr(configuration, param.name):
  126. globals()[param.name] = getattr(configuration, param.name)
  127. # Validate update repo URL and timeout
  128. if RELEASE_CHECK_URL:
  129. validator = URLValidator(
  130. message=(
  131. "RELEASE_CHECK_URL must be a valid API URL. Example: "
  132. "https://api.github.com/repos/netbox-community/netbox"
  133. )
  134. )
  135. try:
  136. validator(RELEASE_CHECK_URL)
  137. except ValidationError as err:
  138. raise ImproperlyConfigured(str(err))
  139. #
  140. # Database
  141. #
  142. # Only PostgreSQL is supported
  143. if METRICS_ENABLED:
  144. DATABASE.update({
  145. 'ENGINE': 'django_prometheus.db.backends.postgresql'
  146. })
  147. else:
  148. DATABASE.update({
  149. 'ENGINE': 'django.db.backends.postgresql'
  150. })
  151. DATABASES = {
  152. 'default': DATABASE,
  153. }
  154. #
  155. # Media storage
  156. #
  157. if STORAGE_BACKEND is not None:
  158. DEFAULT_FILE_STORAGE = STORAGE_BACKEND
  159. # django-storages
  160. if STORAGE_BACKEND.startswith('storages.'):
  161. try:
  162. import storages.utils
  163. except ModuleNotFoundError as e:
  164. if getattr(e, 'name') == 'storages':
  165. raise ImproperlyConfigured(
  166. f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storages is not present. It can be "
  167. f"installed by running 'pip install django-storages'."
  168. )
  169. raise e
  170. # Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
  171. def _setting(name, default=None):
  172. if name in STORAGE_CONFIG:
  173. return STORAGE_CONFIG[name]
  174. return globals().get(name, default)
  175. storages.utils.setting = _setting
  176. if STORAGE_CONFIG and STORAGE_BACKEND is None:
  177. warnings.warn(
  178. "STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
  179. "ignored."
  180. )
  181. #
  182. # Redis
  183. #
  184. # Background task queuing
  185. if 'tasks' not in REDIS:
  186. raise ImproperlyConfigured(
  187. "REDIS section in configuration.py is missing the 'tasks' subsection."
  188. )
  189. TASKS_REDIS = REDIS['tasks']
  190. TASKS_REDIS_HOST = TASKS_REDIS.get('HOST', 'localhost')
  191. TASKS_REDIS_PORT = TASKS_REDIS.get('PORT', 6379)
  192. TASKS_REDIS_SENTINELS = TASKS_REDIS.get('SENTINELS', [])
  193. TASKS_REDIS_USING_SENTINEL = all([
  194. isinstance(TASKS_REDIS_SENTINELS, (list, tuple)),
  195. len(TASKS_REDIS_SENTINELS) > 0
  196. ])
  197. TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
  198. TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
  199. TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
  200. TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
  201. TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
  202. TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
  203. # Caching
  204. if 'caching' not in REDIS:
  205. raise ImproperlyConfigured(
  206. "REDIS section in configuration.py is missing caching subsection."
  207. )
  208. CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
  209. CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
  210. CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
  211. CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
  212. CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
  213. CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
  214. CACHING_REDIS_PROTO = 'rediss' if REDIS['caching'].get('SSL', False) else 'redis'
  215. CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY', False)
  216. CACHES = {
  217. 'default': {
  218. 'BACKEND': 'django_redis.cache.RedisCache',
  219. 'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}',
  220. 'OPTIONS': {
  221. 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
  222. 'PASSWORD': CACHING_REDIS_PASSWORD,
  223. }
  224. }
  225. }
  226. if CACHING_REDIS_SENTINELS:
  227. DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory'
  228. CACHES['default']['LOCATION'] = f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_SENTINEL_SERVICE}/{CACHING_REDIS_DATABASE}'
  229. CACHES['default']['OPTIONS']['CLIENT_CLASS'] = 'django_redis.client.SentinelClient'
  230. CACHES['default']['OPTIONS']['SENTINELS'] = CACHING_REDIS_SENTINELS
  231. if CACHING_REDIS_SKIP_TLS_VERIFY:
  232. CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
  233. CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_cert_reqs'] = False
  234. #
  235. # Sessions
  236. #
  237. if LOGIN_TIMEOUT is not None:
  238. # Django default is 1209600 seconds (14 days)
  239. SESSION_COOKIE_AGE = LOGIN_TIMEOUT
  240. SESSION_SAVE_EVERY_REQUEST = bool(LOGIN_PERSISTENCE)
  241. if SESSION_FILE_PATH is not None:
  242. SESSION_ENGINE = 'django.contrib.sessions.backends.file'
  243. #
  244. # Email
  245. #
  246. EMAIL_HOST = EMAIL.get('SERVER')
  247. EMAIL_HOST_USER = EMAIL.get('USERNAME')
  248. EMAIL_HOST_PASSWORD = EMAIL.get('PASSWORD')
  249. EMAIL_PORT = EMAIL.get('PORT', 25)
  250. EMAIL_SSL_CERTFILE = EMAIL.get('SSL_CERTFILE')
  251. EMAIL_SSL_KEYFILE = EMAIL.get('SSL_KEYFILE')
  252. EMAIL_SUBJECT_PREFIX = '[NetBox] '
  253. EMAIL_USE_SSL = EMAIL.get('USE_SSL', False)
  254. EMAIL_USE_TLS = EMAIL.get('USE_TLS', False)
  255. EMAIL_TIMEOUT = EMAIL.get('TIMEOUT', 10)
  256. SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
  257. #
  258. # Django
  259. #
  260. INSTALLED_APPS = [
  261. 'django.contrib.admin',
  262. 'django.contrib.auth',
  263. 'django.contrib.contenttypes',
  264. 'django.contrib.sessions',
  265. 'django.contrib.messages',
  266. 'django.contrib.staticfiles',
  267. 'django.contrib.humanize',
  268. 'corsheaders',
  269. 'debug_toolbar',
  270. 'graphiql_debug_toolbar',
  271. 'django_filters',
  272. 'django_tables2',
  273. 'django_prometheus',
  274. 'graphene_django',
  275. 'mptt',
  276. 'rest_framework',
  277. 'social_django',
  278. 'taggit',
  279. 'timezone_field',
  280. 'circuits',
  281. 'dcim',
  282. 'ipam',
  283. 'extras',
  284. 'tenancy',
  285. 'users',
  286. 'utilities',
  287. 'virtualization',
  288. 'wireless',
  289. 'django_rq', # Must come after extras to allow overriding management commands
  290. 'drf_yasg',
  291. ]
  292. # Middleware
  293. MIDDLEWARE = [
  294. 'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
  295. 'django_prometheus.middleware.PrometheusBeforeMiddleware',
  296. 'corsheaders.middleware.CorsMiddleware',
  297. 'django.contrib.sessions.middleware.SessionMiddleware',
  298. 'django.middleware.common.CommonMiddleware',
  299. 'django.middleware.csrf.CsrfViewMiddleware',
  300. 'django.contrib.auth.middleware.AuthenticationMiddleware',
  301. 'django.contrib.messages.middleware.MessageMiddleware',
  302. 'django.middleware.clickjacking.XFrameOptionsMiddleware',
  303. 'django.middleware.security.SecurityMiddleware',
  304. 'netbox.middleware.ExceptionHandlingMiddleware',
  305. 'netbox.middleware.RemoteUserMiddleware',
  306. 'netbox.middleware.LoginRequiredMiddleware',
  307. 'netbox.middleware.DynamicConfigMiddleware',
  308. 'netbox.middleware.APIVersionMiddleware',
  309. 'netbox.middleware.ObjectChangeMiddleware',
  310. 'django_prometheus.middleware.PrometheusAfterMiddleware',
  311. ]
  312. ROOT_URLCONF = 'netbox.urls'
  313. TEMPLATES_DIR = BASE_DIR + '/templates'
  314. TEMPLATES = [
  315. {
  316. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  317. 'DIRS': [TEMPLATES_DIR],
  318. 'APP_DIRS': True,
  319. 'OPTIONS': {
  320. 'builtins': [
  321. 'utilities.templatetags.builtins.filters',
  322. 'utilities.templatetags.builtins.tags',
  323. ],
  324. 'context_processors': [
  325. 'django.template.context_processors.debug',
  326. 'django.template.context_processors.request',
  327. 'django.template.context_processors.media',
  328. 'django.contrib.auth.context_processors.auth',
  329. 'django.contrib.messages.context_processors.messages',
  330. 'netbox.context_processors.settings_and_registry',
  331. ],
  332. },
  333. },
  334. ]
  335. # Set up authentication backends
  336. AUTHENTICATION_BACKENDS = [
  337. REMOTE_AUTH_BACKEND,
  338. 'netbox.authentication.ObjectPermissionBackend',
  339. ]
  340. # Internationalization
  341. LANGUAGE_CODE = 'en-us'
  342. USE_I18N = True
  343. USE_L10N = False
  344. USE_TZ = True
  345. USE_DEPRECATED_PYTZ = True
  346. # WSGI
  347. WSGI_APPLICATION = 'netbox.wsgi.application'
  348. SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
  349. USE_X_FORWARDED_HOST = True
  350. X_FRAME_OPTIONS = 'SAMEORIGIN'
  351. # Static files (CSS, JavaScript, Images)
  352. STATIC_ROOT = BASE_DIR + '/static'
  353. STATIC_URL = f'/{BASE_PATH}static/'
  354. STATICFILES_DIRS = (
  355. os.path.join(BASE_DIR, 'project-static', 'dist'),
  356. os.path.join(BASE_DIR, 'project-static', 'img'),
  357. ('docs', os.path.join(BASE_DIR, 'project-static', 'docs')), # Prefix with /docs
  358. )
  359. # Media
  360. MEDIA_URL = '/{}media/'.format(BASE_PATH)
  361. # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
  362. DATA_UPLOAD_MAX_NUMBER_FIELDS = None
  363. # Messages
  364. MESSAGE_TAGS = {
  365. messages.ERROR: 'danger',
  366. }
  367. # Authentication URLs
  368. LOGIN_URL = f'/{BASE_PATH}login/'
  369. LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
  370. DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
  371. TEST_RUNNER = "django_rich.test.RichRunner"
  372. # Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
  373. # by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
  374. EXEMPT_EXCLUDE_MODELS = (
  375. ('auth', 'group'),
  376. ('auth', 'user'),
  377. ('users', 'objectpermission'),
  378. )
  379. # All URLs starting with a string listed here are exempt from login enforcement
  380. EXEMPT_PATHS = (
  381. f'/{BASE_PATH}api/',
  382. f'/{BASE_PATH}graphql/',
  383. f'/{BASE_PATH}login/',
  384. f'/{BASE_PATH}oauth/',
  385. f'/{BASE_PATH}metrics',
  386. )
  387. #
  388. # Sentry
  389. #
  390. if SENTRY_ENABLED:
  391. if not SENTRY_DSN:
  392. raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
  393. # If using the default DSN, force sampling rates
  394. if SENTRY_DSN == DEFAULT_SENTRY_DSN:
  395. SENTRY_SAMPLE_RATE = 1.0
  396. SENTRY_TRACES_SAMPLE_RATE = 0
  397. # Initialize the SDK
  398. sentry_sdk.init(
  399. dsn=SENTRY_DSN,
  400. release=VERSION,
  401. integrations=[DjangoIntegration()],
  402. sample_rate=SENTRY_SAMPLE_RATE,
  403. traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
  404. send_default_pii=True,
  405. http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
  406. https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
  407. )
  408. # Assign any configured tags
  409. for k, v in SENTRY_TAGS.items():
  410. sentry_sdk.set_tag(k, v)
  411. # If using the default DSN, append a unique deployment ID tag for error correlation
  412. if SENTRY_DSN == DEFAULT_SENTRY_DSN:
  413. sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID)
  414. #
  415. # Django social auth
  416. #
  417. SOCIAL_AUTH_PIPELINE = (
  418. 'social_core.pipeline.social_auth.social_details',
  419. 'social_core.pipeline.social_auth.social_uid',
  420. 'social_core.pipeline.social_auth.social_user',
  421. 'social_core.pipeline.user.get_username',
  422. 'social_core.pipeline.social_auth.associate_by_email',
  423. 'social_core.pipeline.user.create_user',
  424. 'social_core.pipeline.social_auth.associate_user',
  425. 'netbox.authentication.user_default_groups_handler',
  426. 'social_core.pipeline.social_auth.load_extra_data',
  427. 'social_core.pipeline.user.user_details',
  428. )
  429. # Load all SOCIAL_AUTH_* settings from the user configuration
  430. for param in dir(configuration):
  431. if param.startswith('SOCIAL_AUTH_'):
  432. globals()[param] = getattr(configuration, param)
  433. # Force usage of PostgreSQL's JSONB field for extra data
  434. SOCIAL_AUTH_JSONFIELD_ENABLED = True
  435. #
  436. # Django Prometheus
  437. #
  438. PROMETHEUS_EXPORT_MIGRATIONS = False
  439. #
  440. # Django filters
  441. #
  442. FILTERS_NULL_CHOICE_LABEL = 'None'
  443. FILTERS_NULL_CHOICE_VALUE = 'null'
  444. #
  445. # Django REST framework (API)
  446. #
  447. REST_FRAMEWORK_VERSION = '.'.join(VERSION.split('-')[0].split('.')[:2]) # Use major.minor as API version
  448. REST_FRAMEWORK = {
  449. 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
  450. 'COERCE_DECIMAL_TO_STRING': False,
  451. 'DEFAULT_AUTHENTICATION_CLASSES': (
  452. 'rest_framework.authentication.SessionAuthentication',
  453. 'netbox.api.authentication.TokenAuthentication',
  454. ),
  455. 'DEFAULT_FILTER_BACKENDS': (
  456. 'django_filters.rest_framework.DjangoFilterBackend',
  457. 'rest_framework.filters.OrderingFilter',
  458. ),
  459. 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata',
  460. 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
  461. 'DEFAULT_PARSER_CLASSES': (
  462. 'rest_framework.parsers.JSONParser',
  463. 'rest_framework.parsers.MultiPartParser',
  464. ),
  465. 'DEFAULT_PERMISSION_CLASSES': (
  466. 'netbox.api.authentication.TokenPermissions',
  467. ),
  468. 'DEFAULT_RENDERER_CLASSES': (
  469. 'rest_framework.renderers.JSONRenderer',
  470. 'netbox.api.renderers.FormlessBrowsableAPIRenderer',
  471. ),
  472. 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
  473. 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
  474. 'SCHEMA_COERCE_METHOD_NAMES': {
  475. # Default mappings
  476. 'retrieve': 'read',
  477. 'destroy': 'delete',
  478. # Custom operations
  479. 'bulk_destroy': 'bulk_delete',
  480. },
  481. 'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
  482. }
  483. #
  484. # Graphene
  485. #
  486. GRAPHENE = {
  487. # Avoids naming collision on models with 'type' field; see
  488. # https://github.com/graphql-python/graphene-django/issues/185
  489. 'DJANGO_CHOICE_FIELD_ENUM_V3_NAMING': True,
  490. }
  491. #
  492. # drf_yasg (OpenAPI/Swagger)
  493. #
  494. SWAGGER_SETTINGS = {
  495. 'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
  496. 'DEFAULT_FIELD_INSPECTORS': [
  497. 'utilities.custom_inspectors.CustomFieldsDataFieldInspector',
  498. 'utilities.custom_inspectors.NullableBooleanFieldInspector',
  499. 'utilities.custom_inspectors.ChoiceFieldInspector',
  500. 'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
  501. 'drf_yasg.inspectors.CamelCaseJSONFilter',
  502. 'drf_yasg.inspectors.ReferencingSerializerInspector',
  503. 'drf_yasg.inspectors.RelatedFieldInspector',
  504. 'drf_yasg.inspectors.ChoiceFieldInspector',
  505. 'drf_yasg.inspectors.FileFieldInspector',
  506. 'drf_yasg.inspectors.DictFieldInspector',
  507. 'drf_yasg.inspectors.JSONFieldInspector',
  508. 'drf_yasg.inspectors.SerializerMethodFieldInspector',
  509. 'drf_yasg.inspectors.SimpleFieldInspector',
  510. 'drf_yasg.inspectors.StringDefaultFieldInspector',
  511. ],
  512. 'DEFAULT_FILTER_INSPECTORS': [
  513. 'drf_yasg.inspectors.CoreAPICompatInspector',
  514. ],
  515. 'DEFAULT_INFO': 'netbox.urls.openapi_info',
  516. 'DEFAULT_MODEL_DEPTH': 1,
  517. 'DEFAULT_PAGINATOR_INSPECTORS': [
  518. 'utilities.custom_inspectors.NullablePaginatorInspector',
  519. 'drf_yasg.inspectors.DjangoRestResponsePagination',
  520. 'drf_yasg.inspectors.CoreAPICompatInspector',
  521. ],
  522. 'SECURITY_DEFINITIONS': {
  523. 'Bearer': {
  524. 'type': 'apiKey',
  525. 'name': 'Authorization',
  526. 'in': 'header',
  527. }
  528. },
  529. 'VALIDATOR_URL': None,
  530. }
  531. #
  532. # Django RQ (Webhooks backend)
  533. #
  534. if TASKS_REDIS_USING_SENTINEL:
  535. RQ_PARAMS = {
  536. 'SENTINELS': TASKS_REDIS_SENTINELS,
  537. 'MASTER_NAME': TASKS_REDIS_SENTINEL_SERVICE,
  538. 'DB': TASKS_REDIS_DATABASE,
  539. 'PASSWORD': TASKS_REDIS_PASSWORD,
  540. 'SOCKET_TIMEOUT': None,
  541. 'CONNECTION_KWARGS': {
  542. 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
  543. },
  544. }
  545. else:
  546. RQ_PARAMS = {
  547. 'HOST': TASKS_REDIS_HOST,
  548. 'PORT': TASKS_REDIS_PORT,
  549. 'DB': TASKS_REDIS_DATABASE,
  550. 'PASSWORD': TASKS_REDIS_PASSWORD,
  551. 'SSL': TASKS_REDIS_SSL,
  552. 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
  553. 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
  554. }
  555. RQ_QUEUES = {
  556. 'high': RQ_PARAMS,
  557. 'default': RQ_PARAMS,
  558. 'low': RQ_PARAMS,
  559. }
  560. #
  561. # Plugins
  562. #
  563. for plugin_name in PLUGINS:
  564. # Import plugin module
  565. try:
  566. plugin = importlib.import_module(plugin_name)
  567. except ModuleNotFoundError as e:
  568. if getattr(e, 'name') == plugin_name:
  569. raise ImproperlyConfigured(
  570. "Unable to import plugin {}: Module not found. Check that the plugin module has been installed within the "
  571. "correct Python environment.".format(plugin_name)
  572. )
  573. raise e
  574. # Determine plugin config and add to INSTALLED_APPS.
  575. try:
  576. plugin_config = plugin.config
  577. INSTALLED_APPS.append("{}.{}".format(plugin_config.__module__, plugin_config.__name__))
  578. except AttributeError:
  579. raise ImproperlyConfigured(
  580. "Plugin {} does not provide a 'config' variable. This should be defined in the plugin's __init__.py file "
  581. "and point to the PluginConfig subclass.".format(plugin_name)
  582. )
  583. # Validate user-provided configuration settings and assign defaults
  584. if plugin_name not in PLUGINS_CONFIG:
  585. PLUGINS_CONFIG[plugin_name] = {}
  586. plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION)
  587. # Add middleware
  588. plugin_middleware = plugin_config.middleware
  589. if plugin_middleware and type(plugin_middleware) in (list, tuple):
  590. MIDDLEWARE.extend(plugin_middleware)
  591. # Create RQ queues dedicated to the plugin
  592. # we use the plugin name as a prefix for queue name's defined in the plugin config
  593. # ex: mysuperplugin.mysuperqueue1
  594. if type(plugin_config.queues) is not list:
  595. raise ImproperlyConfigured(
  596. "Plugin {} queues must be a list.".format(plugin_name)
  597. )
  598. RQ_QUEUES.update({
  599. f"{plugin_name}.{queue}": RQ_PARAMS for queue in plugin_config.queues
  600. })