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

Move available prefixes endpoint to its own view

jeremystretch 4 лет назад
Родитель
Сommit
ef5bbdb1e2
4 измененных файлов с 118 добавлено и 151 удалено
  1. 0 84
      netbox/ipam/api/mixins.py
  2. 8 1
      netbox/ipam/api/urls.py
  3. 92 51
      netbox/ipam/api/views.py
  4. 18 15
      netbox/netbox/api/views.py

+ 0 - 84
netbox/ipam/api/mixins.py

@@ -13,90 +13,6 @@ from utilities.constants import ADVISORY_LOCK_KEYS
 from . import serializers
 
 
-class AvailablePrefixesMixin:
-
-    @swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
-    @swagger_auto_schema(method='post', responses={201: serializers.PrefixSerializer(many=False)})
-    @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
-    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
-    def available_prefixes(self, request, pk=None):
-        """
-        A convenience method for returning available child prefixes within a parent.
-
-        The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
-        invoked in parallel, which results in a race condition where multiple insertions can occur.
-        """
-        prefix = get_object_or_404(self.queryset, pk=pk)
-        available_prefixes = prefix.get_available_prefixes()
-
-        if request.method == 'POST':
-
-            # Validate Requested Prefixes' length
-            serializer = serializers.PrefixLengthSerializer(
-                data=request.data if isinstance(request.data, list) else [request.data],
-                many=True,
-                context={
-                    'request': request,
-                    'prefix': prefix,
-                }
-            )
-            if not serializer.is_valid():
-                return Response(
-                    serializer.errors,
-                    status=status.HTTP_400_BAD_REQUEST
-                )
-
-            requested_prefixes = serializer.validated_data
-            # Allocate prefixes to the requested objects based on availability within the parent
-            for i, requested_prefix in enumerate(requested_prefixes):
-
-                # Find the first available prefix equal to or larger than the requested size
-                for available_prefix in available_prefixes.iter_cidrs():
-                    if requested_prefix['prefix_length'] >= available_prefix.prefixlen:
-                        allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length'])
-                        requested_prefix['prefix'] = allocated_prefix
-                        requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None
-                        break
-                else:
-                    return Response(
-                        {
-                            "detail": "Insufficient space is available to accommodate the requested prefix size(s)"
-                        },
-                        status=status.HTTP_204_NO_CONTENT
-                    )
-
-                # Remove the allocated prefix from the list of available prefixes
-                available_prefixes.remove(allocated_prefix)
-
-            # Initialize the serializer with a list or a single object depending on what was requested
-            context = {'request': request}
-            if isinstance(request.data, list):
-                serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
-            else:
-                serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
-
-            # Create the new Prefix(es)
-            if serializer.is_valid():
-                try:
-                    with transaction.atomic():
-                        created = serializer.save()
-                        self._validate_objects(created)
-                except ObjectDoesNotExist:
-                    raise PermissionDenied()
-                return Response(serializer.data, status=status.HTTP_201_CREATED)
-
-            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-        else:
-
-            serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={
-                'request': request,
-                'vrf': prefix.vrf,
-            })
-
-            return Response(serializer.data)
-
-
 class AvailableIPsMixin:
     parent_model = Prefix
 

+ 8 - 1
netbox/ipam/api/urls.py

@@ -1,3 +1,5 @@
+from django.urls import path
+
 from netbox.api import OrderedDefaultRouter
 from . import views
 
@@ -42,4 +44,9 @@ router.register('vlans', views.VLANViewSet)
 router.register('services', views.ServiceViewSet)
 
 app_name = 'ipam-api'
-urlpatterns = router.urls
+
+urlpatterns = [
+    path('prefixes/<int:pk>/available-prefixes/', views.AvailablePrefixesView.as_view(), name='prefix-available-prefixes'),
+]
+
+urlpatterns += router.urls

+ 92 - 51
netbox/ipam/api/views.py

