From b567dfd09e0f4c1ad203f208cde30f3fac280b14 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Nov 2023 12:44:42 -0500 Subject: [PATCH] Move ConfigRevision resources from extras to core --- netbox/core/filtersets.py | 21 ++++ netbox/core/forms/filtersets.py | 7 ++ netbox/core/forms/model_forms.py | 118 ++++++++++++++++- netbox/core/models/config.py | 2 +- netbox/core/signals.py | 11 ++ netbox/core/tables/__init__.py | 1 + netbox/core/tables/config.py | 33 +++++ netbox/core/urls.py | 7 ++ netbox/core/views.py | 73 ++++++++++- netbox/extras/filtersets.py | 27 +--- netbox/extras/forms/filtersets.py | 7 -- netbox/extras/forms/model_forms.py | 119 +----------------- netbox/extras/signals.py | 13 -- netbox/extras/tables/tables.py | 27 ---- netbox/extras/urls.py | 7 -- netbox/extras/views.py | 71 +---------- netbox/netbox/navigation/menu.py | 6 +- .../{extras => core}/configrevision.html | 6 +- .../configrevision_restore.html | 6 +- 19 files changed, 279 insertions(+), 283 deletions(-) create mode 100644 netbox/core/tables/config.py rename netbox/templates/{extras => core}/configrevision.html (96%) rename netbox/templates/{extras => core}/configrevision_restore.html (85%) diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index 410e2e80c..a293b44ec 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -9,6 +9,7 @@ from .choices import * from .models import * __all__ = ( + 'ConfigRevisionFilterSet', 'DataFileFilterSet', 'DataSourceFilterSet', 'JobFilterSet', @@ -123,3 +124,23 @@ class JobFilterSet(BaseFilterSet): Q(user__username__icontains=value) | Q(name__icontains=value) ) + + +class ConfigRevisionFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + + class Meta: + model = ConfigRevision + fields = [ + 'id', + ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(comment__icontains=value) + ) diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 14f0fb6ed..f21bd3f87 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -12,6 +12,7 @@ from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleC from utilities.forms.widgets import APISelectMultiple, DateTimePicker __all__ = ( + 'ConfigRevisionFilterForm', 'DataFileFilterForm', 'DataSourceFilterForm', 'JobFilterForm', @@ -123,3 +124,9 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): api_url='/api/users/users/', ) ) + + +class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm): + fieldsets = ( + (None, ('q', 'filter_id')), + ) diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index e3184acf6..652728734 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -1,22 +1,28 @@ import copy +import json from django import forms +from django.conf import settings from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin from core.models import * +from netbox.config import get_config, PARAMS from netbox.forms import NetBoxModelForm from netbox.registry import registry from netbox.utils import get_data_backend_choices -from utilities.forms import get_field_value +from utilities.forms import BootstrapMixin, get_field_value from utilities.forms.fields import CommentField from utilities.forms.widgets import HTMXSelect __all__ = ( + 'ConfigRevisionForm', 'DataSourceForm', 'ManagedFileForm', ) +EMPTY_VALUES = ('', None, [], ()) + class DataSourceForm(NetBoxModelForm): type = forms.ChoiceField( @@ -111,3 +117,113 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm): new_file.write(self.cleaned_data['upload_file'].read()) return super().save(*args, **kwargs) + + +class ConfigFormMetaclass(forms.models.ModelFormMetaclass): + + def __new__(mcs, name, bases, attrs): + + # Emulate a declared field for each supported configuration parameter + param_fields = {} + for param in PARAMS: + field_kwargs = { + 'required': False, + 'label': param.label, + 'help_text': param.description, + } + field_kwargs.update(**param.field_kwargs) + param_fields[param.name] = param.field(**field_kwargs) + attrs.update(param_fields) + + return super().__new__(mcs, name, bases, attrs) + + +class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass): + """ + Form for creating a new ConfigRevision. + """ + + fieldsets = ( + (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')), + (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')), + (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')), + (_('Security'), ('ALLOWED_URL_SCHEMES',)), + (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')), + (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')), + (_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')), + (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)), + (_('Miscellaneous'), ( + 'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL', + )), + (_('Config Revision'), ('comment',)) + ) + + class Meta: + model = ConfigRevision + fields = '__all__' + widgets = { + 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}), + 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}), + 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}), + 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}), + 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}), + 'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}), + 'comment': forms.Textarea(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Append current parameter values to form field help texts and check for static configurations + config = get_config() + for param in PARAMS: + value = getattr(config, param.name) + + # Set the field's initial value, if it can be serialized. (This may not be the case e.g. for + # CUSTOM_VALIDATORS, which may reference Python objects.) + try: + json.dumps(value) + if type(value) in (tuple, list): + self.fields[param.name].initial = ', '.join(value) + else: + self.fields[param.name].initial = value + except TypeError: + pass + + # Check whether this parameter is statically configured (e.g. in configuration.py) + if hasattr(settings, param.name): + self.fields[param.name].disabled = True + self.fields[param.name].help_text = _( + 'This parameter has been defined statically and cannot be modified.' + ) + continue + + # Set the field's help text + help_text = self.fields[param.name].help_text + if help_text: + help_text += '
' # Line break + help_text += _('Current value: {value}').format(value=value or '—') + if value == param.default: + help_text += _(' (default)') + self.fields[param.name].help_text = help_text + + 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/core/models/config.py b/netbox/core/models/config.py index 08326f5b9..6c8e41477 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -51,7 +51,7 @@ class ConfigRevision(models.Model): def get_absolute_url(self): if not self.pk: return reverse('core:config') # Default config view - return reverse('extras:configrevision', args=[self.pk]) + return reverse('core:configrevision', args=[self.pk]) def activate(self): """ diff --git a/netbox/core/signals.py b/netbox/core/signals.py index a39a87c6a..cd1633a1a 100644 --- a/netbox/core/signals.py +++ b/netbox/core/signals.py @@ -1,5 +1,8 @@ +from django.db.models.signals import post_save from django.dispatch import Signal, receiver +from .models import ConfigRevision + __all__ = ( 'post_sync', 'pre_sync', @@ -19,3 +22,11 @@ def auto_sync(instance, **kwargs): for autosync in AutoSyncRecord.objects.filter(datafile__source=instance).prefetch_related('object'): autosync.object.sync(save=True) + + +@receiver(post_save, sender=ConfigRevision) +def update_config(sender, instance, **kwargs): + """ + Update the cached NetBox configuration when a new ConfigRevision is created. + """ + instance.activate() diff --git a/netbox/core/tables/__init__.py b/netbox/core/tables/__init__.py index 052f68b68..69f9d8a48 100644 --- a/netbox/core/tables/__init__.py +++ b/netbox/core/tables/__init__.py @@ -1,2 +1,3 @@ +from .config import * from .data import * from .jobs import * diff --git a/netbox/core/tables/config.py b/netbox/core/tables/config.py new file mode 100644 index 000000000..9d4cb6393 --- /dev/null +++ b/netbox/core/tables/config.py @@ -0,0 +1,33 @@ +from django.utils.translation import gettext_lazy as _ + +from core.models import ConfigRevision +from netbox.tables import NetBoxTable, columns + +__all__ = ( + 'ConfigRevisionTable', +) + +REVISION_BUTTONS = """ +{% if not record.is_active %} + + + +{% endif %} +""" + + +class ConfigRevisionTable(NetBoxTable): + is_active = columns.BooleanColumn( + verbose_name=_('Is Active'), + ) + actions = columns.ActionsColumn( + actions=('delete',), + extra_buttons=REVISION_BUTTONS + ) + + class Meta(NetBoxTable.Meta): + model = ConfigRevision + fields = ( + 'pk', 'id', 'is_active', 'created', 'comment', + ) + default_columns = ('pk', 'id', 'is_active', 'created', 'comment') diff --git a/netbox/core/urls.py b/netbox/core/urls.py index f17a50c81..77c0d3194 100644 --- a/netbox/core/urls.py +++ b/netbox/core/urls.py @@ -25,6 +25,13 @@ urlpatterns = ( path('jobs//', views.JobView.as_view(), name='job'), path('jobs//delete/', views.JobDeleteView.as_view(), name='job_delete'), + # Config revisions + path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'), + path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'), + path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'), + path('config-revisions//restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'), + path('config-revisions//', include(get_model_urls('core', 'configrevision'))), + # Configuration path('config/', views.ConfigView.as_view(), name='config'), diff --git a/netbox/core/views.py b/netbox/core/views.py index 2152eb7f5..61ef93642 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -1,12 +1,13 @@ from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect +from django.http import HttpResponseForbidden +from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic import View -from core.models import ConfigRevision -from netbox.config import get_config +from netbox.config import get_config, PARAMS from netbox.views import generic from netbox.views.generic.base import BaseObjectView from utilities.utils import count_related -from utilities.views import register_model_view +from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables from .models import * @@ -164,3 +165,67 @@ class ConfigView(generic.ObjectView): return ConfigRevision( data=get_config().defaults ) + + +class ConfigRevisionListView(generic.ObjectListView): + queryset = ConfigRevision.objects.all() + filterset = filtersets.ConfigRevisionFilterSet + filterset_form = forms.ConfigRevisionFilterForm + table = tables.ConfigRevisionTable + + +@register_model_view(ConfigRevision) +class ConfigRevisionView(generic.ObjectView): + queryset = ConfigRevision.objects.all() + + +class ConfigRevisionEditView(generic.ObjectEditView): + queryset = ConfigRevision.objects.all() + form = forms.ConfigRevisionForm + + +@register_model_view(ConfigRevision, 'delete') +class ConfigRevisionDeleteView(generic.ObjectDeleteView): + queryset = ConfigRevision.objects.all() + + +class ConfigRevisionBulkDeleteView(generic.BulkDeleteView): + queryset = ConfigRevision.objects.all() + filterset = filtersets.ConfigRevisionFilterSet + table = tables.ConfigRevisionTable + + +class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): + + def get_required_permission(self): + return 'core.configrevision_edit' + + def get(self, request, pk): + candidate_config = get_object_or_404(ConfigRevision, pk=pk) + + # Get the current ConfigRevision + config_version = get_config().version + current_config = ConfigRevision.objects.filter(pk=config_version).first() + + params = [] + for param in PARAMS: + params.append(( + param.name, + current_config.data.get(param.name, None), + candidate_config.data.get(param.name, None) + )) + + return render(request, 'core/configrevision_restore.html', { + 'object': candidate_config, + 'params': params, + }) + + def post(self, request, pk): + if not request.user.has_perm('core.configrevision_edit'): + return HttpResponseForbidden() + + candidate_config = get_object_or_404(ConfigRevision, pk=pk) + candidate_config.activate() + messages.success(request, f"Restored configuration revision #{pk}") + + return redirect(candidate_config.get_absolute_url()) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 1443c0b9d..d336394f9 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext as _ -from core.models import ConfigRevision, DataSource +from core.models import DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from tenancy.models import Tenant, TenantGroup @@ -17,7 +17,6 @@ from .models import * __all__ = ( 'BookmarkFilterSet', 'ConfigContextFilterSet', - 'ConfigRevisionFilterSet', 'ConfigTemplateFilterSet', 'ContentTypeFilterSet', 'CustomFieldChoiceSetFilterSet', @@ -625,27 +624,3 @@ class ContentTypeFilterSet(django_filters.FilterSet): Q(app_label__icontains=value) | Q(model__icontains=value) ) - - -# -# ConfigRevisions -# - -class ConfigRevisionFilterSet(BaseFilterSet): - q = django_filters.CharFilter( - method='search', - label=_('Search'), - ) - - class Meta: - model = ConfigRevision - fields = [ - 'id', - ] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(comment__icontains=value) - ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 28aefa685..b68845c2f 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -18,7 +18,6 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( 'ConfigContextFilterForm', - 'ConfigRevisionFilterForm', 'ConfigTemplateFilterForm', 'CustomFieldChoiceSetFilterForm', 'CustomFieldFilterForm', @@ -499,9 +498,3 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): api_url='/api/extras/content-types/', ) ) - - -class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm): - fieldsets = ( - (None, ('q', 'filter_id')), - ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 01cd45bab..9553a839a 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -1,16 +1,14 @@ import json from django import forms -from django.conf import settings from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin -from core.models import ConfigRevision, ContentType +from core.models import ContentType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * -from netbox.config import get_config, PARAMS from netbox.forms import NetBoxModelForm from tenancy.models import Tenant, TenantGroup from utilities.forms import BootstrapMixin, add_blank_choice @@ -21,11 +19,9 @@ from utilities.forms.fields import ( from utilities.forms.widgets import ChoicesWidget from virtualization.models import Cluster, ClusterGroup, ClusterType - __all__ = ( 'BookmarkForm', 'ConfigContextForm', - 'ConfigRevisionForm', 'ConfigTemplateForm', 'CustomFieldChoiceSetForm', 'CustomFieldForm', @@ -445,116 +441,3 @@ class JournalEntryForm(NetBoxModelForm): 'assigned_object_type': forms.HiddenInput, 'assigned_object_id': forms.HiddenInput, } - - -EMPTY_VALUES = ('', None, [], ()) - - -class ConfigFormMetaclass(forms.models.ModelFormMetaclass): - - def __new__(mcs, name, bases, attrs): - - # Emulate a declared field for each supported configuration parameter - param_fields = {} - for param in PARAMS: - field_kwargs = { - 'required': False, - 'label': param.label, - 'help_text': param.description, - } - field_kwargs.update(**param.field_kwargs) - param_fields[param.name] = param.field(**field_kwargs) - attrs.update(param_fields) - - return super().__new__(mcs, name, bases, attrs) - - -class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass): - """ - Form for creating a new ConfigRevision. - """ - - fieldsets = ( - (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')), - (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')), - (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')), - (_('Security'), ('ALLOWED_URL_SCHEMES',)), - (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')), - (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')), - (_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')), - (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)), - (_('Miscellaneous'), ( - 'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL', - )), - (_('Config Revision'), ('comment',)) - ) - - class Meta: - model = ConfigRevision - fields = '__all__' - widgets = { - 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}), - 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}), - 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}), - 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}), - 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}), - 'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}), - 'comment': forms.Textarea(), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Append current parameter values to form field help texts and check for static configurations - config = get_config() - for param in PARAMS: - value = getattr(config, param.name) - - # Set the field's initial value, if it can be serialized. (This may not be the case e.g. for - # CUSTOM_VALIDATORS, which may reference Python objects.) - try: - json.dumps(value) - if type(value) in (tuple, list): - self.fields[param.name].initial = ', '.join(value) - else: - self.fields[param.name].initial = value - except TypeError: - pass - - # Check whether this parameter is statically configured (e.g. in configuration.py) - if hasattr(settings, param.name): - self.fields[param.name].disabled = True - self.fields[param.name].help_text = _( - 'This parameter has been defined statically and cannot be modified.' - ) - continue - - # Set the field's help text - help_text = self.fields[param.name].help_text - if help_text: - help_text += '
' # Line break - help_text += _('Current value: {value}').format(value=value or '—') - if value == param.default: - help_text += _(' (default)') - self.fields[param.name].help_text = help_text - - 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/signals.py b/netbox/extras/signals.py index efecaddc8..e1d424960 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -8,7 +8,6 @@ from django.dispatch import receiver, Signal from django.utils.translation import gettext_lazy as _ from django_prometheus.models import model_deletes, model_inserts, model_updates -from core.models import ConfigRevision from extras.validators import CustomValidator from netbox.config import get_config from netbox.context import current_request, webhooks_queue @@ -220,18 +219,6 @@ def run_delete_validators(sender, instance, **kwargs): ) -# -# Dynamic configuration -# - -@receiver(post_save, sender=ConfigRevision) -def update_config(sender, instance, **kwargs): - """ - Update the cached NetBox configuration when a new ConfigRevision is created. - """ - instance.activate() - - # # Tags # diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 02593e2ba..b78ab0c94 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -4,7 +4,6 @@ import django_tables2 as tables from django.conf import settings from django.utils.translation import gettext_lazy as _ -from core.models import ConfigRevision from extras.models import * from netbox.tables import NetBoxTable, columns from .template_code import * @@ -12,7 +11,6 @@ from .template_code import * __all__ = ( 'BookmarkTable', 'ConfigContextTable', - 'ConfigRevisionTable', 'ConfigTemplateTable', 'CustomFieldChoiceSetTable', 'CustomFieldTable', @@ -35,31 +33,6 @@ IMAGEATTACHMENT_IMAGE = ''' {% endif %} ''' -REVISION_BUTTONS = """ -{% if not record.is_active %} - - - -{% endif %} -""" - - -class ConfigRevisionTable(NetBoxTable): - is_active = columns.BooleanColumn( - verbose_name=_('Is Active'), - ) - actions = columns.ActionsColumn( - actions=('delete',), - extra_buttons=REVISION_BUTTONS - ) - - class Meta(NetBoxTable.Meta): - model = ConfigRevision - fields = ( - 'pk', 'id', 'is_active', 'created', 'comment', - ) - default_columns = ('pk', 'id', 'is_active', 'created', 'comment') - class CustomFieldTable(NetBoxTable): name = tables.Column( diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index fd95186e4..bcab007e7 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -98,13 +98,6 @@ urlpatterns = [ path('journal-entries/import/', views.JournalEntryBulkImportView.as_view(), name='journalentry_import'), path('journal-entries//', include(get_model_urls('extras', 'journalentry'))), - # Config revisions - path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'), - path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'), - path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'), - path('config-revisions//restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'), - path('config-revisions//', include(get_model_urls('extras', 'configrevision'))), - # Change logging path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), path('changelog//', include(get_model_urls('extras', 'objectchange'))), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ba2415f9e..b62165e1a 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -11,11 +11,10 @@ from django.views.generic import View from core.choices import JobStatusChoices, ManagedFileRootPathChoices from core.forms import ManagedFileForm -from core.models import ConfigRevision, Job +from core.models import Job from core.tables import JobTable from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class -from netbox.config import get_config, PARAMS from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic from utilities.forms import ConfirmationForm, get_field_value @@ -1316,74 +1315,6 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, View): }) -# -# Config Revisions -# - -class ConfigRevisionListView(generic.ObjectListView): - queryset = ConfigRevision.objects.all() - filterset = filtersets.ConfigRevisionFilterSet - filterset_form = forms.ConfigRevisionFilterForm - table = tables.ConfigRevisionTable - - -@register_model_view(ConfigRevision) -class ConfigRevisionView(generic.ObjectView): - queryset = ConfigRevision.objects.all() - - -class ConfigRevisionEditView(generic.ObjectEditView): - queryset = ConfigRevision.objects.all() - form = forms.ConfigRevisionForm - - -@register_model_view(ConfigRevision, 'delete') -class ConfigRevisionDeleteView(generic.ObjectDeleteView): - queryset = ConfigRevision.objects.all() - - -class ConfigRevisionBulkDeleteView(generic.BulkDeleteView): - queryset = ConfigRevision.objects.all() - filterset = filtersets.ConfigRevisionFilterSet - table = tables.ConfigRevisionTable - - -class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): - - def get_required_permission(self): - return 'extras.configrevision_edit' - - def get(self, request, pk): - candidate_config = get_object_or_404(ConfigRevision, pk=pk) - - # Get the current ConfigRevision - config_version = get_config().version - current_config = ConfigRevision.objects.filter(pk=config_version).first() - - params = [] - for param in PARAMS: - params.append(( - param.name, - current_config.data.get(param.name, None), - candidate_config.data.get(param.name, None) - )) - - return render(request, 'extras/configrevision_restore.html', { - 'object': candidate_config, - 'params': params, - }) - - def post(self, request, pk): - if not request.user.has_perm('extras.configrevision_edit'): - return HttpResponseForbidden() - - candidate_config = get_object_or_404(ConfigRevision, pk=pk) - candidate_config.activate() - messages.success(request, f"Restored configuration revision #{pk}") - - return redirect(candidate_config.get_absolute_url()) - - # # Markdown # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 43cf3f869..7ad317324 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -424,13 +424,13 @@ ADMIN_MENU = Menu( MenuItem( link='core:config', link_text=_('Current Config'), - permissions=['extras.view_configrevision'], + permissions=['core.view_configrevision'], staff_only=True ), MenuItem( - link='extras:configrevision_list', + link='core:configrevision_list', link_text=_('Config Revisions'), - permissions=['extras.view_configrevision'], + permissions=['core.view_configrevision'], staff_only=True ), ), diff --git a/netbox/templates/extras/configrevision.html b/netbox/templates/core/configrevision.html similarity index 96% rename from netbox/templates/extras/configrevision.html rename to netbox/templates/core/configrevision.html index a880865c3..6481127fa 100644 --- a/netbox/templates/extras/configrevision.html +++ b/netbox/templates/core/configrevision.html @@ -14,11 +14,11 @@
{% plugin_buttons object %} - {% if not object.pk or object.is_active and perms.extras.add_configrevision %} - {% url 'extras:configrevision_add' as edit_url %} + {% if not object.pk or object.is_active and perms.core.add_configrevision %} + {% url 'core:configrevision_add' as edit_url %} {% include "buttons/edit.html" with url=edit_url %} {% endif %} - {% if object.pk and not object.is_active and perms.extras.delete_configrevision %} + {% if object.pk and not object.is_active and perms.core.delete_configrevision %} {% delete_button object %} {% endif %}
diff --git a/netbox/templates/extras/configrevision_restore.html b/netbox/templates/core/configrevision_restore.html similarity index 85% rename from netbox/templates/extras/configrevision_restore.html rename to netbox/templates/core/configrevision_restore.html index 134a0b547..ad6fb1bd9 100644 --- a/netbox/templates/extras/configrevision_restore.html +++ b/netbox/templates/core/configrevision_restore.html @@ -18,8 +18,8 @@ @@ -77,7 +77,7 @@
- {% trans "Cancel" %} + {% trans "Cancel" %}