Coverage for netbox/settings.py: 63%

406 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import hashlib 

2import importlib 

3import importlib.util 

4import os 

5import platform 

6import sys 

7import warnings 

8 

9import storages.utils # type: ignore 

10from django.contrib.messages import constants as messages 

11from django.core.exceptions import ImproperlyConfigured, ValidationError 

12from django.core.validators import URLValidator 

13from django.utils.module_loading import import_string 

14from django.utils.translation import gettext_lazy as _ 

15 

16from core.exceptions import IncompatiblePluginError 

17from netbox.config import PARAMS as CONFIG_PARAMS 

18from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW 

19from netbox.plugins import PluginConfig 

20from netbox.registry import registry 

21from netbox.settings_utils import ( 

22 get_configuration_dir, 

23 load_configuration, 

24 parse_job_timeout, 

25 resolve_install_paths, 

26 secret_key_hint, 

27 validate_webhook_default_timeout, 

28) 

29from utilities.release import load_release_data 

30from utilities.security import validate_peppers 

31from utilities.string import trailing_slash 

32 

33from .monkey import get_unique_validators 

34 

35# 

36# Environment setup 

37# 

38 

39RELEASE = load_release_data() 

40VERSION = RELEASE.full_version # Retained for backward compatibility 

41# Settings package directory (settings.py lives here in both checkout & wheel). 

42_SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__)) 

43 

44# All wheel-vs-checkout path branching is centralized in resolve_install_paths(): a wheel 

45# bundles package data under netbox/_data and keeps mutable instance files under an external 

46# instance root (NETBOX_ROOT, default /opt/netbox); a checkout keeps both roots as the project 

47# directory, so archive/git behavior is unchanged. 

48_PATHS = resolve_install_paths(_SETTINGS_DIR, os.environ) 

49NETBOX_INSTALL_MODE = _PATHS.install_mode 

50BASE_DIR = _PATHS.base_dir 

51# Instance root for wheel installs (holds conf/, media/, reports/, scripts/, static/, units). 

52NETBOX_ROOT = _PATHS.netbox_root 

53 

54# Validate the Python version 

55if sys.version_info < (3, 12): # noqa: UP036 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 raise RuntimeError( 

57 f"NetBox requires Python 3.12 or later. (Currently installed: Python {platform.python_version()})" 

58 ) 

59 

60# 

61# Configuration import 

62# 

63 

64# Import the configuration module (wheel mode prefers NETBOX_ROOT/conf/configuration.py). 

65configuration = load_configuration( 

66 install_mode=NETBOX_INSTALL_MODE, 

67 install_root=NETBOX_ROOT, 

68 environ=os.environ, 

69) 

70 

71# The directory holding the active configuration.py; ldap_config.py lives beside it. 

72CONFIGURATION_DIR = get_configuration_dir(configuration) 

73 

74# Check for missing/conflicting required configuration parameters 

75for parameter in ('ALLOWED_HOSTS', 'SECRET_KEY', 'REDIS'): 

76 if not hasattr(configuration, parameter): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 raise ImproperlyConfigured(f"Required parameter {parameter} is missing from configuration.") 

78if not hasattr(configuration, 'DATABASE') and not hasattr(configuration, 'DATABASES'): 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true

79 raise ImproperlyConfigured("The database configuration must be defined using DATABASE or DATABASES.") 

80elif hasattr(configuration, 'DATABASE') and hasattr(configuration, 'DATABASES'): 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true

81 raise ImproperlyConfigured("DATABASE and DATABASES may not be set together. The use of DATABASES is encouraged.") 

82 

83# Set static config parameters 

84ADMINS = getattr(configuration, 'ADMINS', []) 

85ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS') # Required 

86API_TOKEN_PEPPERS = getattr(configuration, 'API_TOKEN_PEPPERS', {}) 

87AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [ 

88 { 

89 "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 

90 "OPTIONS": { 

91 "min_length": 12, 

92 }, 

93 }, 

94 { 

95 "NAME": "utilities.password_validation.AlphanumericPasswordValidator", 

96 }, 

97]) 

98BASE_PATH = trailing_slash(getattr(configuration, 'BASE_PATH', '')) 

99BULK_UPDATE_CHUNK_SIZE = getattr(configuration, 'BULK_UPDATE_CHUNK_SIZE', 5000) 

100if BULK_UPDATE_CHUNK_SIZE is not None and (type(BULK_UPDATE_CHUNK_SIZE) is not int or BULK_UPDATE_CHUNK_SIZE < 1): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 raise ImproperlyConfigured( 

102 f"BULK_UPDATE_CHUNK_SIZE must be a positive integer or None (found {BULK_UPDATE_CHUNK_SIZE!r})" 

103 ) 

104CHANGELOG_SKIP_EMPTY_CHANGES = getattr(configuration, 'CHANGELOG_SKIP_EMPTY_CHANGES', True) 

105CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True) 

106CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False) 

107CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) 

108CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) 

109CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') 

110CSRF_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}' 

111CSRF_COOKIE_HTTPONLY = True 

112CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False) 

113CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) 

114DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440) 

115DATABASE = getattr(configuration, 'DATABASE', None) # Legacy DB definition 

116DATABASE_ROUTERS = getattr(configuration, 'DATABASE_ROUTERS', []) 

117DATABASES = getattr(configuration, 'DATABASES', {'default': DATABASE}) 

118DEBUG = getattr(configuration, 'DEBUG', False) 

119DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None) 

120DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', { 

121 # Permit users to manage their own bookmarks 

122 'extras.view_bookmark': ({'user': '$user'},), 

123 'extras.add_bookmark': ({'user': '$user'},), 

124 'extras.change_bookmark': ({'user': '$user'},), 

125 'extras.delete_bookmark': ({'user': '$user'},), 

126 # Permit users to manage their own notifications 

127 'extras.view_notification': ({'user': '$user'},), 

128 'extras.add_notification': ({'user': '$user'},), 

