From 82243732a19f83cb55c60fe206470cbe04df638b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 25 Oct 2021 14:42:20 -0400 Subject: [PATCH 01/19] Initial work on #5883 --- netbox/dcim/models/devices.py | 4 +- netbox/dcim/tables/devices.py | 17 +-- netbox/extras/admin.py | 68 ++++++++++- netbox/extras/forms/__init__.py | 1 + netbox/extras/forms/config.py | 67 +++++++++++ .../extras/migrations/0064_configrevision.py | 20 ++++ netbox/extras/models/__init__.py | 3 +- netbox/extras/models/models.py | 111 ++++++++++-------- netbox/extras/signals.py | 16 ++- netbox/ipam/models/ip.py | 7 +- netbox/netbox/config/__init__.py | 35 ++++++ netbox/netbox/config/parameters.py | 55 +++++++++ netbox/netbox/context_processors.py | 2 + netbox/netbox/settings.py | 57 ++++----- netbox/templates/base/layout.html | 8 +- netbox/templates/login.html | 4 +- netbox/virtualization/models.py | 5 +- netbox/virtualization/tables.py | 4 +- 18 files changed, 375 insertions(+), 109 deletions(-) create mode 100644 netbox/extras/forms/config.py create mode 100644 netbox/extras/migrations/0064_configrevision.py create mode 100644 netbox/netbox/config/__init__.py create mode 100644 netbox/netbox/config/parameters.py 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 %} {% endif %} @@ -98,9 +98,9 @@ {% endblock %} - {% if settings.BANNER_BOTTOM %} + {% if config.BANNER_BOTTOM %} {% endif %} diff --git a/netbox/templates/login.html b/netbox/templates/login.html index 37cdd8e53..a01d75422 100644 --- a/netbox/templates/login.html +++ b/netbox/templates/login.html @@ -7,9 +7,9 @@
{# Login banner #} - {% if settings.BANNER_LOGIN %} + {% if config.BANNER_LOGIN %} {% endif %} diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index c614618c0..51d255fc7 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator @@ -9,6 +8,7 @@ from dcim.models import BaseInterface, Device 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.fields import NaturalOrderingField from utilities.ordering import naturalize_interface @@ -340,7 +340,8 @@ class VirtualMachine(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/virtualization/tables.py b/netbox/virtualization/tables.py index 56ad88f1f..0a605267d 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -17,8 +17,6 @@ __all__ = ( 'VMInterfaceTable', ) -PRIMARY_IP_ORDERING = ('primary_ip4', 'primary_ip6') if settings.PREFER_IPV4 else ('primary_ip6', 'primary_ip4') - VMINTERFACE_BUTTONS = """ {% if perms.ipam.add_ipaddress %} @@ -136,7 +134,7 @@ class VirtualMachineTable(BaseTable): ) primary_ip = tables.Column( linkify=True, - order_by=PRIMARY_IP_ORDERING, + order_by=('primary_ip4', 'primary_ip6'), verbose_name='IP Address' ) tags = TagColumn( From 7c0f32e8ee73914cca3262c68c685da02c82bf5a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Oct 2021 10:04:56 -0400 Subject: [PATCH 02/19] Introduce ConfigItem; add rack elevation parameters --- netbox/dcim/api/serializers.py | 5 +++-- netbox/dcim/models/devices.py | 6 ++---- netbox/dcim/models/racks.py | 10 +++++++--- netbox/extras/admin.py | 6 +++--- netbox/ipam/models/ip.py | 11 ++++------- netbox/netbox/config/__init__.py | 23 +++++++++++++++++++---- netbox/netbox/config/parameters.py | 16 ++++++++++++++++ netbox/netbox/configuration.example.py | 20 -------------------- netbox/netbox/context_processors.py | 4 ++-- netbox/netbox/settings.py | 2 -- netbox/virtualization/models.py | 5 ++--- 11 files changed, 58 insertions(+), 50 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d8c5a7771..a5f4ac5fe 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -13,6 +13,7 @@ from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import ( NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) +from netbox.config import ConfigItem from tenancy.api.nested_serializers import NestedTenantSerializer from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model @@ -229,10 +230,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer): default=RackElevationDetailRenderChoices.RENDER_JSON ) unit_width = serializers.IntegerField( - default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') ) unit_height = serializers.IntegerField( - default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') ) legend_width = serializers.IntegerField( default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index d6b23fed4..418944a4a 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -1,7 +1,6 @@ from collections import OrderedDict import yaml -from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -15,7 +14,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.config import ConfigItem from netbox.models import OrganizationalModel, PrimaryModel from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField @@ -816,8 +815,7 @@ class Device(PrimaryModel, ConfigContextModel): @property def primary_ip(self): - config = ConfigResolver() - if config.PREFER_IPV4 and self.primary_ip4: + if ConfigItem('PREFER_IPV4')() and self.primary_ip4: return self.primary_ip4 elif self.primary_ip6: return self.primary_ip6 diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index a6d7f33af..4a023477f 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -1,6 +1,5 @@ from collections import OrderedDict -from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -15,6 +14,7 @@ from dcim.choices import * from dcim.constants import * from dcim.svg import RackElevationSVG from extras.utils import extras_features +from netbox.config import Config from netbox.models import OrganizationalModel, PrimaryModel from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField @@ -373,8 +373,8 @@ class Rack(PrimaryModel): self, face=DeviceFaceChoices.FACE_FRONT, user=None, - unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH, - unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT, + unit_width=None, + unit_height=None, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, include_images=True, base_url=None @@ -393,6 +393,10 @@ class Rack(PrimaryModel): :param base_url: Base URL for links and images. If none, URLs will be relative. """ elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url) + if unit_width is None or unit_height is None: + config = Config() + unit_width = unit_width or config.RACK_ELEVATION_DEFAULT_UNIT_WIDTH + unit_height = unit_height or config.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT return elevation.render(face, unit_width, unit_height, legend_width) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index e99406e49..cac600626 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -10,9 +10,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin): # ('Authentication', { # 'fields': ('LOGIN_REQUIRED', 'LOGIN_PERSISTENCE', 'LOGIN_TIMEOUT'), # }), - # ('Rack Elevations', { - # 'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'), - # }), + ('Rack Elevations', { + 'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'), + }), ('IPAM', { 'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'), }), diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d655dcb21..af114537a 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -1,10 +1,9 @@ import netaddr -from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models -from django.db.models import F, Q +from django.db.models import F from django.urls import reverse from django.utils.functional import cached_property @@ -17,7 +16,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 netbox.config import Config from utilities.querysets import RestrictedQuerySet from virtualization.models import VirtualMachine @@ -317,8 +316,7 @@ class Prefix(PrimaryModel): }) # Enforce unique IP space (if applicable) - config = ConfigResolver() - if (self.vrf is None and config.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + 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({ @@ -813,8 +811,7 @@ class IPAddress(PrimaryModel): }) # Enforce unique IP space (if applicable) - config = ConfigResolver() - if (self.vrf is None and config.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + 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 index 34ee127fc..7e57f3e8d 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -4,18 +4,20 @@ from django.core.cache import cache from .parameters import PARAMS __all__ = ( - 'ConfigResolver', + 'Config', + 'ConfigItem', 'PARAMS', ) -class ConfigResolver: +class Config: """ - Active NetBox configuration. + Fetch and store in memory the current NetBox configuration. This class must be instantiated prior to access, and + must be re-instantiated each time it's necessary to check for updates to the cached config. """ def __init__(self): self.config = cache.get('config') - self.version = self.config.get('config_version') + self.version = cache.get('config_version') self.defaults = {param.name: param.default for param in PARAMS} def __getattr__(self, item): @@ -33,3 +35,16 @@ class ConfigResolver: return self.defaults[item] raise AttributeError(f"Invalid configuration parameter: {item}") + + +class ConfigItem: + """ + A callable to retrieve a configuration parameter from the cache. This can serve as a placeholder to defer + referencing a configuration parameter. + """ + def __init__(self, item): + self.item = item + + def __call__(self): + config = Config() + return getattr(config, self.item) diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 604131ec1..4e1ff80f4 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -52,4 +52,20 @@ PARAMS = ( field=OptionalBooleanField ), + # Racks + ConfigParam( + name='RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', + label='Rack Unit Height', + default=22, + description="Default unit height for rendered rack elevations", + field=forms.IntegerField + ), + ConfigParam( + name='RACK_ELEVATION_DEFAULT_UNIT_WIDTH', + label='Rack Unit Width', + default=220, + description="Default unit width for rendered rack elevations", + field=forms.IntegerField + ), + ) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 03023740f..bb4a9021e 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -77,14 +77,6 @@ ALLOWED_URL_SCHEMES = ( 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', ) -# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same -# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. -BANNER_TOP = '' -BANNER_BOTTOM = '' - -# Text to include on the login page above the login form. HTML is allowed. -BANNER_LOGIN = '' - # Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set: # BASE_PATH = 'netbox/' BASE_PATH = '' @@ -134,10 +126,6 @@ EMAIL = { 'FROM_EMAIL': '', } -# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table -# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. -ENFORCE_GLOBAL_UNIQUE = False - # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. EXEMPT_VIEW_PERMISSIONS = [ @@ -229,14 +217,6 @@ PLUGINS = [] # } # } -# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to -# prefer IPv4 instead. -PREFER_IPV4 = False - -# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. -RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22 -RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220 - # Remote authentication support REMOTE_AUTH_ENABLED = False REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend' diff --git a/netbox/netbox/context_processors.py b/netbox/netbox/context_processors.py index fee32a063..8ae0a0f26 100644 --- a/netbox/netbox/context_processors.py +++ b/netbox/netbox/context_processors.py @@ -1,7 +1,7 @@ from django.conf import settings as django_settings from extras.registry import registry -from netbox.config import ConfigResolver +from netbox.config import Config def settings_and_registry(request): @@ -10,7 +10,7 @@ def settings_and_registry(request): """ return { 'settings': django_settings, - 'config': ConfigResolver(), + 'config': Config(), '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 0eb164523..248b3d697 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -140,8 +140,6 @@ 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 diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 51d255fc7..f82550b4f 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -8,7 +8,7 @@ from dcim.models import BaseInterface, Device from extras.models import ConfigContextModel from extras.querysets import ConfigContextModelQuerySet from extras.utils import extras_features -from netbox.config import ConfigResolver +from netbox.config import Config from netbox.models import OrganizationalModel, PrimaryModel from utilities.fields import NaturalOrderingField from utilities.ordering import naturalize_interface @@ -340,8 +340,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): @property def primary_ip(self): - config = ConfigResolver() - if config.PREFER_IPV4 and self.primary_ip4: + if Config().PREFER_IPV4 and self.primary_ip4: return self.primary_ip4 elif self.primary_ip6: return self.primary_ip6 From 559dc2f8652959c136727ecf11fc3392fd6ac4f5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Oct 2021 10:24:33 -0400 Subject: [PATCH 03/19] Add ALLOWED_URL_SCHEMES --- netbox/extras/admin.py | 8 +++----- netbox/netbox/config/parameters.py | 14 ++++++++++++++ netbox/netbox/configuration.example.py | 5 ----- netbox/netbox/settings.py | 3 --- netbox/utilities/templatetags/helpers.py | 3 ++- netbox/utilities/validators.py | 9 +++++++-- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index cac600626..5e2de7e16 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -16,11 +16,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin): ('IPAM', { 'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'), }), - # ('Security', { - # 'fields': ( - # 'ALLOWED_URL_SCHEMES', 'EXEMPT_VIEW_PERMISSIONS', - # ), - # }), + ('Security', { + 'fields': ('ALLOWED_URL_SCHEMES',), + }), ('Banners', { 'fields': ('BANNER_LOGIN', 'BANNER_TOP', 'BANNER_BOTTOM'), }), diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 4e1ff80f4..8ad02a5dd 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.postgres.forms import SimpleArrayField class OptionalBooleanSelect(forms.Select): @@ -68,4 +69,17 @@ PARAMS = ( field=forms.IntegerField ), + # Security + ConfigParam( + name='ALLOWED_URL_SCHEMES', + label='Allowed URL schemes', + default=( + 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', + 'xmpp', + ), + description="Permitted schemes for URLs in user-provided content", + field=SimpleArrayField, + field_kwargs={'base_field': forms.CharField()} + ), + ) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index bb4a9021e..63e74524a 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -72,11 +72,6 @@ ADMINS = [ # ('John Doe', 'jdoe@example.com'), ] -# URL schemes that are allowed within links in NetBox -ALLOWED_URL_SCHEMES = ( - 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', -) - # Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set: # BASE_PATH = 'netbox/' BASE_PATH = '' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 248b3d697..f42c99dbf 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -122,9 +122,6 @@ 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) diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 1b5bb220d..833d19535 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -14,6 +14,7 @@ from django.utils.html import strip_tags from django.utils.safestring import mark_safe from markdown import markdown +from netbox.config import Config from utilities.forms import get_selected_values, TableConfigForm from utilities.utils import foreground_color @@ -44,7 +45,7 @@ def render_markdown(value): value = strip_tags(value) # Sanitize Markdown links - schemes = '|'.join(settings.ALLOWED_URL_SCHEMES) + schemes = '|'.join(Config().ALLOWED_URL_SCHEMES) pattern = fr'\[(.+)\]\((?!({schemes})).*:(.+)\)' value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE) diff --git a/netbox/utilities/validators.py b/netbox/utilities/validators.py index b087b0867..5b5775482 100644 --- a/netbox/utilities/validators.py +++ b/netbox/utilities/validators.py @@ -1,9 +1,10 @@ import re -from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator +from netbox.config import Config + class EnhancedURLValidator(URLValidator): """ @@ -19,7 +20,11 @@ class EnhancedURLValidator(URLValidator): r'(?::\d{2,5})?' # Port number r'(?:[/?#][^\s]*)?' # Path r'\Z', re.IGNORECASE) - schemes = settings.ALLOWED_URL_SCHEMES + + def __init__(self, schemes=None, **kwargs): + super().__init__(**kwargs) + if schemes is not None: + self.schemes = Config().ALLOWED_URL_SCHEMES class ExclusionValidator(BaseValidator): From 94804fecd8c00043dcdf50c45393f86a1e87b6c7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 26 Oct 2021 10:57:33 -0400 Subject: [PATCH 04/19] Add MAINTENANCE_MODE, MAPS_URL --- netbox/extras/admin.py | 6 +++--- netbox/netbox/config/parameters.py | 19 +++++++++++++++++-- netbox/netbox/configuration.example.py | 6 ------ netbox/netbox/settings.py | 2 -- netbox/templates/base/layout.html | 2 +- netbox/templates/dcim/site.html | 4 ++-- netbox/users/views.py | 4 ++-- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 5e2de7e16..6df9c55cf 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -28,9 +28,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin): # ('Pagination', { # 'fields': ('MAX_PAGE_SIZE', 'PAGINATE_COUNT'), # }), - # ('Miscellaneous', { - # 'fields': ('GRAPHQL_ENABLED', 'METRICS_ENABLED', 'MAINTENANCE_MODE', 'MAPS_URL'), - # }), + ('Miscellaneous', { + 'fields': ('MAINTENANCE_MODE', 'MAPS_URL'), + }), ('Config Revision', { 'fields': ('comment',), }) diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 8ad02a5dd..4e77cec0e 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -56,14 +56,14 @@ PARAMS = ( # Racks ConfigParam( name='RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', - label='Rack Unit Height', + label='Rack unit height', default=22, description="Default unit height for rendered rack elevations", field=forms.IntegerField ), ConfigParam( name='RACK_ELEVATION_DEFAULT_UNIT_WIDTH', - label='Rack Unit Width', + label='Rack unit width', default=220, description="Default unit width for rendered rack elevations", field=forms.IntegerField @@ -82,4 +82,19 @@ PARAMS = ( field_kwargs={'base_field': forms.CharField()} ), + # Miscellaneous + ConfigParam( + name='MAINTENANCE_MODE', + label='Maintenance mode', + default=False, + description="Enable maintenance mode", + field=OptionalBooleanField + ), + ConfigParam( + name='MAPS_URL', + label='Maps URL', + default='https://maps.google.com/?q=', + description="Base URL for mapping geographic locations" + ), + ) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 63e74524a..c40ea4eff 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -158,12 +158,6 @@ LOGIN_REQUIRED = False # re-authenticate. (Default: 1209600 [14 days]) LOGIN_TIMEOUT = None -# Setting this to True will display a "maintenance mode" banner at the top of every page. -MAINTENANCE_MODE = False - -# The URL to use when mapping physical addresses or GPS coordinates -MAPS_URL = 'https://maps.google.com/?q=' - # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request # all objects by specifying "?limit=0". diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f42c99dbf..beae4d568 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -128,8 +128,6 @@ 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', {}) diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index a4c8c77b6..2770a6dc6 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -64,7 +64,7 @@ {% endif %} - {% if settings.MAINTENANCE_MODE %} + {% if config.MAINTENANCE_MODE %}