settings.py 26 KB

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