Просмотр исходного кода

7503 do device validate-create in serial (#12222)

* 7503 do device validate-create in serial

* 7503 fix single instance

* 7503 atomic transaction

* 7503 fix return data for bulk operations

* 7503 add test

* Move sequential creation logic to a mixin

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
Arthur Hanson 2 лет назад
Родитель
Сommit
8b051ea2f3
3 измененных файлов с 69 добавлено и 6 удалено
  1. 9 4
      netbox/dcim/api/views.py
  2. 34 1
      netbox/dcim/tests/test_api.py
  3. 26 1
      netbox/netbox/api/viewsets/mixins.py

+ 9 - 4
netbox/dcim/api/views.py

@@ -1,12 +1,12 @@
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
-from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
 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.status import HTTP_400_BAD_REQUEST
 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,7 +14,6 @@ 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.nested_serializers import NestedConfigTemplateSerializer
 from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
 from ipam.models import Prefix, VLAN
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
@@ -22,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
@@ -386,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',

+ 34 - 1
netbox/dcim/tests/test_api.py

@@ -1115,7 +1115,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
 
         device_types = (
             DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
-            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
+            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', u_height=2),
         )
         DeviceType.objects.bulk_create(device_types)
 
@@ -1229,6 +1229,39 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
 
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
 
+    def test_rack_fit(self):
+        """
+        Check that creating multiple devices with overlapping position fails.
+        """
+        device = Device.objects.first()
+        device_type = DeviceType.objects.all()[1]
+        data = [
+            {
+                'device_type': device_type.pk,
+                'device_role': device.device_role.pk,
+                'site': device.site.pk,
+                'name': 'Test Device 7',
+                'rack': device.rack.pk,
+                'face': 'front',
+                'position': 1
+            },
+            {
+                'device_type': device_type.pk,
+                'device_role': device.device_role.pk,
+                'site': device.site.pk,
+                'name': 'Test Device 8',
+                'rack': device.rack.pk,
+                'face': 'front',
+                'position': 2
+            }
+        ]
+
+        self.add_permissions('dcim.add_device')
+        url = reverse('dcim-api:device-list')
+        response = self.client.post(url, data, format='json', **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+
 
 class ModuleTest(APIViewTestCases.APIViewTestCase):
     model = Module

+ 26 - 1
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