129 'extras.change_notification': ({'user': '$user'},), 

130 'extras.delete_notification': ({'user': '$user'},), 

131 # Permit users to manage their own subscriptions 

132 'extras.view_subscription': ({'user': '$user'},), 

133 'extras.add_subscription': ({'user': '$user'},), 

134 'extras.change_subscription': ({'user': '$user'},), 

135 'extras.delete_subscription': ({'user': '$user'},), 

136 # Permit users to manage their own API tokens 

137 'users.view_token': ({'user': '$user'},), 

138 'users.add_token': ({'user': '$user'},), 

139 'users.change_token': ({'user': '$user'},), 

140 'users.delete_token': ({'user': '$user'},), 

141}) 

142DEVELOPER = getattr(configuration, 'DEVELOPER', False) 

143DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', _PATHS.docs_root) 

144EMAIL = getattr(configuration, 'EMAIL', {}) 

145STREAMING_EXPORTS = getattr(configuration, 'STREAMING_EXPORTS', False) 

146EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', [ 

147 'extras.events.process_event_queue', 

148]) 

149EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) 

150FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {}) 

151FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440) 

152GRAPHQL_DEFAULT_VERSION = getattr(configuration, 'GRAPHQL_DEFAULT_VERSION', 1) 

153GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10) 

154GRAPHQL_MAX_QUERY_DEPTH = getattr(configuration, 'GRAPHQL_MAX_QUERY_DEPTH', None) 

155HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node()) 

156HTTP_CLIENT_IP_HEADERS = getattr(configuration, 'HTTP_CLIENT_IP_HEADERS', ( 

157 'HTTP_X_REAL_IP', 

158 'HTTP_X_FORWARDED_FOR', 

159 'REMOTE_ADDR', 

160)) 

161HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {}) 

162INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) 

163ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False) 

164JINJA_ENVIRONMENT_PARAMS = getattr(configuration, 'JINJA_ENVIRONMENT_PARAMS', []) 

165JINJA_FILTERS = getattr(configuration, 'JINJA_FILTERS', getattr(configuration, 'JINJA2_FILTERS', {})) 

166LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us') 

167LANGUAGE_COOKIE_PATH = CSRF_COOKIE_PATH 

168LOGGING = getattr(configuration, 'LOGGING', {}) 

169LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False) 

170LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', True) 

171LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None) 

172LOGIN_FORM_HIDDEN = getattr(configuration, 'LOGIN_FORM_HIDDEN', False) 

173LOGOUT_REDIRECT_URL = getattr(configuration, 'LOGOUT_REDIRECT_URL', 'home') 

174MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(NETBOX_ROOT, 'media')).rstrip('/') 

175METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False) 

176PLUGINS = getattr(configuration, 'PLUGINS', []) 

177PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {}) 

178PLUGINS_CATALOG_CONFIG = getattr(configuration, 'PLUGINS_CATALOG_CONFIG', {}) 

179PROXY_ROUTERS = getattr(configuration, 'PROXY_ROUTERS', ['utilities.proxy.DefaultProxyRouter']) 

180QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {}) 

181REDIS = getattr(configuration, 'REDIS') # Required 

182RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None) 

183REMOTE_AUTH_AUTO_CREATE_GROUPS = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_GROUPS', False) 

184REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False) 

185REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend') 

186REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', []) 

187REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {}) 

188REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False) 

189REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP') 

190REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|') 

191REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False) 

192REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') 

193REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', []) 

194REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', []) 

195REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL') 

196REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME') 

197REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME') 

198# Required by extras/migrations/0109_script_models.py 

199REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(NETBOX_ROOT, 'reports')).rstrip('/') 

200RQ = getattr(configuration, 'RQ', {}) 

201if 'WORKER_CLASS' in RQ and RQ['WORKER_CLASS'] != 'utilities.rqworker.NetBoxRQWorker': 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 warnings.warn( 

203 f"RQ['WORKER_CLASS'] is set to {RQ['WORKER_CLASS']!r}; NetBoxRQWorker's self-healing heartbeat " 

204 f"logic will not be applied. Workers may not automatically recover from a Redis outage." 

205 ) 

206else: 

207 RQ.setdefault('WORKER_CLASS', 'utilities.rqworker.NetBoxRQWorker') 

208RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) 

209RQ_RETRY_INTERVAL = getattr(configuration, 'RQ_RETRY_INTERVAL', 60) 

210RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0) 

211SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(NETBOX_ROOT, 'scripts')).rstrip('/') 

212SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend') 

213SECRET_KEY = getattr(configuration, 'SECRET_KEY') # Required 

214SECURE_HSTS_INCLUDE_SUBDOMAINS = getattr(configuration, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', False) 

215SECURE_HSTS_PRELOAD = getattr(configuration, 'SECURE_HSTS_PRELOAD', False) 

216SECURE_HSTS_SECONDS = getattr(configuration, 'SECURE_HSTS_SECONDS', 0) 

217SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False) 

218SENTRY_CONFIG = getattr(configuration, 'SENTRY_CONFIG', {}) 

219SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False) 

220SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {}) 

221SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') 

222SESSION_COOKIE_PATH = CSRF_COOKIE_PATH 

223SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False) 

224SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) 

225STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None) 

226STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', None) 

227STORAGES = getattr(configuration, 'STORAGES', {}) 

228TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC') 

229TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True) 

230WEBHOOK_DEFAULT_TIMEOUT = getattr(configuration, 'WEBHOOK_DEFAULT_TIMEOUT', 60) 

231validate_webhook_default_timeout(WEBHOOK_DEFAULT_TIMEOUT, parse_job_timeout(RQ_DEFAULT_TIMEOUT)) 

232DISK_BASE_UNIT = getattr(configuration, 'DISK_BASE_UNIT', 1000) 

233if DISK_BASE_UNIT not in [1000, 1024]: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 raise ImproperlyConfigured(f"DISK_BASE_UNIT must be 1000 or 1024 (found {DISK_BASE_UNIT})") 

