settings.py 22 KB

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