Просмотр исходного кода

Extend admin UI to allow restoring previous config revisions

jeremystretch 4 лет назад
Родитель
Сommit
70f71e0f57

+ 60 - 1
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(
+            '<a href="{url}" class="button">Restore</a>',
+            url=reverse('admin:extras_configrevision_restore', args=(obj.pk,))
+        )
+    restore_link.short_description = "Actions"
+
+    # URLs
+
+    def get_urls(self):
+        urls = [
+            path('<int:pk>/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

+ 1 - 1
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.
         """

+ 1 - 1
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()

+ 4 - 2
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()
 

+ 34 - 0
netbox/templates/admin/extras/configrevision/restore.html

@@ -0,0 +1,34 @@
+{% extends "admin/base_site.html" %}
+
+{% block content %}
+  <p>Restore configuration #{{ object.pk }} from <strong>{{ object.created }}</strong>?</p>
+
+  <table>
+    <thead>
+      <tr>
+        <th>Parameter</th>
+        <th>Current Value</th>
+        <th>New Value</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for param, current, new in params %}
+        <tr{% if current != new %} style="color: #cc0000"{% endif %}>
+          <td>{{ param }}</td>
+          <td>{{ current }}</td>
+          <td>{{ new }}</td>
+        </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+
+  <form method="post">
+    {% csrf_token %}
+    <div class="submit-row" style="margin-top: 20px">
+      <input type="submit" name="restore" value="Restore" class="default" style="float: left" />
+      <a href="{% url 'admin:extras_configrevision_changelist' %}" style="float: left; margin: 2px 0; padding: 10px 15px">Cancel</a>
+    </div>
+  </form>
+{% endblock content %}
+
+