235RAM_BASE_UNIT = getattr(configuration, 'RAM_BASE_UNIT', 1000) 

236if RAM_BASE_UNIT not in [1000, 1024]: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 raise ImproperlyConfigured(f"RAM_BASE_UNIT must be 1000 or 1024 (found {RAM_BASE_UNIT})") 

238 

239# Load any dynamic configuration parameters which have been hard-coded in the configuration file 

240for param in CONFIG_PARAMS: 

241 if hasattr(configuration, param.name): 

242 globals()[param.name] = getattr(configuration, param.name) 

243 

244# Enforce minimum length for SECRET_KEY 

245if type(SECRET_KEY) is not str: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 raise ImproperlyConfigured(f"SECRET_KEY must be a string (found {type(SECRET_KEY).__name__})") 

247if len(SECRET_KEY) < 50: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 raise ImproperlyConfigured( 

249 f"SECRET_KEY must be at least 50 characters in length. To generate a suitable key, run the following command:\n" 

250 f" {secret_key_hint(NETBOX_INSTALL_MODE, BASE_DIR)}" 

251 ) 

252 

253# Validate API token peppers 

254if API_TOKEN_PEPPERS: 254 ↛ 257line 254 didn't jump to line 257 because the condition on line 254 was always true

255 validate_peppers(API_TOKEN_PEPPERS) 

256else: 

257 warnings.warn("API_TOKEN_PEPPERS is not defined. v2 API tokens cannot be used.") 

258 

259# Validate update repo URL and timeout 

260if RELEASE_CHECK_URL: 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true

261 try: 

262 URLValidator()(RELEASE_CHECK_URL) 

263 except ValidationError: 

264 raise ImproperlyConfigured( 

265 "RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox" 

266 ) 

267 

268# Validate configured proxy routers 

269for path in PROXY_ROUTERS: 

270 if type(path) is str: 270 ↛ 269line 270 didn't jump to line 269 because the condition on line 270 was always true

271 try: 

272 import_string(path) 

273 except ImportError: 

274 raise ImproperlyConfigured(f"Invalid path in PROXY_ROUTERS: {path}") 

275 

276# Warn on the presence of deprecated configuration parameters 

277if not LOGIN_REQUIRED: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true

278 warnings.warn( 

279 "LOGIN_REQUIRED is deprecated and will be removed in NetBox v5.0. Unauthenticated access to the application " 

280 "will no longer be supported. Please plan to require authentication for all users before upgrading.", 

281 FutureWarning, 

282 ) 

283elif hasattr(configuration, 'LOGIN_REQUIRED'): 283 ↛ 289line 283 didn't jump to line 289 because the condition on line 283 was always true

284 warnings.warn( 

285 "LOGIN_REQUIRED is deprecated and will be removed in NetBox v5.0. This parameter can be removed from your " 

286 "configuration file.", 

287 FutureWarning, 

288 ) 

289if hasattr(configuration, 'JINJA2_FILTERS'): 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true

290 warnings.warn( 

291 "JINJA2_FILTERS has been renamed to JINJA_FILTERS and the old name will be removed in NetBox v5.0. Please " 

292 "update your configuration file to use JINJA_FILTERS instead.", 

293 DeprecationWarning, 

294 ) 

295 

296 

297# 

298# Database 

299# 

300 

301# Verify that a default database has been configured 

302if 'default' not in DATABASES: 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true

303 raise ImproperlyConfigured("No default database has been configured.") 

304 

305# Set the database engine 

306if 'ENGINE' not in DATABASES['default']: 306 ↛ 316line 306 didn't jump to line 316 because the condition on line 306 was always true

307 DATABASES['default'].update({ 

308 'ENGINE': 'django_prometheus.db.backends.postgresql' if METRICS_ENABLED else 'django.db.backends.postgresql' 

309 }) 

310 

311 

312# 

313# Storage backend 

314# 

315 

316if STORAGE_BACKEND is not None: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true

317 if not STORAGES: 

318 raise ImproperlyConfigured( 

319 "STORAGE_BACKEND and STORAGES are both set, remove the deprecated STORAGE_BACKEND setting." 

320 ) 

321 else: 

322 warnings.warn( 

323 "STORAGE_BACKEND is deprecated, use the new STORAGES setting instead.", 

324 FutureWarning, 

325 ) 

326 

327if STORAGE_CONFIG is not None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 warnings.warn( 

329 "STORAGE_CONFIG is deprecated, use the new STORAGES setting instead.", 

330 FutureWarning, 

331 ) 

332 

333# Default STORAGES for Django 

334DEFAULT_STORAGES = { 

335 "default": { 

336 "BACKEND": "django.core.files.storage.FileSystemStorage", 

337 }, 

338 "staticfiles": { 

339 "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 

340 }, 

341 "scripts": { 

342 "BACKEND": "extras.storage.ScriptFileSystemStorage", 

343 "OPTIONS": { 

344 "allow_overwrite": True, 

345 }, 

346 }, 

347} 

348STORAGES = DEFAULT_STORAGES | STORAGES 

349 

350# TODO: This code is deprecated and needs to be removed in the future 

351if STORAGE_BACKEND is not None: 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true

352 STORAGES['default']['BACKEND'] = STORAGE_BACKEND 

353 

354# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG 

355if STORAGE_CONFIG is not None: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true

356 def _setting(name, default=None): 

357 if name in STORAGE_CONFIG: 

358 return STORAGE_CONFIG[name] 

359 return globals().get(name, default) 

360 storages.utils.setting = _setting 

361 

362# django-storage-swift 

363if STORAGE_BACKEND == 'swift.storage.SwiftStorage': 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true

364 try: 

365 import swift.utils # noqa: F401 

366 except ModuleNotFoundError as e: 

367 if getattr(e, 'name') == 'swift': 