@@ -1,10 +1,19 @@
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
+from django.db import transaction
+from django_pglocks import advisory_lock
+from django.shortcuts import get_object_or_404
+from rest_framework import status
+from rest_framework.response import Response
 from rest_framework.routers import APIRootView
+from rest_framework.views import APIView
+
 
 from dcim.models import Site
 from extras.api.views import CustomFieldModelViewSet
 from ipam import filtersets
 from ipam.models import *
-from netbox.api.views import ModelViewSet
+from netbox.api.views import ModelViewSet, ObjectValidationMixin
+from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import count_related
 from . import mixins, serializers
 
@@ -18,7 +27,7 @@ class IPAMRootView(APIRootView):
 
 
 #
-# ASNs
+# Viewsets
 #
 
 class ASNViewSet(CustomFieldModelViewSet):
@@ -27,10 +36,6 @@ class ASNViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.ASNFilterSet
 
 
-#
-# VRFs
-#
-
 class VRFViewSet(CustomFieldModelViewSet):
     queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
         'import_targets', 'export_targets', 'tags'
@@ -42,20 +47,12 @@ class VRFViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VRFFilterSet
 
 
-#
-# Route targets
-#
-
 class RouteTargetViewSet(CustomFieldModelViewSet):
     queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
     serializer_class = serializers.RouteTargetSerializer
     filterset_class = filtersets.RouteTargetFilterSet
 
 
