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:
@ -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
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
34
netbox/templates/admin/extras/configrevision/restore.html
Normal file
34
netbox/templates/admin/extras/configrevision/restore.html
Normal 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 %}
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user