368 raise ImproperlyConfigured( 

369 f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. " 

370 "It can be installed by running 'pip install django-storage-swift'." 

371 ) 

372 raise e 

373 

374 # Load all SWIFT_* settings from the user configuration 

375 for param, value in STORAGE_CONFIG.items(): 

376 if param.startswith('SWIFT_'): 

377 globals()[param] = value 

378# TODO: End of deprecated code 

379 

380# 

381# Redis 

382# 

383 

384# Background task queuing 

385if 'tasks' not in REDIS: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true

386 raise ImproperlyConfigured("REDIS section in configuration.py is missing the 'tasks' subsection.") 

387TASKS_REDIS = REDIS['tasks'] 

388TASKS_REDIS_HOST = TASKS_REDIS.get('HOST', 'localhost') 

389TASKS_REDIS_PORT = TASKS_REDIS.get('PORT', 6379) 

390TASKS_REDIS_URL = TASKS_REDIS.get('URL') 

391TASKS_REDIS_SENTINELS = TASKS_REDIS.get('SENTINELS', []) 

392TASKS_REDIS_USING_SENTINEL = all([ 

393 isinstance(TASKS_REDIS_SENTINELS, (list, tuple)), 

394 len(TASKS_REDIS_SENTINELS) > 0 

395]) 

396TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default') 

397TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10) 

398TASKS_REDIS_USERNAME = TASKS_REDIS.get('USERNAME', '') 

399TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '') 

400TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0) 

401TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False) 

402TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False) 

403TASKS_REDIS_CA_CERT_PATH = TASKS_REDIS.get('CA_CERT_PATH', False) 

404 

405# Caching 

406if 'caching' not in REDIS: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true

407 raise ImproperlyConfigured("REDIS section in configuration.py is missing caching subsection.") 

408CACHING_REDIS_HOST = REDIS['caching'].get('HOST', 'localhost') 

409CACHING_REDIS_PORT = REDIS['caching'].get('PORT', 6379) 

410CACHING_REDIS_DATABASE = REDIS['caching'].get('DATABASE', 0) 

411CACHING_REDIS_USERNAME = REDIS['caching'].get('USERNAME', '') 

412CACHING_REDIS_USERNAME_HOST = '@'.join(filter(None, [CACHING_REDIS_USERNAME, CACHING_REDIS_HOST])) 

413CACHING_REDIS_PASSWORD = REDIS['caching'].get('PASSWORD', '') 

414CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', []) 

415CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default') 

416CACHING_REDIS_PROTO = 'rediss' if REDIS['caching'].get('SSL', False) else 'redis' 

417CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY', False) 

418CACHING_REDIS_CA_CERT_PATH = REDIS['caching'].get('CA_CERT_PATH', False) 

419CACHING_REDIS_URL = REDIS['caching'].get('URL', f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}') 

420 

421# Configure Django's default cache to use Redis 

422CACHES = { 

423 'default': { 

424 'BACKEND': 'django_redis.cache.RedisCache', 

425 'LOCATION': CACHING_REDIS_URL, 

426 'OPTIONS': { 

427 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 

428 'USERNAME': CACHING_REDIS_USERNAME, 

429 'PASSWORD': CACHING_REDIS_PASSWORD, 

430 } 

431 } 

432} 

433 

434if CACHING_REDIS_SENTINELS: 434 ↛ 435line 434 didn't jump to line 435 because the condition on line 434 was never true

435 DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory' 

436 CACHES['default']['LOCATION'] = f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_SENTINEL_SERVICE}/{CACHING_REDIS_DATABASE}' 

437 CACHES['default']['OPTIONS']['CLIENT_CLASS'] = 'django_redis.client.SentinelClient' 

438 CACHES['default']['OPTIONS']['SENTINELS'] = CACHING_REDIS_SENTINELS 

439if CACHING_REDIS_SKIP_TLS_VERIFY: 439 ↛ 440line 439 didn't jump to line 440 because the condition on line 439 was never true

440 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {}) 

441 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_cert_reqs'] = False 

442if CACHING_REDIS_CA_CERT_PATH: 442 ↛ 443line 442 didn't jump to line 443 because the condition on line 442 was never true

443 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {}) 

444 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_ca_certs'] = CACHING_REDIS_CA_CERT_PATH 

445 

446# Merge in KWARGS for additional parameters 

447if caching_redis_kwargs := REDIS['caching'].get('KWARGS'): 447 ↛ 448line 447 didn't jump to line 448 because the condition on line 447 was never true

448 CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {}) 

449 CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS'].update(caching_redis_kwargs) 

450 

451 

452# 

453# Sessions 

454# 

455 

456if LOGIN_TIMEOUT is not None: 456 ↛ 459line 456 didn't jump to line 459 because the condition on line 456 was always true

457 # Django default is 1209600 seconds (14 days) 

458 SESSION_COOKIE_AGE = LOGIN_TIMEOUT 

459SESSION_SAVE_EVERY_REQUEST = bool(LOGIN_PERSISTENCE) 

460if SESSION_FILE_PATH is not None: 460 ↛ 461line 460 didn't jump to line 461 because the condition on line 460 was never true

461 SESSION_ENGINE = 'django.contrib.sessions.backends.file' 

462 

463 

464# 

465# Email 

466# 

467 

468MAILERS = { 

469 'default': { 

470 'BACKEND': 'django.core.mail.backends.smtp.EmailBackend', 

471 'OPTIONS': { 

472 'host': EMAIL.get('SERVER'), 

473 'port': EMAIL.get('PORT', 25), 

474 'username': EMAIL.get('USERNAME'), 

475 'password': EMAIL.get('PASSWORD'), 

476 'use_ssl': EMAIL.get('USE_SSL', False), 

477 'use_tls': EMAIL.get('USE_TLS', False), 

478 'ssl_certfile': EMAIL.get('SSL_CERTFILE'), 

479 'ssl_keyfile': EMAIL.get('SSL_KEYFILE'), 

480 'timeout': EMAIL.get('TIMEOUT', 10), 

481 }, 

482 }, 

483} 

