From 46396d7667403b322cf3fa96478a237ac1fc2060 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Apr 2026 15:41:34 -0700 Subject: [PATCH 1/8] #21782 - Enable optional config template selection on Device --- netbox/dcim/tests/test_api.py | 26 +++++++++++++++++ netbox/dcim/tests/test_views.py | 26 +++++++++++++++++ netbox/extras/api/mixins.py | 23 +++++++++++---- netbox/extras/views.py | 14 ++++++++-- .../extras/object_render_config.html | 14 +++++----- netbox/virtualization/tests/test_api.py | 28 +++++++++++++++++++ 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 61928e562..f2e9406bf 100644 --- a/netbox/dcim/tests/test_api.py +++ b/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) 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): model = Module diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a67cea127..56fb15fc4 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2362,6 +2362,32 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.remove_permissions('dcim.view_device') 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): 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') diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index a98218b78..506d192bd 100644 --- a/netbox/extras/api/mixins.py +++ b/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.renderers import TextRenderer +from extras.models import ConfigTemplate from .serializers import ConfigTemplateSerializer __all__ = ( @@ -85,15 +86,25 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): instance = self.get_object() 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 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}) return self.render_configtemplate(request, configtemplate, context_data) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 99e8d976a..8df93e50f 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1268,10 +1268,20 @@ class ObjectRenderConfigView(generic.ObjectView): context_data = instance.get_config_context() 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 rendered_config = None - error_message = '' - if config_template := instance.get_config_template(): + if config_template and not error_message: try: rendered_config = config_template.render(context=context_data) except TemplateError as e: diff --git a/netbox/templates/extras/object_render_config.html b/netbox/templates/extras/object_render_config.html index aec25cd64..e7bd2fd9b 100644 --- a/netbox/templates/extras/object_render_config.html +++ b/netbox/templates/extras/object_render_config.html @@ -49,13 +49,18 @@
- {% if config_template %} + {% if error_message %} +
+

{% trans "Error rendering template" %}

+ {% trans error_message %} +
+ {% elif config_template %} {% if rendered_config %}

{% trans "Rendered Config" %}
- + {% trans "Download" %} {% copy_content "rendered_config" %} @@ -63,11 +68,6 @@

{{ rendered_config }}
- {% elif error_message %} -
-

{% trans "Error rendering template" %}

- {% trans error_message %} -
{% else %}

{% trans "Template output is empty" %}

diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 453298025..bf5e93a76 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/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) 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): model = VMInterface From 2fde9db66ec8e069b76bf778d7e55e604fc89468 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Apr 2026 15:41:42 -0700 Subject: [PATCH 2/8] #21782 - Enable optional config template selection on Device --- netbox/extras/api/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 506d192bd..9080e33b7 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -4,10 +4,10 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST +from extras.models import ConfigTemplate from netbox.api.authentication import TokenWritePermission from netbox.api.renderers import TextRenderer -from extras.models import ConfigTemplate from .serializers import ConfigTemplateSerializer __all__ = ( From 7f14434162675ea019e4d8b09b81531c4ebaba2f Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 09:10:45 -0700 Subject: [PATCH 3/8] cleanup --- netbox/extras/api/mixins.py | 4 ++-- netbox/extras/views.py | 2 +- netbox/templates/extras/object_render_config.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 9080e33b7..7f8c4a865 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -90,8 +90,8 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): # 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: + configtemplate = ConfigTemplate.objects.restrict(request.user, 'view').get(pk=config_template_id) + except (ConfigTemplate.DoesNotExist, ValueError): return Response({ 'error': f'Config template with ID {config_template_id} not found.' }, status=HTTP_400_BAD_REQUEST) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 8df93e50f..bdce5c5c1 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1273,7 +1273,7 @@ class ObjectRenderConfigView(generic.ObjectView): error_message = '' if config_template_id := request.GET.get('config_template_id'): try: - config_template = ConfigTemplate.objects.get(pk=config_template_id) + config_template = ConfigTemplate.objects.restrict(request.user, 'view').get(pk=config_template_id) except (ConfigTemplate.DoesNotExist, ValueError): error_message = _("Config template with ID {id} not found.").format(id=config_template_id) else: diff --git a/netbox/templates/extras/object_render_config.html b/netbox/templates/extras/object_render_config.html index e7bd2fd9b..6fa098da1 100644 --- a/netbox/templates/extras/object_render_config.html +++ b/netbox/templates/extras/object_render_config.html @@ -52,7 +52,7 @@ {% if error_message %}

