1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Extend admin UI to allow restoring previous config revisions

This commit is contained in:
jeremystretch
2021-10-27 14:05:49 -04:00
parent 77bd26d17f
commit 70f71e0f57
5 changed files with 100 additions and 5 deletions

View File

@ -1,5 +1,10 @@
from django.contrib import admin 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 .forms import ConfigRevisionForm
from .models import ConfigRevision, JobResult from .models import ConfigRevision, JobResult
@ -33,7 +38,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
}) })
] ]
form = ConfigRevisionForm form = ConfigRevisionForm
list_display = ('id', 'is_active', 'created', 'comment') list_display = ('id', 'is_active', 'created', 'comment', 'restore_link')
ordering = ('-id',) ordering = ('-id',)
readonly_fields = ('data',) readonly_fields = ('data',)
@ -47,6 +52,8 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
return initial return initial
# Permissions
def has_add_permission(self, request): def has_add_permission(self, request):
# Only superusers may modify the configuration. # Only superusers may modify the configuration.
return request.user.is_superuser return request.user.is_superuser
@ -61,6 +68,58 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
obj is None or not obj.is_active() 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 # Reports & scripts

View File

@ -560,7 +560,7 @@ class ConfigRevision(models.Model):
return self.data[item] return self.data[item]
return super().__getattribute__(item) return super().__getattribute__(item)
def cache(self): def activate(self):
""" """
Cache the configuration data. Cache the configuration data.
""" """

View File

@ -172,4 +172,4 @@ def update_config(sender, instance, **kwargs):
""" """
Update the cached NetBox configuration when a new ConfigRevision is created. Update the cached NetBox configuration when a new ConfigRevision is created.
""" """
instance.cache() instance.activate()

View File

@ -48,7 +48,6 @@ class Config:
if not self.config or not self.version: if not self.config or not self.version:
self._populate_from_db() self._populate_from_db()
self.defaults = {param.name: param.default for param in PARAMS} self.defaults = {param.name: param.default for param in PARAMS}
logger.debug("Loaded configuration data from cache")
def __getattr__(self, item): def __getattr__(self, item):
@ -70,6 +69,8 @@ class Config:
"""Populate config data from Redis cache""" """Populate config data from Redis cache"""
self.config = cache.get('config') or {} self.config = cache.get('config') or {}
self.version = cache.get('config_version') self.version = cache.get('config_version')
if self.config:
logger.debug("Loaded configuration data from cache")
def _populate_from_db(self): def _populate_from_db(self):
"""Cache data from latest ConfigRevision, then populate from cache""" """Cache data from latest ConfigRevision, then populate from cache"""
@ -77,6 +78,7 @@ class Config:
try: try:
revision = ConfigRevision.objects.last() revision = ConfigRevision.objects.last()
logger.debug("Loaded configuration data from database")
except DatabaseError: except DatabaseError:
# The database may not be available yet (e.g. when running a management command) # The database may not be available yet (e.g. when running a management command)
logger.warning(f"Skipping config initialization (database unavailable)") 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") logger.debug("No previous configuration found in database; proceeding with default values")
return return
revision.cache() revision.activate()
logger.debug("Filled cache with data from latest ConfigRevision") logger.debug("Filled cache with data from latest ConfigRevision")
self._populate_from_cache() self._populate_from_cache()

View File

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