484EMAIL_SUBJECT_PREFIX = '[NetBox] ' 

485SERVER_EMAIL = EMAIL.get('FROM_EMAIL') 

486 

487 

488# 

489# Django core settings 

490# 

491 

492INSTALLED_APPS = [ 

493 'django.contrib.auth', 

494 'django.contrib.contenttypes', 

495 'django.contrib.sessions', 

496 'django.contrib.messages', 

497 'django.contrib.staticfiles', 

498 'django.contrib.humanize', 

499 'django.contrib.postgres', 

500 'django.forms', 

501 'corsheaders', 

502 'debug_toolbar', 

503 'django_filters', 

504 'django_htmx', 

505 'django_tables2', 

506 'django_prometheus', 

507 'strawberry_django', 

508 'mptt', 

509 'rest_framework', 

510 'social_django', 

511 'sorl.thumbnail', 

512 'taggit', 

513 'timezone_field', 

514 'core', 

515 'account', 

516 'circuits', 

517 'dcim', 

518 'ipam', 

519 'extras', 

520 'tenancy', 

521 'users', 

522 'utilities', 

523 'virtualization', 

524 'vpn', 

525 'wireless', 

526 'django_rq', # Must come after extras to allow overriding management commands 

527 'drf_spectacular', 

528 'drf_spectacular_sidecar', 

529] 

530if not DEBUG and 'collectstatic' not in sys.argv: 530 ↛ 534line 530 didn't jump to line 534 because the condition on line 530 was always true

531 INSTALLED_APPS.remove('debug_toolbar') 

532 

533# Middleware 

534MIDDLEWARE = [ 

535 'corsheaders.middleware.CorsMiddleware', 

536 'django.contrib.sessions.middleware.SessionMiddleware', 

537 'django.middleware.locale.LocaleMiddleware', 

538 'netbox.middleware.CommonMiddleware', # Replaces django.middleware.common.CommonMiddleware 

539 'django.middleware.csrf.CsrfViewMiddleware', 

540 'django.contrib.auth.middleware.AuthenticationMiddleware', 

541 'django.contrib.messages.middleware.MessageMiddleware', 

542 'django.middleware.clickjacking.XFrameOptionsMiddleware', 

543 'django.middleware.security.SecurityMiddleware', 

544 'django_htmx.middleware.HtmxMiddleware', 

545 'netbox.middleware.RemoteUserMiddleware', 

546 'netbox.middleware.CoreMiddleware', 

547 'netbox.middleware.MaintenanceModeMiddleware', 

548 'netbox.middleware.SocialAuthExceptionMiddleware', 

549] 

550 

551if DEBUG: 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true

552 MIDDLEWARE = [ 

553 "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware", 

554 *MIDDLEWARE, 

555 ] 

556 

557if METRICS_ENABLED: 557 ↛ 559line 557 didn't jump to line 559 because the condition on line 557 was never true

558 # If metrics are enabled, add the before & after Prometheus middleware 

559 MIDDLEWARE = [ 

560 'netbox.middleware.PrometheusBeforeMiddleware', 

561 *MIDDLEWARE, 

562 'netbox.middleware.PrometheusAfterMiddleware', 

563 ] 

564 

565# URLs 

566ROOT_URLCONF = 'netbox.urls' 

567 

568# Templates 

569TEMPLATES_DIR = BASE_DIR + '/templates' 

570TEMPLATES = [ 

571 { 

572 'BACKEND': 'django.template.backends.django.DjangoTemplates', 

573 'DIRS': [TEMPLATES_DIR], 

574 'APP_DIRS': True, 

575 'OPTIONS': { 

576 'builtins': [ 

577 'utilities.templatetags.builtins.filters', 

578 'utilities.templatetags.builtins.tags', 

579 ], 

580 'context_processors': [ 

581 'django.template.context_processors.debug', 

582 'django.template.context_processors.request', 

583 'django.template.context_processors.media', 

584 'django.contrib.auth.context_processors.auth', 

585 'django.contrib.messages.context_processors.messages', 

586 'netbox.context_processors.settings', 

587 'netbox.context_processors.config', 

588 'netbox.context_processors.registry', 

589 'netbox.context_processors.preferences', 

590 ], 

591 }, 

592 }, 

593] 

594 

595# This allows us to override Django's stock form widget templates 

596FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' 

597 

598# Set up authentication backends 

599if type(REMOTE_AUTH_BACKEND) not in (list, tuple): 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true

600 REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND] 

601AUTHENTICATION_BACKENDS = [ 

602 *REMOTE_AUTH_BACKEND, 

603 'netbox.authentication.ObjectPermissionBackend', 

604] 

605 

606# Use our custom User model 

607AUTH_USER_MODEL = 'users.User' 

608 

609# Authentication URLs 

610LOGIN_URL = f'/{BASE_PATH}login/' 

611LOGIN_REDIRECT_URL = f'/{BASE_PATH}' 

612 

613# Use timezone-aware datetime objects 

614USE_TZ = True 

615 

616# Toggle language translation support 

617USE_I18N = TRANSLATION_ENABLED 

618 

619# WSGI 

620WSGI_APPLICATION = 'netbox.wsgi.application' 

621SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 

622USE_X_FORWARDED_HOST = True 

623X_FRAME_OPTIONS = 'SAMEORIGIN' 

624 

625# Static files (CSS, JavaScript, Images) 

626# STATIC_ROOT is deliberately not a configuration parameter; static files are collected to <NETBOX_ROOT>/static. 

627STATIC_ROOT = os.path.join(NETBOX_ROOT, 'static') 

628STATIC_URL = f'/{BASE_PATH}static/' 

