Sfoglia il codice sorgente

Add /api/virtualization/virtual-machines/{id}/render-config/ endpoint (#14287)

* Add /api/virtualization/virtual-machines/{id}/render-config/ endpoint

* Update Docstring "Device" -> "Virtual Machine"

Docstring should mention "..this Virtual Machine" instead of "...this Device", thanks @LuPo!

* Move config rendering logic to new RenderConfigMixin

* Add tests for render-config API endpoint

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Pavel Korovin 2 anni fa
parent
commit
e13bf48a35

+ 2 - 22
netbox/dcim/api/views.py

@@ -3,10 +3,8 @@ from django.shortcuts import get_object_or_404
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema, OpenApiParameter
 from rest_framework.decorators import action
-from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
 from rest_framework.routers import APIRootView
-from rest_framework.status import HTTP_400_BAD_REQUEST
 from rest_framework.viewsets import ViewSet
 
 from circuits.models import Circuit
@@ -14,12 +12,11 @@ from dcim import filtersets
 from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
 from dcim.models import *
 from dcim.svg import CableTraceSVG
-from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
+from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
 from ipam.models import Prefix, VLAN
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.pagination import StripCountAnnotationsPaginator
-from netbox.api.renderers import TextRenderer
 from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
 from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
 from netbox.constants import NESTED_SERIALIZER_PREFIX
@@ -390,7 +387,7 @@ class PlatformViewSet(NetBoxModelViewSet):
 class DeviceViewSet(
     SequentialBulkCreatesMixin,
     ConfigContextQuerySetMixin,
-    ConfigTemplateRenderMixin,
+    RenderConfigMixin,
     NetBoxModelViewSet
 ):
     queryset = Device.objects.prefetch_related(
@@ -420,23 +417,6 @@ class DeviceViewSet(
 
         return serializers.DeviceWithConfigContextSerializer
 
-    @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.
-        """
-        device = self.get_object()
-        configtemplate = device.get_config_template()
-        if not configtemplate:
-            return Response({'error': 'No config template found for this device.'}, status=HTTP_400_BAD_REQUEST)
-
-        # Compile context data
-        context_data = device.get_config_context()
-        context_data.update(request.data)
-        context_data.update({'device': device})
-
-        return self.render_configtemplate(request, configtemplate, context_data)
-
 
 class VirtualDeviceContextViewSet(NetBoxModelViewSet):
     queryset = VirtualDeviceContext.objects.prefetch_related(

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

@@ -6,6 +6,7 @@ from rest_framework import status
 from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
+from extras.models import ConfigTemplate
 from ipam.models import ASN, RIR, VLAN, VRF
 from netbox.api.serializers import GenericObjectSerializer
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
@@ -1265,6 +1266,22 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
 
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
 
+    def test_render_config(self):
+        configtemplate = ConfigTemplate.objects.create(
+            name='Config Template 1',
+            template_code='Config for device {{ device.name }}'
+        )
+
+        device = Device.objects.first()
+        device.config_template = configtemplate
+        device.save()
+
+        self.add_permissions('dcim.add_device')
+        url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/'
+        response = self.client.post(url, {}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data['content'], f'Config for device {device.name}')
+
 
 class ModuleTest(APIViewTestCases.APIViewTestCase):
     model = Module

+ 34 - 1
netbox/extras/api/mixins.py

@@ -1,10 +1,16 @@
 from jinja2.exceptions import TemplateError
+from rest_framework.decorators import action
+from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
+from rest_framework.status import HTTP_400_BAD_REQUEST
 
+from netbox.api.renderers import TextRenderer
 from .nested_serializers import NestedConfigTemplateSerializer
 
 __all__ = (
     'ConfigContextQuerySetMixin',
+    'ConfigTemplateRenderMixin',
+    'RenderConfigMixin',
 )
 
 
@@ -31,7 +37,9 @@ class ConfigContextQuerySetMixin:
 
 
 class ConfigTemplateRenderMixin:
-
+    """
+    Provides a method to return a rendered ConfigTemplate as REST API data.
+    """
     def render_configtemplate(self, request, configtemplate, context):
         try:
             output = configtemplate.render(context=context)
@@ -50,3 +58,28 @@ class ConfigTemplateRenderMixin:
             'configtemplate': template_serializer.data,
             'content': output
         })
+
+
+class RenderConfigMixin(ConfigTemplateRenderMixin):
+    """
+    Provides a /render-config/ endpoint for REST API views whose model may have a ConfigTemplate assigned.
+    """
+    @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.
+        """
+        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)
+
+        # Compile context data
+        context_data = instance.get_config_context()
+        context_data.update(request.data)
+        context_data.update({object_type: instance})
+
+        return self.render_configtemplate(request, configtemplate, context_data)

+ 3 - 3
netbox/virtualization/api/views.py

@@ -1,7 +1,7 @@
 from rest_framework.routers import APIRootView
 
 from dcim.models import Device
-from extras.api.mixins import ConfigContextQuerySetMixin
+from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
 from netbox.api.viewsets import NetBoxModelViewSet
 from utilities.query_functions import CollateAsChar
 from utilities.utils import count_related
@@ -53,9 +53,9 @@ class ClusterViewSet(NetBoxModelViewSet):
 # Virtual machines
 #
 
-class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
+class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
     queryset = VirtualMachine.objects.prefetch_related(
-        'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
+        'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'config_template', 'tags'
     )
     filterset_class = filtersets.VirtualMachineFilterSet
 

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

@@ -3,6 +3,7 @@ from rest_framework import status
 
 from dcim.choices import InterfaceModeChoices
 from dcim.models import Site
+from extras.models import ConfigTemplate
 from ipam.models import VLAN, VRF
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from virtualization.choices import *
@@ -228,6 +229,22 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
         response = self.client.post(url, data, format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
 
+    def test_render_config(self):
+        configtemplate = ConfigTemplate.objects.create(
+            name='Config Template 1',
+            template_code='Config for virtual machine {{ virtualmachine.name }}'
+        )
+
+        vm = VirtualMachine.objects.first()
+        vm.config_template = configtemplate
+        vm.save()
+
+        self.add_permissions('virtualization.add_virtualmachine')
+        url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/'
+        response = self.client.post(url, {}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data['content'], f'Config for virtual machine {vm.name}')
+
 
 class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
     model = VMInterface