Merge pull request #7778 from netbox-community/7775-dynamic-config

7775 dynamic config
This commit is contained in:
Jeremy Stretch 2021-11-08 15:54:25 -05:00 committed by GitHub
commit c0653da736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 84 additions and 68 deletions

View File

@ -31,6 +31,41 @@ This defines custom content to be displayed on the login page above the login fo
--- ---
## CHANGELOG_RETENTION
Default: 90
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
changes in the database indefinitely.
!!! warning
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CUSTOM_VALIDATORS
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
```python
CUSTOM_VALIDATORS = {
"dcim.site": [
{
"name": {
"min_length": 5,
"max_length": 30
}
},
"my_plugin.validators.Validator1"
],
"dim.device": [
"my_plugin.validators.Validator1"
]
}
```
---
## ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
Default: False Default: False
@ -39,6 +74,14 @@ By default, NetBox will permit users to create duplicate prefixes and IP address
--- ---
## GRAPHQL_ENABLED
Default: True
Setting this to False will disable the GraphQL API.
---
## MAINTENANCE_MODE ## MAINTENANCE_MODE
Default: False Default: False

View File

@ -25,18 +25,6 @@ BASE_PATH = 'netbox/'
--- ---
## CHANGELOG_RETENTION
Default: 90
The number of days to retain logged changes (object creations, updates, and deletions). Set this to `0` to retain
changes in the database indefinitely.
!!! warning
If enabling indefinite changelog retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CORS_ORIGIN_ALLOW_ALL ## CORS_ORIGIN_ALLOW_ALL
Default: False Default: False
@ -61,22 +49,6 @@ CORS_ORIGIN_WHITELIST = [
--- ---
## CUSTOM_VALIDATORS
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
```python
CUSTOM_VALIDATORS = {
'dcim.site': (
Validator1,
Validator2,
Validator3
)
}
```
---
## DEBUG ## DEBUG
Default: False Default: False
@ -168,14 +140,6 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
--- ---
## GRAPHQL_ENABLED
Default: True
Setting this to False will disable the GraphQL API.
---
## HTTP_PROXIES ## HTTP_PROXIES
Default: None Default: None

View File

@ -3,6 +3,7 @@
### Enhancements ### Enhancements
* [#7619](https://github.com/netbox-community/netbox/issues/7619) - Permit custom validation rules to be defined as plain data or dotted path to class * [#7619](https://github.com/netbox-community/netbox/issues/7619) - Permit custom validation rules to be defined as plain data or dotted path to class
* [#7775](https://github.com/netbox-community/netbox/issues/7775) - Enable dynamic config for `CHANGELOG_RETENTION`, `CUSTOM_VALIDATORS`, and `GRAPHQL_ENABLED`
### Bug Fixes ### Bug Fixes

View File

@ -27,11 +27,14 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
('Pagination', { ('Pagination', {
'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'), 'fields': ('PAGINATE_COUNT', 'MAX_PAGE_SIZE'),
}), }),
('Validation', {
'fields': ('CUSTOM_VALIDATORS',),
}),
('NAPALM', { ('NAPALM', {
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'), 'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
}), }),
('Miscellaneous', { ('Miscellaneous', {
'fields': ('MAINTENANCE_MODE', 'MAPS_URL'), 'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
}), }),
('Config Revision', { ('Config Revision', {
'fields': ('comment',), 'fields': ('comment',),

View File

@ -10,12 +10,14 @@ from django.utils import timezone
from packaging import version from packaging import version
from extras.models import ObjectChange from extras.models import ObjectChange
from netbox.config import Config
class Command(BaseCommand): class Command(BaseCommand):
help = "Perform nightly housekeeping tasks. (This command can be run at any time.)" help = "Perform nightly housekeeping tasks. (This command can be run at any time.)"
def handle(self, *args, **options): def handle(self, *args, **options):
config = Config()
# Clear expired authentication sessions (essentially replicating the `clearsessions` command) # Clear expired authentication sessions (essentially replicating the `clearsessions` command)
if options['verbosity']: if options['verbosity']:
@ -37,10 +39,10 @@ class Command(BaseCommand):
# Delete expired ObjectRecords # Delete expired ObjectRecords
if options['verbosity']: if options['verbosity']:
self.stdout.write("[*] Checking for expired changelog records") self.stdout.write("[*] Checking for expired changelog records")
if settings.CHANGELOG_RETENTION: if config.CHANGELOG_RETENTION:
cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION) cutoff = timezone.now() - timedelta(days=config.CHANGELOG_RETENTION)
if options['verbosity'] >= 2: if options['verbosity'] >= 2:
self.stdout.write(f"\tRetention period: {settings.CHANGELOG_RETENTION} days") self.stdout.write(f"\tRetention period: {config.CHANGELOG_RETENTION} days")
self.stdout.write(f"\tCut-off time: {cutoff}") self.stdout.write(f"\tCut-off time: {cutoff}")
expired_records = ObjectChange.objects.filter(time__lt=cutoff).count() expired_records = ObjectChange.objects.filter(time__lt=cutoff).count()
if expired_records: if expired_records:
@ -58,7 +60,7 @@ class Command(BaseCommand):
self.stdout.write("\tNo expired records found.", self.style.SUCCESS) self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
elif options['verbosity']: elif options['verbosity']:
self.stdout.write( self.stdout.write(
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {settings.CHANGELOG_RETENTION})" f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
) )
# Check for new releases (if enabled) # Check for new releases (if enabled)

View File

@ -1,13 +1,13 @@
import importlib import importlib
import logging import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import m2m_changed, post_save, pre_delete from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from extras.validators import CustomValidator from extras.validators import CustomValidator
from netbox.config import get_config
from netbox.signals import post_clean from netbox.signals import post_clean
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange from .models import ConfigRevision, CustomField, ObjectChange
@ -159,8 +159,9 @@ m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_type
@receiver(post_clean) @receiver(post_clean)
def run_custom_validators(sender, instance, **kwargs): def run_custom_validators(sender, instance, **kwargs):
config = get_config()
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}' model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
validators = settings.CUSTOM_VALIDATORS.get(model_name, []) validators = config.CUSTOM_VALIDATORS.get(model_name, [])
for validator in validators: for validator in validators:

View File

@ -94,6 +94,15 @@ PARAMS = (
field=forms.IntegerField field=forms.IntegerField
), ),
# Validation
ConfigParam(
name='CUSTOM_VALIDATORS',
label='Custom validators',
default={},
description="Custom validation rules (JSON)",
field=forms.JSONField
),
# NAPALM # NAPALM
ConfigParam( ConfigParam(
name='NAPALM_USERNAME', name='NAPALM_USERNAME',
@ -130,6 +139,20 @@ PARAMS = (
description="Enable maintenance mode", description="Enable maintenance mode",
field=forms.BooleanField field=forms.BooleanField
), ),
ConfigParam(
name='GRAPHQL_ENABLED',
label='GraphQL enabled',
default=True,
description="Enable the GraphQL API",
field=forms.BooleanField
),
ConfigParam(
name='CHANGELOG_RETENTION',
label='Changelog retention',
default=90,
description="Days to retain changelog history (set to zero for unlimited)",
field=forms.IntegerField
),
ConfigParam( ConfigParam(
name='MAPS_URL', name='MAPS_URL',
label='Maps URL', label='Maps URL',

View File

@ -76,9 +76,6 @@ ADMINS = [
# BASE_PATH = 'netbox/' # BASE_PATH = 'netbox/'
BASE_PATH = '' BASE_PATH = ''
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
CHANGELOG_RETENTION = 90
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
@ -90,20 +87,6 @@ CORS_ORIGIN_REGEX_WHITELIST = [
# r'^(https?://)?(\w+\.)?example\.com$', # r'^(https?://)?(\w+\.)?example\.com$',
] ]
# Specify any custom validators here, as a mapping of model to a list of validators classes. Validators should be
# instances of or inherit from CustomValidator.
# from extras.validators import CustomValidator
CUSTOM_VALIDATORS = {
# 'dcim.site': [
# CustomValidator({
# 'name': {
# 'min_length': 10,
# 'regex': r'\d{3}$',
# }
# })
# ],
}
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
# on a production system. # on a production system.
@ -129,9 +112,6 @@ EXEMPT_VIEW_PERMISSIONS = [
# 'ipam.prefix', # 'ipam.prefix',
] ]
# Enable the GraphQL API
GRAPHQL_ENABLED = True
# HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). # HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks).
# HTTP_PROXIES = { # HTTP_PROXIES = {
# 'http': 'http://10.10.1.10:3128', # 'http': 'http://10.10.1.10:3128',

View File

@ -6,6 +6,7 @@ from graphene_django.views import GraphQLView as GraphQLView_
from rest_framework.exceptions import AuthenticationFailed from rest_framework.exceptions import AuthenticationFailed
from netbox.api.authentication import TokenAuthentication from netbox.api.authentication import TokenAuthentication
from netbox.config import get_config
class GraphQLView(GraphQLView_): class GraphQLView(GraphQLView_):
@ -15,9 +16,10 @@ class GraphQLView(GraphQLView_):
graphiql_template = 'graphiql.html' graphiql_template = 'graphiql.html'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
config = get_config()
# Enforce GRAPHQL_ENABLED # Enforce GRAPHQL_ENABLED
if not settings.GRAPHQL_ENABLED: if not config.GRAPHQL_ENABLED:
return HttpResponseNotFound("The GraphQL API is not enabled.") return HttpResponseNotFound("The GraphQL API is not enabled.")
# Attempt to authenticate the user using a DRF token, if provided # Attempt to authenticate the user using a DRF token, if provided

View File

@ -80,11 +80,9 @@ ADMINS = getattr(configuration, 'ADMINS', [])
BASE_PATH = getattr(configuration, 'BASE_PATH', '') BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH: if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only 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_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CUSTOM_VALIDATORS = getattr(configuration, 'CUSTOM_VALIDATORS', {})
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False) DEBUG = getattr(configuration, 'DEBUG', False)
@ -92,7 +90,6 @@ DEVELOPER = getattr(configuration, 'DEVELOPER', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {}) EMAIL = getattr(configuration, 'EMAIL', {})
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
GRAPHQL_ENABLED = getattr(configuration, 'GRAPHQL_ENABLED', True)
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None) HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1')) INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
LOGGING = getattr(configuration, 'LOGGING', {}) LOGGING = getattr(configuration, 'LOGGING', {})

View File

@ -127,7 +127,7 @@
</a> </a>
{# GraphQL API #} {# GraphQL API #}
{% if settings.GRAPHQL_ENABLED %} {% if config.GRAPHQL_ENABLED %}
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>