-#
-# RIRs
-#
-
 class RIRViewSet(CustomFieldModelViewSet):
     queryset = RIR.objects.annotate(
         aggregate_count=count_related(Aggregate, 'rir')
@@ -64,20 +61,12 @@ class RIRViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.RIRFilterSet
 
 
-#
-# Aggregates
-#
-
 class AggregateViewSet(CustomFieldModelViewSet):
     queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
     serializer_class = serializers.AggregateSerializer
     filterset_class = filtersets.AggregateFilterSet
 
 
-#
-# Roles
-#
-
 class RoleViewSet(CustomFieldModelViewSet):
     queryset = Role.objects.annotate(
         prefix_count=count_related(Prefix, 'role'),
@@ -87,11 +76,7 @@ class RoleViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.RoleFilterSet
 
 
-#
-# Prefixes
-#
-
-class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, CustomFieldModelViewSet):
+class PrefixViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
     queryset = Prefix.objects.prefetch_related(
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
     )
@@ -106,10 +91,6 @@ class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, Cus
         return super().get_serializer_class()
 
 
-#
-# IP ranges
-#
-
 class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
     queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
     serializer_class = serializers.IPRangeSerializer
@@ -118,10 +99,6 @@ class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
     parent_model = IPRange  # AvailableIPsMixin
 
 
-#
-# IP addresses
-#
-
 class IPAddressViewSet(CustomFieldModelViewSet):
     queryset = IPAddress.objects.prefetch_related(
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
@@ -130,10 +107,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.IPAddressFilterSet
 
 
-#
-# FHRP groups
-#
-
 class FHRPGroupViewSet(CustomFieldModelViewSet):
     queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
     serializer_class = serializers.FHRPGroupSerializer
@@ -147,10 +120,6 @@ class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.FHRPGroupAssignmentFilterSet
 
 
-#
-# VLAN groups
-#
-
 class VLANGroupViewSet(CustomFieldModelViewSet):
     queryset = VLANGroup.objects.annotate(
         vlan_count=count_related(VLAN, 'group')
@@ -159,10 +128,6 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VLANGroupFilterSet
 
 
-#
-# VLANs
-#
-
 class VLANViewSet(CustomFieldModelViewSet):
     queryset = VLAN.objects.prefetch_related(
         'site', 'group', 'tenant', 'role', 'tags'
@@ -173,13 +138,89 @@ class VLANViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VLANFilterSet
 
 
-#
-# Services
-#
-
 class ServiceViewSet(ModelViewSet):
     queryset = Service.objects.prefetch_related(
         'device', 'virtual_machine', 'tags', 'ipaddresses'
     )
     serializer_class = serializers.ServiceSerializer
     filterset_class = filtersets.ServiceFilterSet
+
+
+#
+# Views
+#
+
+class AvailablePrefixesView(ObjectValidationMixin, APIView):
+    queryset = Prefix.objects.all()
+
+    def get(self, request, pk):
+        prefix = get_object_or_404(self.queryset, pk=pk)
+        available_prefixes = prefix.get_available_prefixes()
+
+        serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={
+            'request': request,
+            'vrf': prefix.vrf,
+        })
+
+        return Response(serializer.data)
+
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
+    def post(self, request, pk):
+        prefix = get_object_or_404(self.queryset, pk=pk)
+        available_prefixes = prefix.get_available_prefixes()
+
+        # Validate Requested Prefixes' length
+        serializer = serializers.PrefixLengthSerializer(
+            data=request.data if isinstance(request.data, list) else [request.data],
+            many=True,
+            context={
+                'request': request,
+                'prefix': prefix,
+            }
+        )
+        if not serializer.is_valid():
+            return Response(
+                serializer.errors,
+                status=status.HTTP_400_BAD_REQUEST
+            )
+
+        requested_prefixes = serializer.validated_data
+        # Allocate prefixes to the requested objects based on availability within the parent
+        for i, requested_prefix in enumerate(requested_prefixes):
+
+            # Find the first available prefix equal to or larger than the requested size
+            for available_prefix in available_prefixes.iter_cidrs():
+                if requested_prefix['prefix_length'] >= available_prefix.prefixlen:
+                    allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length'])
+                    requested_prefix['prefix'] = allocated_prefix
+                    requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None
+                    break
+            else:
+                return Response(
+                    {
+                        "detail": "Insufficient space is available to accommodate the requested prefix size(s)"
+                    },
+                    status=status.HTTP_204_NO_CONTENT
+                )
+
+            # Remove the allocated prefix from the list of available prefixes
+            available_prefixes.remove(allocated_prefix)
+
+        # Initialize the serializer with a list or a single object depending on what was requested
+        context = {'request': request}
+        if isinstance(request.data, list):
+            serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
+        else:
+            serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
+
+        # Create the new Prefix(es)
+        if serializer.is_valid():
+            try:
+                with transaction.atomic():
+                    created = serializer.save()
+                    self._validate_objects(created)
+            except ObjectDoesNotExist:
+                raise PermissionDenied()
+            return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

+ 18 - 15
netbox/netbox/api/views.py

@@ -123,11 +123,28 @@ class BulkDestroyModelMixin:
                 self.perform_destroy(obj)
 
 
+class ObjectValidationMixin:
+
+    def _validate_objects(self, instance):
+        """
+        Check that the provided instance or list of instances are matched by the current queryset. This confirms that
+        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
+        """
+        if type(instance) is list:
+            # Check that all instances are still included in the view's queryset
+            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
+            if conforming_count != len(instance):
+                raise ObjectDoesNotExist
+        else:
+            # Check that the instance is matched by the view's queryset
+            self.queryset.get(pk=instance.pk)
+
+
 #
 # Viewsets
 #
 
-class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
+class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet_):
     """
     Extend DRF's ModelViewSet to support bulk update and delete functions.
     """
@@ -211,20 +228,6 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
                 **kwargs
             )
 
-    def _validate_objects(self, instance):
-        """
-        Check that the provided instance or list of instances are matched by the current queryset. This confirms that
-        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
-        """
-        if type(instance) is list:
-            # Check that all instances are still included in the view's queryset
-            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
-            if conforming_count != len(instance):
-                raise ObjectDoesNotExist
-        else:
-            # Check that the instance is matched by the view's queryset
-            self.queryset.get(pk=instance.pk)
-
     def list(self, request, *args, **kwargs):
         """
         Overrides ListModelMixin to allow processing ExportTemplates.