settings.py 23 KB

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