diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 5d87d26a9..5b87c4e5d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,9 +1,7 @@ -from django.db import transaction from django.http import Http404, HttpResponse 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 import status from rest_framework.decorators import action from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -23,6 +21,7 @@ from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import StripCountAnnotationsPaginator from netbox.api.renderers import TextRenderer from netbox.api.viewsets import NetBoxModelViewSet +from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from utilities.utils import count_related @@ -387,7 +386,12 @@ class PlatformViewSet(NetBoxModelViewSet): # Devices/modules # -class DeviceViewSet(ConfigContextQuerySetMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet): +class DeviceViewSet( + SequentialBulkCreatesMixin, + ConfigContextQuerySetMixin, + ConfigTemplateRenderMixin, + NetBoxModelViewSet +): queryset = Device.objects.prefetch_related( 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags', @@ -415,25 +419,6 @@ class DeviceViewSet(ConfigContextQuerySetMixin, ConfigTemplateRenderMixin, NetBo return serializers.DeviceWithConfigContextSerializer - @transaction.atomic - def create(self, request, *args, **kwargs): - # do validate / create for each item in serial instead of validating all data at once - if is_bulk := isinstance(request.data, list): - return_data = [] - data_list = request.data if is_bulk else [request.data, ] - - for data in data_list: - serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - if is_bulk: - return_data.append(serializer.data) - else: - return_data = serializer.data - - return Response(return_data, status=status.HTTP_201_CREATED, headers=headers) - @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer]) def render_config(self, request, pk): """ diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 8b629bbc6..fde486fe9 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -15,11 +15,12 @@ from utilities.api import get_serializer_for_model __all__ = ( 'BriefModeMixin', + 'BulkDestroyModelMixin', 'BulkUpdateModelMixin', 'CustomFieldsMixin', 'ExportTemplatesMixin', - 'BulkDestroyModelMixin', 'ObjectValidationMixin', + 'SequentialBulkCreatesMixin', ) @@ -94,6 +95,30 @@ class ExportTemplatesMixin: return super().list(request, *args, **kwargs) +class SequentialBulkCreatesMixin: + """ + Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation + which depends on the evaluation of existing objects (such as checking for free space within a rack) functions + appropriately. + """ + @transaction.atomic + def create(self, request, *args, **kwargs): + if not isinstance(request.data, list): + # Creating a single object + return super().create(request, *args, **kwargs) + + return_data = [] + for data in request.data: + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return_data.append(serializer.data) + + headers = self.get_success_headers(serializer.data) + + return Response(return_data, status=status.HTTP_201_CREATED, headers=headers) + + class BulkUpdateModelMixin: """ Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one