Fixes 18208: Consolidate rendering configuration templates (#18604)

This commit is contained in:
Alexander Haase 2025-02-10 17:03:08 +01:00 committed by GitHub
parent e1d1aab4bd
commit 3e1cc0d7f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 67 additions and 169 deletions

View File

@ -4,17 +4,15 @@ from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction from django.db import transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import View from django.views.generic import View
from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import ASN, IPAddress, Prefix, VLANGroup from ipam.models import ASN, IPAddress, Prefix, VLANGroup
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
@ -2253,54 +2251,14 @@ class DeviceConfigContextView(ObjectConfigContextView):
@register_model_view(Device, 'render-config') @register_model_view(Device, 'render-config')
class DeviceRenderConfigView(generic.ObjectView): class DeviceRenderConfigView(ObjectRenderConfigView):
queryset = Device.objects.all() queryset = Device.objects.all()
template_name = 'dcim/device/render_config.html' base_template = 'dcim/device/base.html'
tab = ViewTab( tab = ViewTab(
label=_('Render Config'), label=_('Render Config'),
weight=2100 weight=2100,
) )
def get(self, request, **kwargs):
instance = self.get_object(**kwargs)
context = self.get_extra_context(request, instance)
# If a direct export has been requested, return the rendered template content as a
# downloadable file.
if request.GET.get('export'):
content = context['rendered_config'] or context['error_message']
response = HttpResponse(content, content_type='text')
filename = f"{instance.name or 'config'}.txt"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
return render(request, self.get_template_name(), {
'object': instance,
'tab': self.tab,
**context,
})
def get_extra_context(self, request, instance):
# Compile context data
context_data = instance.get_config_context()
context_data.update({'device': instance})
# Render the config template
rendered_config = None
error_message = None
if config_template := instance.get_config_template():
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
return {
'config_template': config_template,
'context_data': context_data,
'rendered_config': rendered_config,
'error_message': error_message,
}
@register_model_view(Device, 'virtual-machines') @register_model_view(Device, 'virtual-machines')
class DeviceVirtualMachinesView(generic.ObjectChildrenView): class DeviceVirtualMachinesView(generic.ObjectChildrenView):

View File

@ -10,6 +10,7 @@ from django.utils import timezone
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import View from django.views.generic import View
from jinja2.exceptions import TemplateError
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.forms import ManagedFileForm from core.forms import ManagedFileForm
@ -885,6 +886,61 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
queryset = ConfigTemplate.objects.all() queryset = ConfigTemplate.objects.all()
class ObjectRenderConfigView(generic.ObjectView):
base_template = None
template_name = 'extras/object_render_config.html'
def get(self, request, **kwargs):
instance = self.get_object(**kwargs)
context = self.get_extra_context(request, instance)
# If a direct export has been requested, return the rendered template content as a
# downloadable file.
if request.GET.get('export'):
content = context['rendered_config'] or context['error_message']
response = HttpResponse(content, content_type='text')
filename = f"{instance.name or 'config'}.txt"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
return render(
request,
self.get_template_name(),
{
'object': instance,
'tab': self.tab,
**context,
},
)
def get_extra_context_data(self, request, instance):
return {
f'{instance._meta.model_name}': instance,
}
def get_extra_context(self, request, instance):
# Compile context data
context_data = instance.get_config_context()
context_data.update(self.get_extra_context_data(request, instance))
# Render the config template
rendered_config = None
error_message = None
if config_template := instance.get_config_template():
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
return {
'base_template': self.base_template,
'config_template': config_template,
'context_data': context_data,
'rendered_config': rendered_config,
'error_message': error_message,
}
# #
# Image attachments # Image attachments
# #

View File

@ -1,4 +1,5 @@
{% extends 'dcim/device/base.html' %} {% extends base_template %}
{% load helpers %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
@ -67,7 +68,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="alert alert-info"> <div class="alert alert-info">
{% trans "No configuration template has been assigned for this device." %} {% trans "No configuration template has been assigned." %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,75 +0,0 @@
{% extends 'virtualization/virtualmachine/base.html' %}
{% load static %}
{% load i18n %}
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-5">
<div class="card">
<h2 class="card-header">{% trans "Config Template" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Config Template" %}</th>
<td>{{ config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Data Source" %}</th>
<td>{{ config_template.data_file.source|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Data File" %}</th>
<td>{{ config_template.data_file|linkify|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col-7">
<div class="card">
<div class="accordion accordion-flush" id="renderConfig">
<div class="card-body">
<div class="accordion-item">
<h2 class="accordion-header" id="renderConfigHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
{% trans "Context Data" %}
</button>
</h2>
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
<div class="accordion-body">
<pre class="card-body">{{ context_data|pprint }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
{% if config_template %}
{% if rendered_config %}
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Rendered Config" %}
<a href="?export=True" class="btn btn-primary lh-1" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
</h2>
<pre class="card-body">{{ rendered_config }}</pre>
</div>
{% else %}
<div class="alert alert-warning">
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
{% trans error_message %}
</div>
{% endif %}
{% else %}
<div class="alert alert-info">
{% trans "No configuration template has been assigned for this virtual machine." %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,17 +1,15 @@
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import Prefetch, Sum from django.db.models import Prefetch, Sum
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jinja2.exceptions import TemplateError
from dcim.filtersets import DeviceFilterSet from dcim.filtersets import DeviceFilterSet
from dcim.forms import DeviceFilterForm from dcim.forms import DeviceFilterForm
from dcim.models import Device from dcim.models import Device
from dcim.tables import DeviceTable from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import IPAddress from ipam.models import IPAddress
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
@ -427,54 +425,14 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
@register_model_view(VirtualMachine, 'render-config') @register_model_view(VirtualMachine, 'render-config')
class VirtualMachineRenderConfigView(generic.ObjectView): class VirtualMachineRenderConfigView(ObjectRenderConfigView):
queryset = VirtualMachine.objects.all() queryset = VirtualMachine.objects.all()
template_name = 'virtualization/virtualmachine/render_config.html' base_template = 'virtualization/virtualmachine/base.html'
tab = ViewTab( tab = ViewTab(
label=_('Render Config'), label=_('Render Config'),
weight=2100 weight=2100,
) )
def get(self, request, **kwargs):
instance = self.get_object(**kwargs)
context = self.get_extra_context(request, instance)
# If a direct export has been requested, return the rendered template content as a
# downloadable file.
if request.GET.get('export'):
content = context['rendered_config'] or context['error_message']
response = HttpResponse(content, content_type='text')
filename = f"{instance.name or 'config'}.txt"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
return render(request, self.get_template_name(), {
'object': instance,
'tab': self.tab,
**context,
})
def get_extra_context(self, request, instance):
# Compile context data
context_data = instance.get_config_context()
context_data.update({'virtualmachine': instance})
# Render the config template
rendered_config = None
error_message = None
if config_template := instance.get_config_template():
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
return {
'config_template': config_template,
'context_data': context_data,
'rendered_config': rendered_config,
'error_message': error_message,
}
@register_model_view(VirtualMachine, 'add', detail=False) @register_model_view(VirtualMachine, 'add', detail=False)
@register_model_view(VirtualMachine, 'edit') @register_model_view(VirtualMachine, 'edit')