settings.py 21 KB

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