629STATICFILES_DIRS = ( 

630 os.path.join(BASE_DIR, 'project-static', 'dist'), 

631 os.path.join(BASE_DIR, 'project-static', 'img'), 

632 os.path.join(BASE_DIR, 'project-static', 'js'), 

633 # May not exist on a checkout until `manage.py upgrade --build-docs` runs (wheels bundle 

634 # the pre-rendered site); collectstatic tolerates that. 

635 ('docs', _PATHS.static_docs_root), # Prefix with /docs 

636) 

637 

638# Media URL 

639MEDIA_URL = f'/{BASE_PATH}media/' 

640 

641# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.) 

642DATA_UPLOAD_MAX_NUMBER_FIELDS = None 

643 

644# Messages 

645MESSAGE_TAGS = { 

646 messages.ERROR: 'danger', 

647} 

648 

649DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 

650 

651SERIALIZATION_MODULES = { 

652 'json': 'utilities.serializers.json', 

653} 

654 

655DEBUG_TOOLBAR_CONFIG = { 

656 'SHOW_TOOLBAR_CALLBACK': 'utilities.debug.show_toolbar', 

657} 

658 

659 

660# 

661# Permissions & authentication 

662# 

663 

664# Exclude potentially sensitive models from wildcard view exemption. These may still be exempted 

665# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter. 

666EXEMPT_EXCLUDE_MODELS = ( 

667 ('extras', 'configrevision'), 

668 ('users', 'group'), 

669 ('users', 'objectpermission'), 

670 ('users', 'token'), 

671 ('users', 'user'), 

672) 

673 

674# All URLs starting with a string listed here are exempt from maintenance mode enforcement 

675MAINTENANCE_EXEMPT_PATHS = ( 

676 f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration 

677 LOGIN_URL, 

678 LOGIN_REDIRECT_URL, 

679 LOGOUT_REDIRECT_URL 

680) 

681 

682 

683# 

684# Sentry 

685# 

686 

687if SENTRY_ENABLED: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true

688 try: 

689 import sentry_sdk 

690 except ModuleNotFoundError: 

691 raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.") 

692 

693 # Build the Sentry initialization parameters 

694 sentry_config = { 

695 'sample_rate': 1.0, 

696 'send_default_pii': False, 

697 'traces_sample_rate': 0, 

698 # TODO: Support proxy routing 

699 'http_proxy': HTTP_PROXIES.get('http') if HTTP_PROXIES else None, 

700 'https_proxy': HTTP_PROXIES.get('https') if HTTP_PROXIES else None, 

701 } 

702 sentry_config.update(SENTRY_CONFIG) 

703 # Check for a DSN 

704 if not sentry_config.get('dsn'): 

705 raise ImproperlyConfigured( 

706 "Sentry is enabled but a DSN has not been specified. Set one under the SENTRY_CONFIG parameter." 

707 ) 

708 

709 # Initialize the SDK 

710 sentry_sdk.init( 

711 release=RELEASE.full_version, 

712 **sentry_config 

713 ) 

714 # Assign any configured tags 

715 for k, v in SENTRY_TAGS.items(): 

716 sentry_sdk.set_tag(k, v) 

717 

718 

719# 

720# Census collection 

721# 

722 

723# Calculate a unique deployment ID from the secret key 

724DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16] 

725CENSUS_URL = 'https://census.netbox.oss.netboxlabs.com/api/v1/' 

726 

727 

728# 

729# NetBox Copilot 

730# 

731 

732NETBOX_COPILOT_URL = 'https://static.copilot.netboxlabs.ai/load.js' 

733 

734 

735# 

736# Django social auth 

737# 

738 

739SOCIAL_AUTH_PIPELINE = ( 

740 'social_core.pipeline.social_auth.social_details', 

741 'social_core.pipeline.social_auth.social_uid', 

742 'social_core.pipeline.social_auth.social_user', 

743 'social_core.pipeline.user.get_username', 

744 'social_core.pipeline.user.create_user', 

745 'social_core.pipeline.social_auth.associate_user', 

746 'netbox.authentication.user_default_groups_handler', 

747 'social_core.pipeline.social_auth.load_extra_data', 

748 'social_core.pipeline.user.user_details', 

749) 

750 

751# Redirect users back to the login page (surfacing the error via the messages framework) when an 

752# SSO/SAML authentication failure occurs, rather than raising an HTTP 500. Full exceptions are still 

753# raised when DEBUG is enabled. LOGIN_URL is an absolute path which respects BASE_PATH; the social 

754# auth middleware passes this value directly to an HttpResponseRedirect without reversing it. 

755SOCIAL_AUTH_LOGIN_ERROR_URL = LOGIN_URL 

756SOCIAL_AUTH_RAISE_EXCEPTIONS = DEBUG 

757 

758# Load all SOCIAL_AUTH_* settings from the user configuration 

759for param in dir(configuration): 

760 if param.startswith('SOCIAL_AUTH_'): 

761 globals()[param] = getattr(configuration, param) 

762 

763# Force usage of PostgreSQL's JSONB field for extra data 

764SOCIAL_AUTH_JSONFIELD_ENABLED = True 

765SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username' 

766 

767SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL 

768 

769# 

770# Django Prometheus 

771# 

772 

773PROMETHEUS_EXPORT_MIGRATIONS = False 

774 

775 

776# 

777# Django filters 

778# 

779 

780FILTERS_NULL_CHOICE_LABEL = 'None' 

781FILTERS_NULL_CHOICE_VALUE = 'null' 

782 

783 

784# 

785# Django REST framework (API) 

786# 

787 

788REST_FRAMEWORK_VERSION = '.'.join(RELEASE.version.split('-')[0].split('.')[:2]) # Use major.minor as API version 

789REST_FRAMEWORK = { 

790 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], 

791 'COERCE_DECIMAL_TO_STRING': False, 

792 'DEFAULT_AUTHENTICATION_CLASSES': ( 

793 'rest_framework.authentication.SessionAuthentication', 

794 'netbox.api.authentication.TokenAuthentication', 

795 ), 

