Browse Source

#21782 - Enable optional config template selection on Device

Arthur 1 day ago
parent
commit
46396d7667

+ 26 - 0
netbox/dcim/tests/test_api.py

@@ -1633,6 +1633,32 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
         response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
         response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertHttpStatus(response, status.HTTP_200_OK)
 
 
+    def test_render_config_with_config_template_id(self):
+        default_template = ConfigTemplate.objects.create(
+            name='Default Template',
+            template_code='Default config for {{ device.name }}'
+        )
+        override_template = ConfigTemplate.objects.create(
+            name='Override Template',
+            template_code='Override config for {{ device.name }}'
+        )
+
+        device = Device.objects.first()
+        device.config_template = default_template
+        device.save()
+
+        self.add_permissions('dcim.render_config_device', 'dcim.view_device')
+        url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
+
+        # Render with override template
+        response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data['content'], f'Override config for {device.name}')
+
+        # Render with invalid config_template_id
+        response = self.client.post(url, {'config_template_id': 999999}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+
 
 
 class ModuleTest(APIViewTestCases.APIViewTestCase):
 class ModuleTest(APIViewTestCases.APIViewTestCase):
     model = Module
     model = Module

+ 26 - 0
netbox/dcim/tests/test_views.py

@@ -2362,6 +2362,32 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         self.remove_permissions('dcim.view_device')
         self.remove_permissions('dcim.view_device')
         self.assertHttpStatus(self.client.get(url), 403)
         self.assertHttpStatus(self.client.get(url), 403)
 
 
+    def test_device_renderconfig_with_config_template_id(self):
+        default_template = ConfigTemplate.objects.create(
+            name='Default Template',
+            template_code='Default config for {{ device.name }}'
+        )
+        override_template = ConfigTemplate.objects.create(
+            name='Override Template',
+            template_code='Override config for {{ device.name }}'
+        )
+        device = Device.objects.first()
+        device.config_template = default_template
+        device.save()
+
+        self.add_permissions('dcim.view_device', 'dcim.render_config_device')
+        url = reverse('dcim:device_render-config', kwargs={'pk': device.pk})
+
+        # Render with override config_template_id
+        response = self.client.get(url, {'config_template_id': override_template.pk})
+        self.assertHttpStatus(response, 200)
+        self.assertIn(b'Override config for', response.content)
+
+        # Render with invalid config_template_id still returns 200 with error message
+        response = self.client.get(url, {'config_template_id': 999999})
+        self.assertHttpStatus(response, 200)
+        self.assertIn(b'Error rendering template', response.content)
+
     def test_device_role_display_colored(self):
     def test_device_role_display_colored(self):
         parent_role = DeviceRole.objects.create(name='Parent Role', slug='parent-role', color='111111')
         parent_role = DeviceRole.objects.create(name='Parent Role', slug='parent-role', color='111111')
         child_role = DeviceRole.objects.create(name='Child Role', slug='child-role', parent=parent_role, color='aa00bb')
         child_role = DeviceRole.objects.create(name='Child Role', slug='child-role', parent=parent_role, color='aa00bb')

+ 17 - 6
netbox/extras/api/mixins.py

@@ -7,6 +7,7 @@ from rest_framework.status import HTTP_400_BAD_REQUEST
 from netbox.api.authentication import TokenWritePermission
 from netbox.api.authentication import TokenWritePermission
 from netbox.api.renderers import TextRenderer
 from netbox.api.renderers import TextRenderer
 
 
+from extras.models import ConfigTemplate
 from .serializers import ConfigTemplateSerializer
 from .serializers import ConfigTemplateSerializer
 
 
 __all__ = (
 __all__ = (
@@ -85,15 +86,25 @@ class RenderConfigMixin(ConfigTemplateRenderMixin):
         instance = self.get_object()
         instance = self.get_object()
 
 
         object_type = instance._meta.model_name
         object_type = instance._meta.model_name
-        configtemplate = instance.get_config_template()
-        if not configtemplate:
-            return Response({
-                'error': f'No config template found for this {object_type}.'
-            }, status=HTTP_400_BAD_REQUEST)
+
+        # Check for an optional config_template_id override in the request data
+        if config_template_id := request.data.get('config_template_id'):
+            try:
+                configtemplate = ConfigTemplate.objects.get(pk=config_template_id)
+            except ConfigTemplate.DoesNotExist:
+                return Response({
+                    'error': f'Config template with ID {config_template_id} not found.'
+                }, status=HTTP_400_BAD_REQUEST)
+        else:
+            configtemplate = instance.get_config_template()
+            if not configtemplate:
+                return Response({
+                    'error': f'No config template found for this {object_type}.'
+                }, status=HTTP_400_BAD_REQUEST)
 
 
         # Compile context data
         # Compile context data
         context_data = instance.get_config_context()
         context_data = instance.get_config_context()
-        context_data.update(request.data)
+        context_data.update({k: v for k, v in request.data.items() if k != 'config_template_id'})
         context_data.update({object_type: instance})
         context_data.update({object_type: instance})
 
 
         return self.render_configtemplate(request, configtemplate, context_data)
         return self.render_configtemplate(request, configtemplate, context_data)

+ 12 - 2
netbox/extras/views.py

@@ -1268,10 +1268,20 @@ class ObjectRenderConfigView(generic.ObjectView):
         context_data = instance.get_config_context()
         context_data = instance.get_config_context()
         context_data.update(self.get_extra_context_data(request, instance))
         context_data.update(self.get_extra_context_data(request, instance))
 
 
+        # Check for an optional config_template_id override in the query params
+        config_template = None
+        error_message = ''
+        if config_template_id := request.GET.get('config_template_id'):
+            try:
+                config_template = ConfigTemplate.objects.get(pk=config_template_id)
+            except (ConfigTemplate.DoesNotExist, ValueError):
+                error_message = _("Config template with ID {id} not found.").format(id=config_template_id)
+        else:
+            config_template = instance.get_config_template()
+
         # Render the config template
         # Render the config template
         rendered_config = None
         rendered_config = None
-        error_message = ''
-        if config_template := instance.get_config_template():
+        if config_template and not error_message:
             try:
             try:
                 rendered_config = config_template.render(context=context_data)
                 rendered_config = config_template.render(context=context_data)
             except TemplateError as e:
             except TemplateError as e:

+ 7 - 7
netbox/templates/extras/object_render_config.html

@@ -49,13 +49,18 @@
   </div>
   </div>
   <div class="row">
   <div class="row">
     <div class="col">
     <div class="col">
-      {% if config_template %}
+      {% if error_message %}
+        <div class="alert alert-warning">
+          <h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
+          {% trans error_message %}
+        </div>
+      {% elif config_template %}
         {% if rendered_config %}
         {% if rendered_config %}
           <div class="card">
           <div class="card">
             <h2 class="card-header d-flex justify-content-between">
             <h2 class="card-header d-flex justify-content-between">
               {% trans "Rendered Config" %}
               {% trans "Rendered Config" %}
               <div class="card-actions">
               <div class="card-actions">
-                <a href="?export=True" class="btn btn-sm btn-ghost-primary" role="button">
+                <a href="?export=True{% if request.GET.config_template_id %}&config_template_id={{ request.GET.config_template_id }}{% endif %}" class="btn btn-sm btn-ghost-primary" role="button">
                   <i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
                   <i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
                 </a>
                 </a>
                 {% copy_content "rendered_config" %}
                 {% copy_content "rendered_config" %}
@@ -63,11 +68,6 @@
             </h2>
             </h2>
             <pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>
             <pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>
           </div>
           </div>
-        {% elif error_message %}
-          <div class="alert alert-warning">
-            <h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
-            {% trans error_message %}
-          </div>
         {% else %}
         {% else %}
           <div class="alert alert-warning">
           <div class="alert alert-warning">
             <h4 class="alert-title mb-1">{% trans "Template output is empty" %}</h4>
             <h4 class="alert-title mb-1">{% trans "Template output is empty" %}</h4>

+ 28 - 0
netbox/virtualization/tests/test_api.py

@@ -343,6 +343,34 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
         response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
         response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertHttpStatus(response, status.HTTP_200_OK)
 
 
+    def test_render_config_with_config_template_id(self):
+        default_template = ConfigTemplate.objects.create(
+            name='Default Template',
+            template_code='Default config for {{ virtualmachine.name }}'
+        )
+        override_template = ConfigTemplate.objects.create(
+            name='Override Template',
+            template_code='Override config for {{ virtualmachine.name }}'
+        )
+
+        vm = VirtualMachine.objects.first()
+        vm.config_template = default_template
+        vm.save()
+
+        self.add_permissions(
+            'virtualization.render_config_virtualmachine', 'virtualization.view_virtualmachine'
+        )
+        url = reverse('virtualization-api:virtualmachine-render-config', kwargs={'pk': vm.pk})
+
+        # Render with override template
+        response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data['content'], f'Override config for {vm.name}')
+
+        # Render with invalid config_template_id
+        response = self.client.post(url, {'config_template_id': 999999}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+
 
 
 class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
 class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
     model = VMInterface
     model = VMInterface