1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

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>
This commit is contained in:
Pavel Korovin
2023-11-17 16:32:58 +03:00
committed by GitHub
parent e767fec5cc
commit e13bf48a35
5 changed files with 73 additions and 26 deletions

View File

@ -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(

View File

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

View File

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

View File

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

View File

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