settings.py 25 KB

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