796 'DEFAULT_FILTER_BACKENDS': ( 

797 'django_filters.rest_framework.DjangoFilterBackend', 

798 'rest_framework.filters.OrderingFilter', 

799 ), 

800 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata', 

801 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.NetBoxPagination', 

802 'DEFAULT_PARSER_CLASSES': ( 

803 'rest_framework.parsers.JSONParser', 

804 'rest_framework.parsers.MultiPartParser', 

805 ), 

806 'DEFAULT_PERMISSION_CLASSES': ( 

807 'netbox.api.authentication.TokenPermissions', 

808 ), 

809 'DEFAULT_RENDERER_CLASSES': ( 

810 'rest_framework.renderers.JSONRenderer', 

811 'netbox.api.renderers.FormlessBrowsableAPIRenderer', 

812 ), 

813 'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema', 

814 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 

815 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 

816 # Align REST framework's key for errors which pertain to no particular field with Django's 

817 # (django.core.exceptions.NON_FIELD_ERRORS), so that the API reports such an error under one key 

818 # rather than two. Model validation errors reach a response by way of full_clean(), and so are 

819 # keyed by Django; errors raised by a serializer or field are keyed by REST framework. Without 

820 # this, which of the two a client must read depends on the layer which rejected the request. 

821 'NON_FIELD_ERRORS_KEY': '__all__', 

822 'SCHEMA_COERCE_METHOD_NAMES': { 

823 # Default mappings 

824 'retrieve': 'read', 

825 'destroy': 'delete', 

826 # Custom operations 

827 'bulk_destroy': 'bulk_delete', 

828 }, 

829 'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name', 

830} 

831 

832# 

833# DRF Spectacular 

834# 

835 

836SPECTACULAR_SETTINGS = { 

837 'TITLE': 'NetBox REST API', 

838 'LICENSE': {'name': 'Apache v2 License'}, 

839 'VERSION': RELEASE.full_version, 

840 'COMPONENT_SPLIT_REQUEST': True, 

841 'REDOC_DIST': 'SIDECAR', 

842 'SERVERS': [{ 

843 'url': '', 

844 'description': 'NetBox', 

845 }], 

846 'SWAGGER_UI_DIST': 'SIDECAR', 

847 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', 

848 'POSTPROCESSING_HOOKS': [], 

849} 

850 

851# 

852# Django RQ (events backend) 

853# 

854 

855if TASKS_REDIS_USING_SENTINEL: 855 ↛ 856line 855 didn't jump to line 856 because the condition on line 855 was never true

856 RQ_PARAMS = { 

857 'SENTINELS': TASKS_REDIS_SENTINELS, 

858 'MASTER_NAME': TASKS_REDIS_SENTINEL_SERVICE, 

859 'SOCKET_TIMEOUT': None, 

860 'CONNECTION_KWARGS': { 

861 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT 

862 }, 

863 } 

864elif TASKS_REDIS_URL: 864 ↛ 865line 864 didn't jump to line 865 because the condition on line 864 was never true

865 RQ_PARAMS = { 

866 'URL': TASKS_REDIS_URL, 

867 'SSL': TASKS_REDIS_SSL, 

868 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required', 

869 } 

870else: 

871 RQ_PARAMS = { 

872 'HOST': TASKS_REDIS_HOST, 

873 'PORT': TASKS_REDIS_PORT, 

874 'SSL': TASKS_REDIS_SSL, 

875 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required', 

876 } 

877RQ_PARAMS.update({ 

878 'DB': TASKS_REDIS_DATABASE, 

879 'USERNAME': TASKS_REDIS_USERNAME, 

880 'PASSWORD': TASKS_REDIS_PASSWORD, 

881 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT, 

882}) 

883if TASKS_REDIS_CA_CERT_PATH: 883 ↛ 884line 883 didn't jump to line 884 because the condition on line 883 was never true

884 RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {}) 

885 RQ_PARAMS['REDIS_CLIENT_KWARGS']['ssl_ca_certs'] = TASKS_REDIS_CA_CERT_PATH 

886 

887# Merge in KWARGS for additional parameters 

888if tasks_redis_kwargs := TASKS_REDIS.get('KWARGS'): 888 ↛ 889line 888 didn't jump to line 889 because the condition on line 888 was never true

889 RQ_PARAMS.setdefault('REDIS_CLIENT_KWARGS', {}) 

890 RQ_PARAMS['REDIS_CLIENT_KWARGS'].update(tasks_redis_kwargs) 

891 

892# Define named RQ queues 

893RQ_QUEUES = { 

894 RQ_QUEUE_HIGH: RQ_PARAMS, 

895 RQ_QUEUE_DEFAULT: RQ_PARAMS, 

896 RQ_QUEUE_LOW: RQ_PARAMS, 

897} 

898# Add any queues defined in QUEUE_MAPPINGS 

899RQ_QUEUES.update({ 

900 queue: RQ_PARAMS for queue in set(QUEUE_MAPPINGS.values()) if queue not in RQ_QUEUES 

901}) 

902 

903# 

904# Localization 

905# 

906 

907# Supported translation languages 

908LANGUAGES = ( 

909 ('cs', _('Czech')), 

910 ('da', _('Danish')), 

911 ('de', _('German')), 

912 ('en', _('English')), 

913 ('es', _('Spanish')), 

914 ('fr', _('French')), 

915 ('it', _('Italian')), 

916 ('ja', _('Japanese')), 

917 ('ko', _('Korean')), 

918 ('lv', _('Latvian')), 

919 ('nl', _('Dutch')), 

920 ('pl', _('Polish')), 

921 ('pt', _('Portuguese')), 

922 ('ru', _('Russian')), 

923 ('tr', _('Turkish')), 

924 ('uk', _('Ukrainian')), 

925 ('zh', _('Chinese')), 

926) 

927LOCALE_PATHS = ( 

928 BASE_DIR + '/translations', 

929) 

930 

931# 

932# Strawberry (GraphQL) 

933# 

