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

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.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 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.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema, OpenApiParameter
 from rest_framework.decorators import action
 from rest_framework.decorators import action
 from rest_framework.renderers import JSONRenderer
 from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
 from rest_framework.response import Response
-from rest_framework.status import HTTP_400_BAD_REQUEST
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
+from rest_framework.status import HTTP_400_BAD_REQUEST
 from rest_framework.viewsets import ViewSet
 from rest_framework.viewsets import ViewSet
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
@@ -14,7 +14,6 @@ from dcim import filtersets
 from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
 from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
 from dcim.models import *
 from dcim.models import *
 from dcim.svg import CableTraceSVG
 from dcim.svg import CableTraceSVG
-from extras.api.nested_serializers import NestedConfigTemplateSerializer
 from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
 from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
 from ipam.models import Prefix, VLAN
 from ipam.models import Prefix, VLAN
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 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.pagination import StripCountAnnotationsPaginator
 from netbox.api.renderers import TextRenderer
 from netbox.api.renderers import TextRenderer
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
+from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from utilities.utils import count_related
 from utilities.utils import count_related
@@ -386,7 +386,12 @@ class PlatformViewSet(NetBoxModelViewSet):
 # Devices/modules
 # Devices/modules
 #
 #
 
 
-class DeviceViewSet(ConfigContextQuerySetMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
+class DeviceViewSet(
+    SequentialBulkCreatesMixin,
+    ConfigContextQuerySetMixin,
+    ConfigTemplateRenderMixin,
+    NetBoxModelViewSet
+):
     queryset = Device.objects.prefetch_related(
     queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
         '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',
         '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 = (
         device_types = (
             DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
             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)
         DeviceType.objects.bulk_create(device_types)
 
 
@@ -1229,6 +1229,39 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
 
 
         self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
         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):
 class ModuleTest(APIViewTestCases.APIViewTestCase):
     model = Module
     model = Module

+ 26 - 1
netbox/netbox/api/viewsets/mixins.py

@@ -15,11 +15,12 @@ from utilities.api import get_serializer_for_model
 
 
 __all__ = (
 __all__ = (
     'BriefModeMixin',
     'BriefModeMixin',
+    'BulkDestroyModelMixin',
     'BulkUpdateModelMixin',
     'BulkUpdateModelMixin',
     'CustomFieldsMixin',
     'CustomFieldsMixin',
     'ExportTemplatesMixin',
     'ExportTemplatesMixin',
-    'BulkDestroyModelMixin',
     'ObjectValidationMixin',
     'ObjectValidationMixin',
+    'SequentialBulkCreatesMixin',
 )
 )
 
 
 
 
@@ -94,6 +95,30 @@ class ExportTemplatesMixin:
         return super().list(request, *args, **kwargs)
         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:
 class BulkUpdateModelMixin:
     """
     """
     Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
     Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one