1
0
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:
jeremystretch
2021-10-25 14:42:20 -04:00
parent 61d2158f76
commit 82243732a1
18 changed files with 375 additions and 109 deletions

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -3,4 +3,5 @@ from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .customfields import *
from .config import *
from .scripts import *

View 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

View 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)),
],
),
]

View File

@ -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',

View File

@ -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

View File

@ -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)

View File

@ -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

View 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}")

View 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
),
)

View File

@ -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 {},
}

View File

@ -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(

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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

View File

@ -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(