diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 2b3b80d24..d6b23fed4 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -15,6 +15,7 @@ from dcim.constants import *
from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet
from extras.utils import extras_features
+from netbox.config import ConfigResolver
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
@@ -815,7 +816,8 @@ class Device(PrimaryModel, ConfigContextModel):
@property
def primary_ip(self):
- if settings.PREFER_IPV4 and self.primary_ip4:
+ config = ConfigResolver()
+ if config.PREFER_IPV4 and self.primary_ip4:
return self.primary_ip4
elif self.primary_ip6:
return self.primary_ip6
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 8ea27b8a6..167dba95d 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -160,18 +160,11 @@ class DeviceTable(BaseTable):
linkify=True,
verbose_name='Type'
)
- if settings.PREFER_IPV4:
- primary_ip = tables.Column(
- linkify=True,
- order_by=('primary_ip4', 'primary_ip6'),
- verbose_name='IP Address'
- )
- else:
- primary_ip = tables.Column(
- linkify=True,
- order_by=('primary_ip6', 'primary_ip4'),
- verbose_name='IP Address'
- )
+ primary_ip = tables.Column(
+ linkify=True,
+ order_by=('primary_ip4', 'primary_ip6'),
+ verbose_name='IP Address'
+ )
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'
diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py
index dae21c2c9..e99406e49 100644
--- a/netbox/extras/admin.py
+++ b/netbox/extras/admin.py
@@ -1,10 +1,74 @@
from django.contrib import admin
-from .models import JobResult
+from .forms import ConfigRevisionForm
+from .models import ConfigRevision, JobResult
+
+
+@admin.register(ConfigRevision)
+class ConfigRevisionAdmin(admin.ModelAdmin):
+ fieldsets = [
+ # ('Authentication', {
+ # 'fields': ('LOGIN_REQUIRED', 'LOGIN_PERSISTENCE', 'LOGIN_TIMEOUT'),
+ # }),
+ # ('Rack Elevations', {
+ # 'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
+ # }),
+ ('IPAM', {
+ 'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
+ }),
+ # ('Security', {
+ # 'fields': (
+ # 'ALLOWED_URL_SCHEMES', 'EXEMPT_VIEW_PERMISSIONS',
+ # ),
+ # }),
+ ('Banners', {
+ 'fields': ('BANNER_LOGIN', 'BANNER_TOP', 'BANNER_BOTTOM'),
+ }),
+ # ('Logging', {
+ # 'fields': ('CHANGELOG_RETENTION',),
+ # }),
+ # ('Pagination', {
+ # 'fields': ('MAX_PAGE_SIZE', 'PAGINATE_COUNT'),
+ # }),
+ # ('Miscellaneous', {
+ # 'fields': ('GRAPHQL_ENABLED', 'METRICS_ENABLED', 'MAINTENANCE_MODE', 'MAPS_URL'),
+ # }),
+ ('Config Revision', {
+ 'fields': ('comment',),
+ })
+ ]
+ form = ConfigRevisionForm
+ list_display = ('id', 'is_active', 'created', 'comment')
+ ordering = ('-id',)
+ readonly_fields = ('data',)
+
+ def get_changeform_initial_data(self, request):
+ """
+ Populate initial form data from the most recent ConfigRevision.
+ """
+ latest_revision = ConfigRevision.objects.last()
+ initial = latest_revision.data if latest_revision else {}
+ initial.update(super().get_changeform_initial_data(request))
+
+ return initial
+
+ def has_add_permission(self, request):
+ # Only superusers may modify the configuration.
+ return request.user.is_superuser
+
+ def has_change_permission(self, request, obj=None):
+ # ConfigRevisions cannot be modified once created.
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ # Only inactive ConfigRevisions may be deleted (must be superuser).
+ return request.user.is_superuser and (
+ obj is None or not obj.is_active()
+ )
#
-# Reports
+# Reports & scripts
#
@admin.register(JobResult)
diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py
index 1584e2f51..b470650da 100644
--- a/netbox/extras/forms/__init__.py
+++ b/netbox/extras/forms/__init__.py
@@ -3,4 +3,5 @@ from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .customfields import *
+from .config import *
from .scripts import *
diff --git a/netbox/extras/forms/config.py b/netbox/extras/forms/config.py
new file mode 100644
index 000000000..001252f0c
--- /dev/null
+++ b/netbox/extras/forms/config.py
@@ -0,0 +1,67 @@
+from django import forms
+
+from netbox.config.parameters import PARAMS
+
+__all__ = (
+ 'ConfigRevisionForm',
+)
+
+
+EMPTY_VALUES = ('', None, [], ())
+
+
+class FormMetaclass(forms.models.ModelFormMetaclass):
+
+ def __new__(mcs, name, bases, attrs):
+
+ # Emulate a declared field for each supported configuration parameter
+ param_fields = {}
+ for param in PARAMS:
+ help_text = f'{param.description}
' if param.description else ''
+ # help_text += f'Current value: {getattr(settings, param.name)}'
+ param_fields[param.name] = param.field(
+ required=False,
+ label=param.label,
+ help_text=help_text,
+ **param.field_kwargs
+ )
+ attrs.update(param_fields)
+
+ return super().__new__(mcs, name, bases, attrs)
+
+
+class ConfigRevisionForm(forms.BaseModelForm, metaclass=FormMetaclass):
+ """
+ Form for creating a new ConfigRevision.
+ """
+ class Meta:
+ widgets = {
+ 'comment': forms.Textarea(),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Bugfix for django-timezone-field: Add empty choice to default options
+ # self.fields['TIME_ZONE'].choices = [('', ''), *self.fields['TIME_ZONE'].choices]
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+
+ # Populate JSON data on the instance
+ instance.data = self.render_json()
+
+ if commit:
+ instance.save()
+
+ return instance
+
+ def render_json(self):
+ json = {}
+
+ # Iterate through each field and populate non-empty values
+ for field_name in self.declared_fields:
+ if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
+ json[field_name] = self.cleaned_data[field_name]
+
+ return json
diff --git a/netbox/extras/migrations/0064_configrevision.py b/netbox/extras/migrations/0064_configrevision.py
new file mode 100644
index 000000000..c3fce8abe
--- /dev/null
+++ b/netbox/extras/migrations/0064_configrevision.py
@@ -0,0 +1,20 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0063_webhook_conditions'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ConfigRevision',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('comment', models.CharField(blank=True, max_length=200)),
+ ('data', models.JSONField(blank=True, null=True)),
+ ],
+ ),
+ ]
diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py
index 84676453f..3cb6372be 100644
--- a/netbox/extras/models/__init__.py
+++ b/netbox/extras/models/__init__.py
@@ -1,12 +1,13 @@
from .change_logging import ObjectChange
from .configcontexts import ConfigContext, ConfigContextModel
from .customfields import CustomField
-from .models import CustomLink, ExportTemplate, ImageAttachment, JobResult, JournalEntry, Report, Script, Webhook
+from .models import *
from .tags import Tag, TaggedItem
__all__ = (
'ConfigContext',
'ConfigContextModel',
+ 'ConfigRevision',
'CustomField',
'CustomLink',
'ExportTemplate',
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 43af19f82..4f93b19ce 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -1,9 +1,11 @@
import json
import uuid
+from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
+from django.core.cache import cache
from django.core.validators import ValidationError
from django.db import models
from django.http import HttpResponse
@@ -20,8 +22,8 @@ from netbox.models import BigIDModel, ChangeLoggedModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import render_jinja2
-
__all__ = (
+ 'ConfigRevision',
'CustomLink',
'ExportTemplate',
'ImageAttachment',
@@ -33,10 +35,6 @@ __all__ = (
)
-#
-# Webhooks
-#
-
@extras_features('webhooks')
class Webhook(ChangeLoggedModel):
"""
@@ -181,10 +179,6 @@ class Webhook(ChangeLoggedModel):
return json.dumps(context, cls=JSONEncoder)
-#
-# Custom links
-#
-
@extras_features('webhooks')
class CustomLink(ChangeLoggedModel):
"""
@@ -240,10 +234,6 @@ class CustomLink(ChangeLoggedModel):
return reverse('extras:customlink', args=[self.pk])
-#
-# Export templates
-#
-
@extras_features('webhooks')
class ExportTemplate(ChangeLoggedModel):
content_type = models.ForeignKey(
@@ -333,10 +323,6 @@ class ExportTemplate(ChangeLoggedModel):
return response
-#
-# Image attachments
-#
-
class ImageAttachment(BigIDModel):
"""
An uploaded image which is associated with an object.
@@ -409,11 +395,6 @@ class ImageAttachment(BigIDModel):
return None
-#
-# Journal entries
-#
-
-
@extras_features('webhooks')
class JournalEntry(ChangeLoggedModel):
"""
@@ -463,36 +444,6 @@ class JournalEntry(ChangeLoggedModel):
return JournalEntryKindChoices.CSS_CLASSES.get(self.kind)
-#
-# Custom scripts
-#
-
-@extras_features('job_results')
-class Script(models.Model):
- """
- Dummy model used to generate permissions for custom scripts. Does not exist in the database.
- """
- class Meta:
- managed = False
-
-
-#
-# Reports
-#
-
-@extras_features('job_results')
-class Report(models.Model):
- """
- Dummy model used to generate permissions for reports. Does not exist in the database.
- """
- class Meta:
- managed = False
-
-
-#
-# Job results
-#
-
class JobResult(BigIDModel):
"""
This model stores the results from running a user-defined report.
@@ -582,3 +533,59 @@ class JobResult(BigIDModel):
func.delay(*args, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
return job_result
+
+
+class ConfigRevision(models.Model):
+ """
+ An atomic revision of NetBox's configuration.
+ """
+ created = models.DateTimeField(
+ auto_now_add=True
+ )
+ comment = models.CharField(
+ max_length=200,
+ blank=True
+ )
+ data = models.JSONField(
+ blank=True,
+ null=True,
+ verbose_name='Configuration data'
+ )
+
+ def __str__(self):
+ return f'Config revision #{self.pk} ({self.created})'
+
+ def __getattr__(self, item):
+ if item in self.data:
+ return self.data[item]
+ return super().__getattribute__(item)
+
+ @admin.display(boolean=True)
+ def is_active(self):
+ return cache.get('config_version') == self.pk
+
+
+#
+# Custom scripts & reports
+#
+
+@extras_features('job_results')
+class Script(models.Model):
+ """
+ Dummy model used to generate permissions for custom scripts. Does not exist in the database.
+ """
+ class Meta:
+ managed = False
+
+
+#
+# Reports
+#
+
+@extras_features('job_results')
+class Report(models.Model):
+ """
+ Dummy model used to generate permissions for reports. Does not exist in the database.
+ """
+ class Meta:
+ managed = False
diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py
index 4f09706be..01fd30f15 100644
--- a/netbox/extras/signals.py
+++ b/netbox/extras/signals.py
@@ -2,13 +2,14 @@ import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
+from django.core.cache import cache
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal
from django_prometheus.models import model_deletes, model_inserts, model_updates
from netbox.signals import post_clean
from .choices import ObjectChangeActionChoices
-from .models import CustomField, ObjectChange
+from .models import ConfigRevision, CustomField, ObjectChange
from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
@@ -161,3 +162,16 @@ def run_custom_validators(sender, instance, **kwargs):
validators = settings.CUSTOM_VALIDATORS.get(model_name, [])
for validator in validators:
validator(instance)
+
+
+#
+# Dynamic configuration
+#
+
+@receiver(post_save, sender=ConfigRevision)
+def update_config(sender, instance, **kwargs):
+ """
+ Update the cached NetBox configuration when a new ConfigRevision is created.
+ """
+ cache.set('config', instance.data, None)
+ cache.set('config_version', instance.pk, None)
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index 514e87a62..d655dcb21 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -17,6 +17,7 @@ from ipam.fields import IPNetworkField, IPAddressField
from ipam.managers import IPAddressManager
from ipam.querysets import PrefixQuerySet
from ipam.validators import DNSValidator
+from netbox.config import ConfigResolver
from utilities.querysets import RestrictedQuerySet
from virtualization.models import VirtualMachine
@@ -316,7 +317,8 @@ class Prefix(PrimaryModel):
})
# Enforce unique IP space (if applicable)
- if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
+ config = ConfigResolver()
+ if (self.vrf is None and config.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_prefixes = self.get_duplicates()
if duplicate_prefixes:
raise ValidationError({
@@ -811,7 +813,8 @@ class IPAddress(PrimaryModel):
})
# Enforce unique IP space (if applicable)
- if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
+ config = ConfigResolver()
+ if (self.vrf is None and config.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates()
if duplicate_ips and (
self.role not in IPADDRESS_ROLES_NONUNIQUE or
diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py
new file mode 100644
index 000000000..34ee127fc
--- /dev/null
+++ b/netbox/netbox/config/__init__.py
@@ -0,0 +1,35 @@
+from django.conf import settings
+from django.core.cache import cache
+
+from .parameters import PARAMS
+
+__all__ = (
+ 'ConfigResolver',
+ 'PARAMS',
+)
+
+
+class ConfigResolver:
+ """
+ Active NetBox configuration.
+ """
+ def __init__(self):
+ self.config = cache.get('config')
+ self.version = self.config.get('config_version')
+ self.defaults = {param.name: param.default for param in PARAMS}
+
+ def __getattr__(self, item):
+
+ # Check for hard-coded configuration in settings.py
+ if hasattr(settings, item):
+ return getattr(settings, item)
+
+ # Return config value from cache
+ if item in self.config:
+ return self.config[item]
+
+ # Fall back to the parameter's default value
+ if item in self.defaults:
+ return self.defaults[item]
+
+ raise AttributeError(f"Invalid configuration parameter: {item}")
diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py
new file mode 100644
index 000000000..604131ec1
--- /dev/null
+++ b/netbox/netbox/config/parameters.py
@@ -0,0 +1,55 @@
+from django import forms
+
+
+class OptionalBooleanSelect(forms.Select):
+ """
+ An optional boolean field (yes/no/default).
+ """
+ def __init__(self, attrs=None):
+ choices = (
+ ('', 'Default'),
+ (True, 'Yes'),
+ (False, 'No'),
+ )
+ super().__init__(attrs, choices)
+
+
+class OptionalBooleanField(forms.NullBooleanField):
+ widget = OptionalBooleanSelect
+
+
+class ConfigParam:
+
+ def __init__(self, name, label, default, description=None, field=None, field_kwargs=None):
+ self.name = name
+ self.label = label
+ self.default = default
+ self.field = field or forms.CharField
+ self.description = description
+ self.field_kwargs = field_kwargs or {}
+
+
+PARAMS = (
+
+ # Banners
+ ConfigParam('BANNER_LOGIN', 'Login banner', ''),
+ ConfigParam('BANNER_TOP', 'Top banner', ''),
+ ConfigParam('BANNER_BOTTOM', 'Bottom banner', ''),
+
+ # IPAM
+ ConfigParam(
+ name='ENFORCE_GLOBAL_UNIQUE',
+ label='Globally unique IP space',
+ default=False,
+ description="Enforce unique IP addressing within the global table",
+ field=OptionalBooleanField
+ ),
+ ConfigParam(
+ name='PREFER_IPV4',
+ label='Prefer IPv4',
+ default=False,
+ description="Prefer IPv4 addresses over IPv6",
+ field=OptionalBooleanField
+ ),
+
+)
diff --git a/netbox/netbox/context_processors.py b/netbox/netbox/context_processors.py
index d6dd67d99..fee32a063 100644
--- a/netbox/netbox/context_processors.py
+++ b/netbox/netbox/context_processors.py
@@ -1,6 +1,7 @@
from django.conf import settings as django_settings
from extras.registry import registry
+from netbox.config import ConfigResolver
def settings_and_registry(request):
@@ -9,6 +10,7 @@ def settings_and_registry(request):
"""
return {
'settings': django_settings,
+ 'config': ConfigResolver(),
'registry': registry,
'preferences': request.user.config if request.user.is_authenticated else {},
}
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 279b8c453..0eb164523 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -11,6 +11,8 @@ from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import URLValidator
+from netbox.config import PARAMS
+
#
# Environment setup
@@ -68,18 +70,11 @@ DATABASE = getattr(configuration, 'DATABASE')
REDIS = getattr(configuration, 'REDIS')
SECRET_KEY = getattr(configuration, 'SECRET_KEY')
-# Set optional parameters
+# Set static config parameters
ADMINS = getattr(configuration, 'ADMINS', [])
-ALLOWED_URL_SCHEMES = getattr(configuration, 'ALLOWED_URL_SCHEMES', (
- 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
-))
-BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
-BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
-BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
-CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
@@ -90,30 +85,12 @@ DEBUG = getattr(configuration, 'DEBUG', False)
DEVELOPER = getattr(configuration, 'DEVELOPER', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {})
-ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
-EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
-GRAPHQL_ENABLED = getattr(configuration, 'GRAPHQL_ENABLED', True)
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
LOGGING = getattr(configuration, 'LOGGING', {})
-LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
-LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
-MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
-MAPS_URL = getattr(configuration, 'MAPS_URL', 'https://maps.google.com/?q=')
-MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
-METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
-NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
-NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
-NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
-NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
-PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
-LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
PLUGINS = getattr(configuration, 'PLUGINS', [])
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
-PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
-RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22)
-RACK_ELEVATION_DEFAULT_UNIT_WIDTH = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220)
REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
@@ -127,7 +104,6 @@ REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', [])
REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', [])
REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
-RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
@@ -141,6 +117,33 @@ STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
+# Check for hard-coded dynamic config parameters
+for param in PARAMS:
+ if hasattr(configuration, param.name):
+ globals()[param.name] = getattr(configuration, param.name)
+
+ALLOWED_URL_SCHEMES = getattr(configuration, 'ALLOWED_URL_SCHEMES', (
+ 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
+))
+CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
+EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
+GRAPHQL_ENABLED = getattr(configuration, 'GRAPHQL_ENABLED', True)
+LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
+LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
+LOGIN_TIMEOUT = getattr(configuration, 'LOGIN_TIMEOUT', None)
+MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
+MAPS_URL = getattr(configuration, 'MAPS_URL', 'https://maps.google.com/?q=')
+MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
+METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
+NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
+NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
+NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
+NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
+PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
+RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22)
+RACK_ELEVATION_DEFAULT_UNIT_WIDTH = getattr(configuration, 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220)
+RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
+
# Validate update repo URL and timeout
if RELEASE_CHECK_URL:
validator = URLValidator(
diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html
index 9575d4dcb..a4c8c77b6 100644
--- a/netbox/templates/base/layout.html
+++ b/netbox/templates/base/layout.html
@@ -58,9 +58,9 @@
- {% if settings.BANNER_TOP %}
+ {% if config.BANNER_TOP %}