934STRAWBERRY_DJANGO = { 

935 "DEFAULT_PK_FIELD_NAME": "id", 

936 "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, 

937 "PAGINATION_DEFAULT_LIMIT": 100, 

938 # Disable the library's max-limit cap (introduced in strawberry-graphql-django 0.85); NetBox enforces its own 

939 # page-size ceiling via MAX_PAGE_SIZE in netbox.graphql.pagination.apply_pagination(). 

940 "PAGINATION_MAX_LIMIT": None, 

941} 

942 

943# 

944# Plugins 

945# 

946 

947PLUGIN_CATALOG_URL = 'https://api.netbox.oss.netboxlabs.com/v1/plugins' 

948 

949EVENTS_PIPELINE = list(EVENTS_PIPELINE) 

950if 'extras.events.process_event_queue' not in EVENTS_PIPELINE: 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true

951 EVENTS_PIPELINE.insert(0, 'extras.events.process_event_queue') 

952 

953# Register any configured plugins 

954for plugin_name in PLUGINS: 954 ↛ 955line 954 didn't jump to line 955 because the loop on line 954 never started

955 try: 

956 # Import the plugin module 

957 plugin = importlib.import_module(plugin_name) 

958 except ModuleNotFoundError as e: 

959 if getattr(e, 'name') == plugin_name: 

960 raise ImproperlyConfigured( 

961 f"Unable to import plugin {plugin_name}: Module not found. Check that the plugin module has been " 

962 f"installed within the correct Python environment." 

963 ) 

964 raise e 

965 

966 try: 

967 # Load the PluginConfig 

968 plugin_config: PluginConfig = plugin.config 

969 except AttributeError: 

970 raise ImproperlyConfigured( 

971 f"Plugin {plugin_name} does not provide a 'config' variable. This should be defined in the plugin's " 

972 f"__init__.py file and point to the PluginConfig subclass." 

973 ) 

974 

975 # Validate version compatibility and user-provided configuration settings and assign defaults 

976 if plugin_name not in PLUGINS_CONFIG: 

977 PLUGINS_CONFIG[plugin_name] = {} 

978 try: 

979 plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version) 

980 except IncompatiblePluginError as e: 

981 warnings.warn(f'Unable to load plugin {plugin_name}: {e}') 

982 continue 

983 

984 # Register the plugin as installed successfully 

985 registry['plugins']['installed'].append(plugin_name) 

986 

987 plugin_module = "{}.{}".format(plugin_config.__module__, plugin_config.__name__) # type: ignore 

988 

989 # Gather additional apps to load alongside this plugin 

990 django_apps = plugin_config.django_apps 

991 if plugin_name in django_apps: 

992 django_apps.pop(plugin_name) 

993 if plugin_module not in django_apps: 

994 django_apps.append(plugin_module) 

995 

996 # Test if we can import all modules (or its parent, for PluginConfigs and AppConfigs) 

997 for app in django_apps: 

998 if "." in app: 

999 parts = app.split(".") 

1000 spec = importlib.util.find_spec(".".join(parts[:-1])) 

1001 else: 

1002 spec = importlib.util.find_spec(app) 

1003 if spec is None: 

1004 raise ImproperlyConfigured( 

1005 f"Failed to load django_apps specified by plugin {plugin_name}: {django_apps} " 

1006 f"The module {app} cannot be imported. Check that the necessary package has been " 

1007 f"installed within the correct Python environment." 

1008 ) 

1009 

1010 INSTALLED_APPS.extend(django_apps) 

1011 

1012 # Preserve uniqueness of the INSTALLED_APPS list, we keep the last occurrence 

1013 sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS)))) 

1014 INSTALLED_APPS = list(sorted_apps) 

1015 

1016 # Add middleware 

1017 plugin_middleware = plugin_config.middleware 

1018 if plugin_middleware and type(plugin_middleware) in (list, tuple): 

1019 MIDDLEWARE.extend(plugin_middleware) 

1020 

1021 # Create RQ queues dedicated to the plugin 

1022 # we use the plugin name as a prefix for queue name's defined in the plugin config 

1023 # ex: mysuperplugin.mysuperqueue1 

1024 if type(plugin_config.queues) is not list: 

1025 raise ImproperlyConfigured(f"Plugin {plugin_name} queues must be a list.") 

1026 RQ_QUEUES.update({ 

1027 f"{plugin_name}.{queue}": RQ_PARAMS for queue in plugin_config.queues 

1028 }) 

1029 

1030 events_pipeline = plugin_config.events_pipeline 

1031 if events_pipeline: 

1032 if type(events_pipeline) in (list, tuple): 

1033 EVENTS_PIPELINE.extend(events_pipeline) 

1034 else: 

1035 raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple") 

1036 

1037# GraphQL assembly must run after every plugin has initialized. 

1038INSTALLED_APPS.append('netbox.graphql.apps.GraphQLConfig') 

1039 

1040 

1041# 

1042# Monkey-patching 

1043# 

1044 

1045from rest_framework.utils import field_mapping # noqa: E402 

1046from strawberry_django import pagination # noqa: E402 

1047from strawberry_django.fields.field import StrawberryDjangoField # noqa: E402 

1048 

1049from netbox.graphql.pagination import OffsetPaginationInput, apply_pagination # noqa: E402 

1050 

1051# TODO: Remove this once #20547 has been implemented 

1052# Override DRF's get_unique_validators() function with our own (see bug #19302) 

1053field_mapping.get_unique_validators = get_unique_validators 

1054 

1055# Override strawberry-django's OffsetPaginationInput class to add the `start` parameter 

1056pagination.OffsetPaginationInput = OffsetPaginationInput 

1057 

1058# Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination 

1059StrawberryDjangoField.apply_pagination = apply_pagination 

1060 

1061 

1062# UNSUPPORTED FUNCTIONALITY: Import any local overrides. 

1063try: 

1064 from .local_settings import * 

1065 _UNSUPPORTED_SETTINGS = True 

1066except ImportError: 

1067 pass