From 70f71e0f57d3c40b50ec33a91ff9cc30064ca704 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 27 Oct 2021 14:05:49 -0400 Subject: [PATCH] Extend admin UI to allow restoring previous config revisions --- netbox/extras/admin.py | 61 ++++++++++++++++++- netbox/extras/models/models.py | 2 +- netbox/extras/signals.py | 2 +- netbox/netbox/config/__init__.py | 6 +- .../admin/extras/configrevision/restore.html | 34 +++++++++++ 5 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 netbox/templates/admin/extras/configrevision/restore.html diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 85bcd351d..752c8c83d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -1,5 +1,10 @@ from django.contrib import admin +from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse +from django.urls import path, reverse +from django.utils.html import format_html +from netbox.config import get_config, PARAMS from .forms import ConfigRevisionForm from .models import ConfigRevision, JobResult @@ -33,7 +38,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin): }) ] form = ConfigRevisionForm - list_display = ('id', 'is_active', 'created', 'comment') + list_display = ('id', 'is_active', 'created', 'comment', 'restore_link') ordering = ('-id',) readonly_fields = ('data',) @@ -47,6 +52,8 @@ class ConfigRevisionAdmin(admin.ModelAdmin): return initial + # Permissions + def has_add_permission(self, request): # Only superusers may modify the configuration. return request.user.is_superuser @@ -61,6 +68,58 @@ class ConfigRevisionAdmin(admin.ModelAdmin): obj is None or not obj.is_active() ) + # List display methods + + def restore_link(self, obj): + if obj.is_active(): + return '' + return format_html( + 'Restore', + url=reverse('admin:extras_configrevision_restore', args=(obj.pk,)) + ) + restore_link.short_description = "Actions" + + # URLs + + def get_urls(self): + urls = [ + path('/restore/', self.admin_site.admin_view(self.restore), name='extras_configrevision_restore'), + ] + + return urls + super().get_urls() + + # Views + + def restore(self, request, pk): + # Get the ConfigRevision being restored + candidate_config = get_object_or_404(ConfigRevision, pk=pk) + + if request.method == 'POST': + candidate_config.activate() + self.message_user(request, f"Restored configuration revision #{pk}") + + return redirect(reverse('admin:extras_configrevision_changelist')) + + # 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) + )) + + context = self.admin_site.each_context(request) + context.update({ + 'object': candidate_config, + 'params': params, + }) + + return TemplateResponse(request, 'admin/extras/configrevision/restore.html', context) + # # Reports & scripts diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index d4d6d648e..57615c0c5 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -560,7 +560,7 @@ class ConfigRevision(models.Model): return self.data[item] return super().__getattribute__(item) - def cache(self): + def activate(self): """ Cache the configuration data. """ diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index ce1dead8f..9b37dd763 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -172,4 +172,4 @@ def update_config(sender, instance, **kwargs): """ Update the cached NetBox configuration when a new ConfigRevision is created. """ - instance.cache() + instance.activate() diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index 7c353fdb9..9e015a891 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -48,7 +48,6 @@ class Config: if not self.config or not self.version: self._populate_from_db() self.defaults = {param.name: param.default for param in PARAMS} - logger.debug("Loaded configuration data from cache") def __getattr__(self, item): @@ -70,6 +69,8 @@ class Config: """Populate config data from Redis cache""" self.config = cache.get('config') or {} self.version = cache.get('config_version') + if self.config: + logger.debug("Loaded configuration data from cache") def _populate_from_db(self): """Cache data from latest ConfigRevision, then populate from cache""" @@ -77,6 +78,7 @@ class Config: try: revision = ConfigRevision.objects.last() + logger.debug("Loaded configuration data from database") except DatabaseError: # The database may not be available yet (e.g. when running a management command) logger.warning(f"Skipping config initialization (database unavailable)") @@ -86,7 +88,7 @@ class Config: logger.debug("No previous configuration found in database; proceeding with default values") return - revision.cache() + revision.activate() logger.debug("Filled cache with data from latest ConfigRevision") self._populate_from_cache() diff --git a/netbox/templates/admin/extras/configrevision/restore.html b/netbox/templates/admin/extras/configrevision/restore.html new file mode 100644 index 000000000..15531de2d --- /dev/null +++ b/netbox/templates/admin/extras/configrevision/restore.html @@ -0,0 +1,34 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +

Restore configuration #{{ object.pk }} from {{ object.created }}?

+ + + + + + + + + + + {% for param, current, new in params %} + + + + + + {% endfor %} + +
ParameterCurrent ValueNew Value
{{ param }}{{ current }}{{ new }}
+ +
+ {% csrf_token %} +
+ + Cancel +
+
+{% endblock content %} + +