Quellcode durchsuchen

Fixes #21091: Correct OpenAPI schema for rendering config contexts

Jeremy Stretch vor 1 Monat
Ursprung
Commit
004178a299

+ 28 - 8
netbox/extras/api/mixins.py

@@ -1,4 +1,5 @@
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import OpenApiResponse, OpenApiTypes, extend_schema
 from rest_framework.decorators import action
 from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
@@ -8,7 +9,7 @@ from extras.models import ConfigTemplate
 from netbox.api.authentication import TokenWritePermission
 from netbox.api.renderers import TextRenderer
 
-from .serializers import ConfigTemplateSerializer
+from .serializers import RenderConfigInputSerializer, RenderedConfigSerializer
 
 __all__ = (
     'ConfigContextQuerySetMixin',
@@ -56,12 +57,11 @@ class ConfigTemplateRenderMixin:
         if request.accepted_renderer.format == 'txt':
             return Response(output)
 
-        template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
-
-        return Response({
-            'configtemplate': template_serializer.data,
-            'content': output
-        })
+        serializer = RenderedConfigSerializer(
+            instance={'configtemplate': configtemplate, 'content': output},
+            context={'request': request},
+        )
+        return Response(serializer.data)
 
 
 class RenderConfigMixin(ConfigTemplateRenderMixin):
@@ -75,6 +75,26 @@ class RenderConfigMixin(ConfigTemplateRenderMixin):
             return [TokenWritePermission()]
         return super().get_permissions()
 
+    @extend_schema(
+        request=RenderConfigInputSerializer,
+        responses={
+            200: OpenApiResponse(
+                response=RenderedConfigSerializer,
+                description=_(
+                    "The rendered config template. When the client requests `text/plain`, the raw "
+                    "rendered content is returned in place of the JSON object."
+                ),
+            ),
+            400: OpenApiResponse(
+                response=OpenApiTypes.OBJECT,
+                description=_("No config template could be resolved for this object."),
+            ),
+            500: OpenApiResponse(
+                response=OpenApiTypes.OBJECT,
+                description=_("An error occurred while rendering the config template."),
+            ),
+        },
+    )
     @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
     def render_config(self, request, pk):
         """

+ 44 - 0
netbox/extras/api/serializers_/configtemplates.py

@@ -1,3 +1,7 @@
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.extensions import OpenApiSerializerExtension
+from rest_framework import serializers
+
 from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
 from extras.models import ConfigTemplate
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
@@ -6,6 +10,8 @@ from users.api.serializers_.mixins import OwnerMixin
 
 __all__ = (
     'ConfigTemplateSerializer',
+    'RenderConfigInputSerializer',
+    'RenderedConfigSerializer',
 )
 
 
@@ -32,3 +38,41 @@ class ConfigTemplateSerializer(
             'data_file', 'auto_sync_enabled', 'data_synced', 'owner', 'tags', 'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class RenderConfigInputSerializer(serializers.Serializer):
+    """
+    Describes the request body for the device/VM /render-config/ endpoints. Any additional keys
+    supplied are passed through as context variables to the rendered template.
+    """
+    config_template_id = serializers.IntegerField(
+        required=False,
+        help_text=_(
+            "Optional ID of the ConfigTemplate to render. If omitted, the object's assigned "
+            "config template is used."
+        )
+    )
+
+
+class RenderConfigInputSerializerExtension(OpenApiSerializerExtension):
+    """
+    Augment the schema for `RenderConfigInputSerializer` to advertise that additional keys
+    (beyond `config_template_id`) are accepted and forwarded as template context variables.
+    """
+    target_class = 'extras.api.serializers_.configtemplates.RenderConfigInputSerializer'
+
+    def map_serializer(self, auto_schema, direction):
+        schema = auto_schema._map_serializer(self.target, direction, bypass_extensions=True)
+        schema['additionalProperties'] = True
+        return schema
+
+
+class RenderedConfigSerializer(serializers.Serializer):
+    """
+    Describes the JSON response returned by the /render-config/ and /render/ endpoints.
+    """
+    configtemplate = ConfigTemplateSerializer(read_only=True, nested=True)
+    content = serializers.CharField(
+        read_only=True,
+        help_text=_("The rendered template output.")
+    )

+ 21 - 3
netbox/extras/api/views.py

@@ -1,6 +1,7 @@
 from django.http import Http404
 from django.shortcuts import get_object_or_404
-from drf_spectacular.utils import extend_schema, extend_schema_view
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import OpenApiResponse, OpenApiTypes, extend_schema, extend_schema_view
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
@@ -245,11 +246,28 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
             return [TokenWritePermission()]
         return super().get_permissions()
 
+    @extend_schema(
+        request=OpenApiTypes.OBJECT,
+        responses={
+            200: OpenApiResponse(
+                response=serializers.RenderedConfigSerializer,
+                description=_(
+                    "The rendered config template. When the client requests `text/plain`, the raw "
+                    "rendered content is returned in place of the JSON object."
+                ),
+            ),
+            500: OpenApiResponse(
+                response=OpenApiTypes.OBJECT,
+                description=_("An error occurred while rendering the config template."),
+            ),
+        },
+    )
     @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
     def render(self, request, pk):
         """
-        Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
-        return the raw rendered content, rather than serialized JSON.
+        Render a ConfigTemplate using the context data provided (if any). The request body should be a
+        mapping of context variables to make available to the template. If the client requests "text/plain"
+        data, return the raw rendered content, rather than serialized JSON.
         """
         # Override restrict() on the default queryset to enforce the render & view actions
         self.queryset = self.queryset.model.objects.restrict(request.user, 'render').restrict(request.user, 'view')