settings.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  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 urlencode, urlsplit
  9. import django
  10. import requests
  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 django.utils.translation import gettext_lazy as _
  16. from netbox.config import PARAMS as CONFIG_PARAMS
  17. from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
  18. from netbox.plugins import PluginConfig
  19. from utilities.string import trailing_slash
  20. #
  21. # Environment setup
  22. #
  23. VERSION = '4.0-beta1'
  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, 10):
  29. raise RuntimeError(
  30. f"NetBox requires Python 3.10 or later. (Currently installed: Python {platform.python_version()})"
  31. )
  32. #
  33. # Configuration import
  34. #
  35. # Import the configuration module
  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. # Check for missing 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 static config parameters
  51. ADMINS = getattr(configuration, 'ADMINS', [])
  52. ALLOW_TOKEN_RETRIEVAL = getattr(configuration, 'ALLOW_TOKEN_RETRIEVAL', True)
  53. ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS') # Required
  54. AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [])
  55. BASE_PATH = trailing_slash(getattr(configuration, 'BASE_PATH', ''))
  56. CHANGELOG_SKIP_EMPTY_CHANGES = getattr(configuration, 'CHANGELOG_SKIP_EMPTY_CHANGES', True)
  57. CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True)
  58. CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
  59. CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
  60. CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
  61. CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
  62. CSRF_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}'
  63. CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
  64. CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
  65. DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440)
  66. DATABASE = getattr(configuration, 'DATABASE') # Required
  67. DEBUG = getattr(configuration, 'DEBUG', False)
  68. DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None)
  69. DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
  70. # Permit users to manage their own bookmarks
  71. 'extras.view_bookmark': ({'user': '$user'},),
  72. 'extras.add_bookmark': ({'user': '$user'},),
  73. 'extras.change_bookmark': ({'user': '$user'},),
  74. 'extras.delete_bookmark': ({'user': '$user'},),
  75. # Permit users to manage their own API tokens
  76. 'users.view_token': ({'user': '$user'},),
  77. 'users.add_token': ({'user': '$user'},),
  78. 'users.change_token': ({'user': '$user'},),
  79. 'users.delete_token': ({'user': '$user'},),
  80. })
  81. DEVELOPER = getattr(configuration, 'DEVELOPER', False)
  82. DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False)
  83. DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
  84. EMAIL = getattr(configuration, 'EMAIL', {})
  85. EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
  86. 'extras.events.process_event_queue',
  87. ))
  88. EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
  89. FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
  90. FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
  91. HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
  92. INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
  93. JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
  94. LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us')
  95. LANGUAGE_COOKIE_PATH = CSRF_COOKIE_PATH
  96. LOGGING = getattr(configuration, 'LOGGING', {})
  97. LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
  98. LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
  99. LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
  100. LOGOUT_REDIRECT_URL = getattr(configuration, 'LOGOUT_REDIRECT_URL', 'home')
  101. MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
  102. METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
  103. PLUGINS = getattr(configuration, 'PLUGINS', [])
  104. PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
  105. QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
  106. REDIS = getattr(configuration, 'REDIS') # Required
  107. RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
  108. REMOTE_AUTH_AUTO_CREATE_GROUPS = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_GROUPS', False)
  109. REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
  110. REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
  111. REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
  112. REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})
  113. REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False)
  114. REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP')
  115. REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
  116. REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False)
  117. REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
  118. REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', [])
  119. REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', [])
  120. REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL')
  121. REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME')
  122. REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME')
  123. REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', [])
  124. REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
  125. # Required by extras/migrations/0109_script_models.py
  126. REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
  127. RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
  128. RQ_RETRY_INTERVAL = getattr(configuration, 'RQ_RETRY_INTERVAL', 60)
  129. RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
  130. SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
  131. SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
  132. SECRET_KEY = getattr(configuration, 'SECRET_KEY') # Required
  133. SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
  134. SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
  135. SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
  136. SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
  137. SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
  138. SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
  139. SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
  140. SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
  141. SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
  142. SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
  143. STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
  144. STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
  145. TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
  146. # Load any dynamic configuration parameters which have been hard-coded in the configuration file
  147. for param in CONFIG_PARAMS:
  148. if hasattr(configuration, param.name):
  149. globals()[param.name] = getattr(configuration, param.name)
  150. # Enforce minimum length for SECRET_KEY
  151. if type(SECRET_KEY) is not str:
  152. raise ImproperlyConfigured(f"SECRET_KEY must be a string (found {type(SECRET_KEY).__name__})")
  153. if len(SECRET_KEY) < 50:
  154. raise ImproperlyConfigured(
  155. f"SECRET_KEY must be at least 50 characters in length. To generate a suitable key, run the following command:\n"
  156. f" python {BASE_DIR}/generate_secret_key.py"
  157. )
  158. # Validate update repo URL and timeout
  159. if RELEASE_CHECK_URL:
  160. try:
  161. URLValidator()(RELEASE_CHECK_URL)
  162. except ValidationError as e:
  163. raise ImproperlyConfigured(
  164. "RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
  165. )
  166. #
  167. # Database
  168. #
  169. # Set the database engine
  170. if 'ENGINE' not in DATABASE:
  171. if METRICS_ENABLED:
  172. DATABASE.update({'ENGINE': 'django_prometheus.db.backends.postgresql'})
  173. else:
  174. DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})
  175. # Define the DATABASES setting for Django
  176. DATABASES = {
  177. 'default': DATABASE,
  178. }
  179. #
  180. # Storage backend
  181. #
  182. if STORAGE_BACKEND is not None:
  183. DEFAULT_FILE_STORAGE = STORAGE_BACKEND
  184. # django-storages
  185. if STORAGE_BACKEND.startswith('storages.'):
  186. try:
  187. import storages.utils # type: ignore
  188. except ModuleNotFoundError as e:
  189. if getattr(e, 'name') == 'storages':
  190. raise ImproperlyConfigured(
  191. f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storages is not present. It can be "
  192. f"installed by running 'pip install django-storages'."
  193. )
  194. raise e
  195. # Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
  196. def _setting(name, default=None):
  197. if name in STORAGE_CONFIG:
  198. return STORAGE_CONFIG[name]
  199. return globals().get(name, default)
  200. storages.utils.setting = _setting
  201. if STORAGE_CONFIG and STORAGE_BACKEND is None:
  202. warnings.warn(
  203. "STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
  204. "ignored."
  205. )
  206. #
  207. # Redis
  208. #
  209. # Background task queuing
  210. if 'tasks' not in REDIS:
  211. raise ImproperlyConfigured("REDIS section in configuration.py is missing the 'tasks' subsection.")
  212. TASKS_REDIS = REDIS['tasks']
  213. TASKS_REDIS_HOST = TASKS_REDIS.get('HOST', 'localhost')
  214. TASKS_REDIS_PORT = TASKS_REDIS.get('PORT', 6379)
  215. TASKS_REDIS_SENTINELS = TASKS_REDIS.get('SENTINELS', [])
  216. TASKS_REDIS_USING_SENTINEL = all([
  217. isinstance(TASKS_REDIS_SENTINELS, (list, tuple)),
  218. len(TASKS_REDIS_SENTINELS) > 0
  219. ])
  220. TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
  221. TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
  222. TASKS_REDIS_USERNAME = TASKS_REDIS.get('USERNAME', '')
  223. TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
  224. TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
  225. TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
  226. TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
  227. TASKS_REDIS_CA_CERT_PATH = TASKS_REDIS.get('CA_CERT_PATH', False)
  228. # Caching
  229. if 'caching' not in REDIS:
  230. raise ImproperlyConfigured("REDIS section in configuration.py is missing caching subsection.")
  231. CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost')
  232. CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379)
  233. CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0)
  234. CACHING_REDIS_USERNAME = REDIS['caching'].get('USERNAME', '')
  235. CACHING_REDIS_USERNAME_HOST = '@'.join(filter(None, [CACHING_REDIS_USERNAME, CACHING_REDIS_HOST]))
  236. CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '')
  237. CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
  238. CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
  239. CACHING_REDIS_PROTO = 'rediss' if REDIS['caching'].get('SSL', False) else 'redis'
  240. CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY', False)
  241. CACHING_REDIS_CA_CERT_PATH = REDIS['caching'].get('CA_CERT_PATH', False)
  242. CACHING_REDIS_URL = f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}'
  243. # Configure Django's default cache to use Redis
  244. CACHES = {
  245. 'default': {
  246. 'BACKEND': 'django_redis.cache.RedisCache',
  247. 'LOCATION': CACHING_REDIS_URL,
  248. 'OPTIONS': {
  249. 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
  250. 'PASSWORD': CACHING_REDIS_PASSWORD,
  251. }
  252. }
  253. }
  254. if CACHING_REDIS_SENTINELS:
  255. DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory'
  256. CACHES['default']['LOCATION'] = f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_SENTINEL_SERVICE}/{CACHING_REDIS_DATABASE}'
  257. CACHES['default']['OPTIONS']['CLIENT_CLASS'] = 'django_redis.client.SentinelClient'
  258. CACHES['default']['OPTIONS']['SENTINELS'] = CACHING_REDIS_SENTINELS
  259. if CACHING_REDIS_SKIP_TLS_VERIFY:
  260. CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
  261. CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_cert_reqs'] = False
  262. if CACHING_REDIS_CA_CERT_PATH:
  263. CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {})
  264. CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_ca_certs'] = CACHING_REDIS_CA_CERT_PATH
  265. #
  266. # Sessions
  267. #
  268. if LOGIN_TIMEOUT is not None:
  269. # Django default is 1209600 seconds (14 days)
  270. SESSION_COOKIE_AGE = LOGIN_TIMEOUT
  271. SESSION_SAVE_EVERY_REQUEST = bool(LOGIN_PERSISTENCE)
  272. if SESSION_FILE_PATH is not None:
  273. SESSION_ENGINE = 'django.contrib.sessions.backends.file'
  274. #
  275. # Email
  276. #
  277. EMAIL_HOST = EMAIL.get('SERVER')
  278. EMAIL_HOST_USER = EMAIL.get('USERNAME')
  279. EMAIL_HOST_PASSWORD = EMAIL.get('PASSWORD')
  280. EMAIL_PORT = EMAIL.get('PORT', 25)
  281. EMAIL_SSL_CERTFILE = EMAIL.get('SSL_CERTFILE')
  282. EMAIL_SSL_KEYFILE = EMAIL.get('SSL_KEYFILE')
  283. EMAIL_SUBJECT_PREFIX = '[NetBox] '
  284. EMAIL_USE_SSL = EMAIL.get('USE_SSL', False)
  285. EMAIL_USE_TLS = EMAIL.get('USE_TLS', False)
  286. EMAIL_TIMEOUT = EMAIL.get('TIMEOUT', 10)
  287. SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
  288. #
  289. # Django core settings
  290. #
  291. INSTALLED_APPS = [
  292. 'django.contrib.admin',
  293. 'django.contrib.auth',
  294. 'django.contrib.contenttypes',
  295. 'django.contrib.sessions',
  296. 'django.contrib.messages',
  297. 'django.contrib.staticfiles',
  298. 'django.contrib.humanize',
  299. 'django.forms',
  300. 'corsheaders',
  301. 'debug_toolbar',
  302. 'django_filters',
  303. 'django_htmx',
  304. 'django_tables2',
  305. 'django_prometheus',
  306. 'strawberry_django',
  307. 'mptt',
  308. 'rest_framework',
  309. 'social_django',
  310. 'taggit',
  311. 'timezone_field',
  312. 'core',
  313. 'account',
  314. 'circuits',
  315. 'dcim',
  316. 'ipam',
  317. 'extras',
  318. 'tenancy',
  319. 'users',
  320. 'utilities',
  321. 'virtualization',
  322. 'vpn',
  323. 'wireless',
  324. 'django_rq', # Must come after extras to allow overriding management commands
  325. 'drf_spectacular',
  326. 'drf_spectacular_sidecar',
  327. ]
  328. if not DJANGO_ADMIN_ENABLED:
  329. INSTALLED_APPS.remove('django.contrib.admin')
  330. # Middleware
  331. MIDDLEWARE = [
  332. "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
  333. 'django_prometheus.middleware.PrometheusBeforeMiddleware',
  334. 'corsheaders.middleware.CorsMiddleware',
  335. 'django.contrib.sessions.middleware.SessionMiddleware',
  336. 'django.middleware.locale.LocaleMiddleware',
  337. 'django.middleware.common.CommonMiddleware',
  338. 'django.middleware.csrf.CsrfViewMiddleware',
  339. 'django.contrib.auth.middleware.AuthenticationMiddleware',
  340. 'django.contrib.messages.middleware.MessageMiddleware',
  341. 'django.middleware.clickjacking.XFrameOptionsMiddleware',
  342. 'django.middleware.security.SecurityMiddleware',
  343. 'django_htmx.middleware.HtmxMiddleware',
  344. 'netbox.middleware.RemoteUserMiddleware',
  345. 'netbox.middleware.CoreMiddleware',
  346. 'netbox.middleware.MaintenanceModeMiddleware',
  347. 'django_prometheus.middleware.PrometheusAfterMiddleware',
  348. ]
  349. # URLs
  350. ROOT_URLCONF = 'netbox.urls'
  351. # Templates
  352. TEMPLATES_DIR = BASE_DIR + '/templates'
  353. TEMPLATES = [
  354. {
  355. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  356. 'DIRS': [TEMPLATES_DIR],
  357. 'APP_DIRS': True,
  358. 'OPTIONS': {
  359. 'builtins': [
  360. 'utilities.templatetags.builtins.filters',
  361. 'utilities.templatetags.builtins.tags',
  362. ],
  363. 'context_processors': [
  364. 'django.template.context_processors.debug',
  365. 'django.template.context_processors.request',
  366. 'django.template.context_processors.media',
  367. 'django.contrib.auth.context_processors.auth',
  368. 'django.contrib.messages.context_processors.messages',
  369. 'netbox.context_processors.settings_and_registry',
  370. ],
  371. },
  372. },
  373. ]
  374. # This allows us to override Django's stock form widget templates
  375. FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
  376. # Set up authentication backends
  377. if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
  378. REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
  379. AUTHENTICATION_BACKENDS = [
  380. *REMOTE_AUTH_BACKEND,
  381. 'netbox.authentication.ObjectPermissionBackend',
  382. ]
  383. # Use our custom User model
  384. AUTH_USER_MODEL = 'users.User'
  385. # Authentication URLs
  386. LOGIN_URL = f'/{BASE_PATH}login/'
  387. LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
  388. # Use timezone-aware datetime objects
  389. USE_TZ = True
  390. # WSGI
  391. WSGI_APPLICATION = 'netbox.wsgi.application'
  392. SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
  393. USE_X_FORWARDED_HOST = True
  394. X_FRAME_OPTIONS = 'SAMEORIGIN'
  395. # Static files (CSS, JavaScript, Images)
  396. STATIC_ROOT = BASE_DIR + '/static'
  397. STATIC_URL = f'/{BASE_PATH}static/'
  398. STATICFILES_DIRS = (
  399. os.path.join(BASE_DIR, 'project-static', 'dist'),
  400. os.path.join(BASE_DIR, 'project-static', 'img'),
  401. os.path.join(BASE_DIR, 'project-static', 'js'),
  402. ('docs', os.path.join(BASE_DIR, 'project-static', 'docs')), # Prefix with /docs
  403. )
  404. # Media URL
  405. MEDIA_URL = f'/{BASE_PATH}media/'
  406. # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
  407. DATA_UPLOAD_MAX_NUMBER_FIELDS = None
  408. # Messages
  409. MESSAGE_TAGS = {
  410. messages.ERROR: 'danger',
  411. }
  412. DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
  413. SERIALIZATION_MODULES = {
  414. 'json': 'utilities.serializers.json',
  415. }
  416. #
  417. # Permissions & authentication
  418. #
  419. # Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
  420. # by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
  421. EXEMPT_EXCLUDE_MODELS = (
  422. ('auth', 'group'),
  423. ('auth', 'user'),
  424. ('extras', 'configrevision'),
  425. ('users', 'objectpermission'),
  426. ('users', 'token'),
  427. )
  428. # All URLs starting with a string listed here are exempt from login enforcement
  429. AUTH_EXEMPT_PATHS = (
  430. f'/{BASE_PATH}api/',
  431. f'/{BASE_PATH}graphql/',
  432. f'/{BASE_PATH}login/',
  433. f'/{BASE_PATH}oauth/',
  434. f'/{BASE_PATH}metrics',
  435. )
  436. # All URLs starting with a string listed here are exempt from maintenance mode enforcement
  437. MAINTENANCE_EXEMPT_PATHS = (
  438. f'/{BASE_PATH}admin/',
  439. f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
  440. LOGIN_URL,
  441. LOGIN_REDIRECT_URL,
  442. LOGOUT_REDIRECT_URL
  443. )
  444. #
  445. # Sentry
  446. #
  447. if SENTRY_ENABLED:
  448. try:
  449. import sentry_sdk
  450. except ModuleNotFoundError:
  451. raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.")
  452. if not SENTRY_DSN:
  453. raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
  454. # Initialize the SDK
  455. sentry_sdk.init(
  456. dsn=SENTRY_DSN,
  457. release=VERSION,
  458. integrations=[sentry_sdk.integrations.django.DjangoIntegration()],
  459. sample_rate=SENTRY_SAMPLE_RATE,
  460. traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
  461. send_default_pii=True,
  462. http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
  463. https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
  464. )
  465. # Assign any configured tags
  466. for k, v in SENTRY_TAGS.items():
  467. sentry_sdk.set_tag(k, v)
  468. #
  469. # Census collection
  470. #
  471. # Calculate a unique deployment ID from the secret key
  472. DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
  473. CENSUS_URL = 'https://census.netbox.dev/api/v1/'
  474. CENSUS_PARAMS = {
  475. 'version': VERSION,
  476. 'python_version': sys.version.split()[0],
  477. 'deployment_id': DEPLOYMENT_ID,
  478. }
  479. if CENSUS_REPORTING_ENABLED and not DEBUG and 'test' not in sys.argv:
  480. try:
  481. # Report anonymous census data
  482. requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
  483. except requests.exceptions.RequestException:
  484. pass
  485. #
  486. # Django social auth
  487. #
  488. SOCIAL_AUTH_PIPELINE = (
  489. 'social_core.pipeline.social_auth.social_details',
  490. 'social_core.pipeline.social_auth.social_uid',
  491. 'social_core.pipeline.social_auth.social_user',
  492. 'social_core.pipeline.user.get_username',
  493. 'social_core.pipeline.user.create_user',
  494. 'social_core.pipeline.social_auth.associate_user',
  495. 'netbox.authentication.user_default_groups_handler',
  496. 'social_core.pipeline.social_auth.load_extra_data',
  497. 'social_core.pipeline.user.user_details',
  498. )
  499. # Load all SOCIAL_AUTH_* settings from the user configuration
  500. for param in dir(configuration):
  501. if param.startswith('SOCIAL_AUTH_'):
  502. globals()[param] = getattr(configuration, param)
  503. # Force usage of PostgreSQL's JSONB field for extra data
  504. SOCIAL_AUTH_JSONFIELD_ENABLED = True
  505. SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'
  506. SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL
  507. #
  508. # Django Prometheus
  509. #
  510. PROMETHEUS_EXPORT_MIGRATIONS = False
  511. #
  512. # Django filters
  513. #
  514. FILTERS_NULL_CHOICE_LABEL = 'None'
  515. FILTERS_NULL_CHOICE_VALUE = 'null'
  516. #
  517. # Django REST framework (API)
  518. #
  519. REST_FRAMEWORK_VERSION = '.'.join(VERSION.split('-')[0].split('.')[:2]) # Use major.minor as API version
  520. REST_FRAMEWORK = {
  521. 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
  522. 'COERCE_DECIMAL_TO_STRING': False,
  523. 'DEFAULT_AUTHENTICATION_CLASSES': (
  524. 'rest_framework.authentication.SessionAuthentication',
  525. 'netbox.api.authentication.TokenAuthentication',
  526. ),
  527. 'DEFAULT_FILTER_BACKENDS': (
  528. 'django_filters.rest_framework.DjangoFilterBackend',
  529. 'rest_framework.filters.OrderingFilter',
  530. ),
  531. 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata',
  532. 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
  533. 'DEFAULT_PARSER_CLASSES': (
  534. 'rest_framework.parsers.JSONParser',
  535. 'rest_framework.parsers.MultiPartParser',
  536. ),
  537. 'DEFAULT_PERMISSION_CLASSES': (
  538. 'netbox.api.authentication.TokenPermissions',
  539. ),
  540. 'DEFAULT_RENDERER_CLASSES': (
  541. 'rest_framework.renderers.JSONRenderer',
  542. 'netbox.api.renderers.FormlessBrowsableAPIRenderer',
  543. ),
  544. 'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema',
  545. 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
  546. 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
  547. 'SCHEMA_COERCE_METHOD_NAMES': {
  548. # Default mappings
  549. 'retrieve': 'read',
  550. 'destroy': 'delete',
  551. # Custom operations
  552. 'bulk_destroy': 'bulk_delete',
  553. },
  554. 'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
  555. }
  556. #
  557. # DRF Spectacular
  558. #
  559. SPECTACULAR_SETTINGS = {
  560. 'TITLE': 'NetBox REST API',
  561. 'LICENSE': {'name': 'Apache v2 License'},
  562. 'VERSION': VERSION,
  563. 'COMPONENT_SPLIT_REQUEST': True,
  564. 'REDOC_DIST': 'SIDECAR',
  565. 'SERVERS': [{
  566. 'url': BASE_PATH,
  567. 'description': 'NetBox',
  568. }],
  569. 'SWAGGER_UI_DIST': 'SIDECAR',
  570. 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
  571. 'POSTPROCESSING_HOOKS': [],
  572. }
  573. #
  574. # Django RQ (events backend)
  575. #
  576. if TASKS_REDIS_USING_SENTINEL:
  577. RQ_PARAMS = {
  578. 'SENTINELS': TASKS_REDIS_SENTINELS,
  579. 'MASTER_NAME': TASKS_REDIS_SENTINEL_SERVICE,
  580. 'SOCKET_TIMEOUT': None,
  581. 'CONNECTION_KWARGS': {
  582. 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
  583. },
  584. }
  585. else:
  586. RQ_PARAMS = {
  587. 'HOST': TASKS_REDIS_HOST,
  588. 'PORT': TASKS_REDIS_PORT,
  589. 'SSL': TASKS_REDIS_SSL,
  590. 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
  591. }
  592. RQ_PARAMS.update({
  593. 'DB': TASKS_REDIS_DATABASE,
  594. 'USERNAME': TASKS_REDIS_USERNAME,
  595. 'PASSWORD': TASKS_REDIS_PASSWORD,
  596. 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
  597. })
  598. if TASKS_REDIS_CA_CERT_PATH:
  599. RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {})
  600. RQ_PARAMS['REDIS_CLIENT_KWARGS']['ssl_ca_certs'] = TASKS_REDIS_CA_CERT_PATH
  601. # Define named RQ queues
  602. RQ_QUEUES = {
  603. RQ_QUEUE_HIGH: RQ_PARAMS,
  604. RQ_QUEUE_DEFAULT: RQ_PARAMS,
  605. RQ_QUEUE_LOW: RQ_PARAMS,
  606. }
  607. # Add any queues defined in QUEUE_MAPPINGS
  608. RQ_QUEUES.update({
  609. queue: RQ_PARAMS for queue in set(QUEUE_MAPPINGS.values()) if queue not in RQ_QUEUES
  610. })
  611. #
  612. # Localization
  613. #
  614. # Supported translation languages
  615. LANGUAGES = (
  616. ('en', _('English')),
  617. ('es', _('Spanish')),
  618. ('fr', _('French')),
  619. ('ja', _('Japanese')),
  620. ('pt', _('Portuguese')),
  621. ('ru', _('Russian')),
  622. ('tr', _('Turkish')),
  623. )
  624. LOCALE_PATHS = (
  625. BASE_DIR + '/translations',
  626. )
  627. #
  628. # Strawberry (GraphQL)
  629. #
  630. STRAWBERRY_DJANGO = {
  631. "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
  632. "USE_DEPRECATED_FILTERS": True,
  633. }
  634. #
  635. # Plugins
  636. #
  637. # Register any configured plugins
  638. for plugin_name in PLUGINS:
  639. try:
  640. # Import the plugin module
  641. plugin = importlib.import_module(plugin_name)
  642. except ModuleNotFoundError as e:
  643. if getattr(e, 'name') == plugin_name:
  644. raise ImproperlyConfigured(
  645. f"Unable to import plugin {plugin_name}: Module not found. Check that the plugin module has been "
  646. f"installed within the correct Python environment."
  647. )
  648. raise e
  649. try:
  650. # Load the PluginConfig
  651. plugin_config: PluginConfig = plugin.config
  652. except AttributeError:
  653. raise ImproperlyConfigured(
  654. f"Plugin {plugin_name} does not provide a 'config' variable. This should be defined in the plugin's "
  655. f"__init__.py file and point to the PluginConfig subclass."
  656. )
  657. plugin_module = "{}.{}".format(plugin_config.__module__, plugin_config.__name__) # type: ignore
  658. # Gather additional apps to load alongside this plugin
  659. django_apps = plugin_config.django_apps
  660. if plugin_name in django_apps:
  661. django_apps.pop(plugin_name)
  662. if plugin_module not in django_apps:
  663. django_apps.append(plugin_module)
  664. # Test if we can import all modules (or its parent, for PluginConfigs and AppConfigs)
  665. for app in django_apps:
  666. if "." in app:
  667. parts = app.split(".")
  668. spec = importlib.util.find_spec(".".join(parts[:-1]))
  669. else:
  670. spec = importlib.util.find_spec(app)
  671. if spec is None:
  672. raise ImproperlyConfigured(
  673. f"Failed to load django_apps specified by plugin {plugin_name}: {django_apps} "
  674. f"The module {app} cannot be imported. Check that the necessary package has been "
  675. f"installed within the correct Python environment."
  676. )
  677. INSTALLED_APPS.extend(django_apps)
  678. # Preserve uniqueness of the INSTALLED_APPS list, we keep the last occurrence
  679. sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
  680. INSTALLED_APPS = list(sorted_apps)
  681. # Validate user-provided configuration settings and assign defaults
  682. if plugin_name not in PLUGINS_CONFIG:
  683. PLUGINS_CONFIG[plugin_name] = {}
  684. plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION)
  685. # Add middleware
  686. plugin_middleware = plugin_config.middleware
  687. if plugin_middleware and type(plugin_middleware) in (list, tuple):
  688. MIDDLEWARE.extend(plugin_middleware)
  689. # Create RQ queues dedicated to the plugin
  690. # we use the plugin name as a prefix for queue name's defined in the plugin config
  691. # ex: mysuperplugin.mysuperqueue1
  692. if type(plugin_config.queues) is not list:
  693. raise ImproperlyConfigured(f"Plugin {plugin_name} queues must be a list.")
  694. RQ_QUEUES.update({
  695. f"{plugin_name}.{queue}": RQ_PARAMS for queue in plugin_config.queues
  696. })