Merge pull request #22314 from netbox-community/21091-render-config-openapi

Fixes #21091: Correct OpenAPI schema for rendering config contexts
This commit is contained in:
bctiemann 2026-05-28 20:02:59 -04:00 committed by GitHub
commit 14c3c573e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 11 deletions

View File

@ -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):
"""

View File

@ -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.")
)

View File

@ -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')