Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
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
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
Expand Down Expand Up @@ -389,7 +386,7 @@ class PlatformViewSet(NetBoxModelViewSet):
class DeviceViewSet(
SequentialBulkCreatesMixin,
ConfigContextQuerySetMixin,
ConfigTemplateRenderMixin,
RenderConfigMixin,
NetBoxModelViewSet
):
queryset = Device.objects.prefetch_related(
Expand Down Expand Up @@ -419,23 +416,6 @@ def get_serializer_class(self):

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(
Expand Down
17 changes: 17 additions & 0 deletions netbox/dcim/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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
Expand Down Expand Up @@ -1265,6 +1266,22 @@ def test_rack_fit(self):

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
Expand Down
35 changes: 34 additions & 1 deletion netbox/extras/api/mixins.py
Original file line number Diff line number Diff line change
@@ -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',
)


Expand All @@ -31,7 +37,9 @@ def get_queryset(self):


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)
Expand All @@ -50,3 +58,28 @@ def render_configtemplate(self, request, configtemplate, context):
'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)
6 changes: 3 additions & 3 deletions netbox/virtualization/api/views.py
Original file line number Diff line number Diff line change
@@ -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.utils import count_related
from virtualization import filtersets
Expand Down Expand Up @@ -52,9 +52,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

Expand Down
17 changes: 17 additions & 0 deletions netbox/virtualization/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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 *
Expand Down Expand Up @@ -228,6 +229,22 @@ def test_unique_name_per_cluster_constraint(self):
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
Expand Down