{% trans "Error rendering template" %}

- {% trans error_message %} + {{ error_message }}
{% elif config_template %} {% if rendered_config %} From a2845d190e2fe59967e3a4c1e2037dbaeccbf2fd Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 09:20:47 -0700 Subject: [PATCH 4/8] cleanup --- netbox/dcim/tests/test_api.py | 13 ++++++-- netbox/dcim/tests/test_views.py | 15 +++++++-- netbox/virtualization/tests/test_api.py | 14 ++++++-- netbox/virtualization/tests/test_views.py | 40 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index f2e9406bf..357d13302 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1647,7 +1647,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): device.config_template = default_template device.save() - self.add_permissions('dcim.render_config_device', 'dcim.view_device') + self.add_permissions('dcim.render_config_device', 'dcim.view_device', 'extras.view_configtemplate') url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk}) # Render with override template @@ -1655,10 +1655,19 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['content'], f'Override config for {device.name}') - # Render with invalid config_template_id + # Render with nonexistent config_template_id response = self.client.post(url, {'config_template_id': 999999}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + # Render with non-integer config_template_id + response = self.client.post(url, {'config_template_id': 'abc'}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + + # Without view_configtemplate permission, override template should not be accessible + self.remove_permissions('extras.view_configtemplate') + response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + class ModuleTest(APIViewTestCases.APIViewTestCase): model = Module diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 370e92738..86cb3db1a 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2375,7 +2375,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): device.config_template = default_template device.save() - self.add_permissions('dcim.view_device', 'dcim.render_config_device') + self.add_permissions('dcim.view_device', 'dcim.render_config_device', 'extras.view_configtemplate') url = reverse('dcim:device_render-config', kwargs={'pk': device.pk}) # Render with override config_template_id @@ -2383,11 +2383,22 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.assertHttpStatus(response, 200) self.assertIn(b'Override config for', response.content) - # Render with invalid config_template_id still returns 200 with error message + # Render with nonexistent 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) + # Render with non-integer config_template_id still returns 200 with error message + response = self.client.get(url, {'config_template_id': 'abc'}) + self.assertHttpStatus(response, 200) + self.assertIn(b'Error rendering template', response.content) + + # Without view_configtemplate permission, override template should not be accessible + self.remove_permissions('extras.view_configtemplate') + response = self.client.get(url, {'config_template_id': override_template.pk}) + self.assertHttpStatus(response, 200) + self.assertIn(b'Error rendering template', response.content) + def test_device_role_display_colored(self): 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') diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index bf5e93a76..97e179de3 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -358,7 +358,8 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase): vm.save() self.add_permissions( - 'virtualization.render_config_virtualmachine', 'virtualization.view_virtualmachine' + 'virtualization.render_config_virtualmachine', 'virtualization.view_virtualmachine', + 'extras.view_configtemplate' ) url = reverse('virtualization-api:virtualmachine-render-config', kwargs={'pk': vm.pk}) @@ -367,10 +368,19 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['content'], f'Override config for {vm.name}') - # Render with invalid config_template_id + # Render with nonexistent config_template_id response = self.client.post(url, {'config_template_id': 999999}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + # Render with non-integer config_template_id + response = self.client.post(url, {'config_template_id': 'abc'}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + + # Without view_configtemplate permission, override template should not be accessible + self.remove_permissions('extras.view_configtemplate') + response = self.client.post(url, {'config_template_id': override_template.pk}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + class VMInterfaceTest(APIViewTestCases.APIViewTestCase): model = VMInterface diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 12a4eca4b..435e9579e 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -357,6 +357,46 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.remove_permissions('virtualization.view_virtualmachine') self.assertHttpStatus(self.client.get(url), 403) + def test_virtualmachine_renderconfig_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.view_virtualmachine', 'virtualization.render_config_virtualmachine', + 'extras.view_configtemplate' + ) + url = reverse('virtualization:virtualmachine_render-config', kwargs={'pk': vm.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 nonexistent 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) + + # Render with non-integer config_template_id still returns 200 with error message + response = self.client.get(url, {'config_template_id': 'abc'}) + self.assertHttpStatus(response, 200) + self.assertIn(b'Error rendering template', response.content) + + # Without view_configtemplate permission, override template should not be accessible + self.remove_permissions('extras.view_configtemplate') + response = self.client.get(url, {'config_template_id': override_template.pk}) + self.assertHttpStatus(response, 200) + self.assertIn(b'Error rendering template', response.content) + class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): model = VMInterface From 86e29cd3f6c58711f3da4b07748b9c705a34a49f Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 09:27:15 -0700 Subject: [PATCH 5/8] cleanup --- netbox/extras/api/mixins.py | 2 +- netbox/extras/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 7f8c4a865..a45c79bca 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -77,7 +77,7 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer]) def render_config(self, request, pk): """ - Resolve and render the preferred ConfigTemplate for this Device. + Resolve and render the preferred ConfigTemplate for this Device or Virtual Machine. """ # Override restrict() on the default queryset to enforce the render_config & view actions self.queryset = self.queryset.model.objects.restrict(request.user, 'render_config').restrict( diff --git a/netbox/extras/views.py b/netbox/extras/views.py index bdce5c5c1..08b7d3ecf 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1281,7 +1281,7 @@ class ObjectRenderConfigView(generic.ObjectView): # Render the config template rendered_config = None - if config_template and not error_message: + if config_template: try: rendered_config = config_template.render(context=context_data) except TemplateError as e: From 3a30dc5dbc4b656c3740216cd7b1a9b86d65d826 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 11:02:42 -0700 Subject: [PATCH 6/8] internationalize strings --- netbox/extras/api/mixins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index a45c79bca..adaf4d55e 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext as _ from jinja2.exceptions import TemplateError from rest_framework.decorators import action from rest_framework.renderers import JSONRenderer @@ -93,13 +94,13 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): configtemplate = ConfigTemplate.objects.restrict(request.user, 'view').get(pk=config_template_id) except (ConfigTemplate.DoesNotExist, ValueError): return Response({ - 'error': f'Config template with ID {config_template_id} not found.' + 'error': _('Config template with ID {id} not found.').format(id=config_template_id) }, 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}.' + 'error': _('No config template found for this {object_type}.').format(object_type=object_type) }, status=HTTP_400_BAD_REQUEST) # Compile context data From bbd2796c17b45154e02bc1459f4417ef6bd02894 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 11:29:59 -0700 Subject: [PATCH 7/8] documentation --- docs/features/configuration-rendering.md | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md index 7cf197f54..b12d6459f 100644 --- a/docs/features/configuration-rendering.md +++ b/docs/features/configuration-rendering.md @@ -75,6 +75,33 @@ The configuration can be rendered as JSON or as plaintext by setting the `Accept * `Accept: application/json` * `Accept: text/plain` +### Overriding the Config Template + +To render a specific config template against a device's context data - rather than the template resolved via the fallback chain above — include `config_template_id` in the request body: + +```no-highlight +curl -X POST \ +-H "Authorization: Token $TOKEN" \ +-H "Content-Type: application/json" \ +-H "Accept: application/json; indent=4" \ +http://netbox:8000/api/dcim/devices/123/render-config/ \ +--data '{ + "config_template_id": 42 +}' +``` + +This is useful for rendering partial or alternative templates against a device's assembled context without changing any stored assignments. Any additional keys in the request body are passed into the template as context variables alongside the device's own config context data, as with standard rendering: + +```no-highlight +--data '{ + "config_template_id": 42, + "environment": "staging" +}' +``` + +!!! note "Permissions" + Overriding the config template requires the requesting user to have `view` permission for the "Extras > Config Template" object type in addition to the `render_config` permission on the device. + ### General Purpose Use NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template. From 5c1d1d6001a24854ffb6d0655cac0e4cfc76acc8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Apr 2026 11:49:54 -0700 Subject: [PATCH 8/8] documentation --- docs/features/configuration-rendering.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md index b12d6459f..2de0778f7 100644 --- a/docs/features/configuration-rendering.md +++ b/docs/features/configuration-rendering.md @@ -102,6 +102,12 @@ This is useful for rendering partial or alternative templates against a device's !!! note "Permissions" Overriding the config template requires the requesting user to have `view` permission for the "Extras > Config Template" object type in addition to the `render_config` permission on the device. +The same override is available in the UI by appending `config_template_id` as a query parameter to the device's render config URL: + +```no-highlight +/dcim/devices/123/render-config/?config_template_id=42 +``` + ### General Purpose Use NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template.