mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on #5883
This commit is contained in:
@ -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
|
||||
|
@ -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_ip4 = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='IPv4 Address'
|
||||
|
@ -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)
|
||||
|
@ -3,4 +3,5 @@ from .filtersets import *
|
||||
from .bulk_edit import *
|
||||
from .bulk_import import *
|
||||
from .customfields import *
|
||||
from .config import *
|
||||
from .scripts import *
|
||||
|
67
netbox/extras/forms/config.py
Normal file
67
netbox/extras/forms/config.py
Normal file
@ -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}<br />' if param.description else ''
|
||||
# help_text += f'Current value: <strong>{getattr(settings, param.name)}</strong>'
|
||||
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
|
20
netbox/extras/migrations/0064_configrevision.py
Normal file
20
netbox/extras/migrations/0064_configrevision.py
Normal file
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
35
netbox/netbox/config/__init__.py
Normal file
35
netbox/netbox/config/__init__.py
Normal file
@ -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}")
|
55
netbox/netbox/config/parameters.py
Normal file
55
netbox/netbox/config/parameters.py
Normal file
@ -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
|
||||
),
|
||||
|
||||
)
|
@ -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 {},
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -58,9 +58,9 @@
|
||||
|
||||
</nav>
|
||||
|
||||
{% if settings.BANNER_TOP %}
|
||||
{% if config.BANNER_TOP %}
|
||||
<div class="alert alert-info text-center mx-3" role="alert">
|
||||
{{ settings.BANNER_TOP|safe }}
|
||||
{{ config.BANNER_TOP|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -98,9 +98,9 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
{% if config.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center mx-3" role="alert">
|
||||
{{ settings.BANNER_BOTTOM|safe }}
|
||||
{{ config.BANNER_BOTTOM|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -7,9 +7,9 @@
|
||||
<main class="login-container text-center">
|
||||
|
||||
{# Login banner #}
|
||||
{% if settings.BANNER_LOGIN %}
|
||||
{% if config.BANNER_LOGIN %}
|
||||
<div class="alert alert-secondary mw-90 mw-md-75 mw-lg-80 mw-xl-75 mw-xxl-50" role="alert">
|
||||
{{ settings.BANNER_LOGIN|safe }}
|
||||
{{ config.BANNER_LOGIN|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?vminterface={{ record.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-sm btn-success" title="Add IP Address">
|
||||
@ -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(
|
||||
|
Reference in New Issue
Block a user