Move available prefixes endpoint to its own view

This commit is contained in:
jeremystretch 2021-12-10 11:38:58 -05:00
parent 88fae2171d
commit ef5bbdb1e2
4 changed files with 118 additions and 151 deletions

View File

@ -13,90 +13,6 @@ from utilities.constants import ADVISORY_LOCK_KEYS
from . import serializers 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: class AvailableIPsMixin:
parent_model = Prefix parent_model = Prefix

View File

@ -1,3 +1,5 @@
from django.urls import path
from netbox.api import OrderedDefaultRouter from netbox.api import OrderedDefaultRouter
from . import views from . import views
@ -42,4 +44,9 @@ router.register('vlans', views.VLANViewSet)
router.register('services', views.ServiceViewSet) router.register('services', views.ServiceViewSet)
app_name = 'ipam-api' 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

View File

@ -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.routers import APIRootView
from rest_framework.views import APIView
from dcim.models import Site from dcim.models import Site
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from ipam import filtersets from ipam import filtersets
from ipam.models import * 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 utilities.utils import count_related
from . import mixins, serializers from . import mixins, serializers
@ -18,7 +27,7 @@ class IPAMRootView(APIRootView):
# #
# ASNs # Viewsets
# #
class ASNViewSet(CustomFieldModelViewSet): class ASNViewSet(CustomFieldModelViewSet):
@ -27,10 +36,6 @@ class ASNViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.ASNFilterSet filterset_class = filtersets.ASNFilterSet
#
# VRFs
#
class VRFViewSet(CustomFieldModelViewSet): class VRFViewSet(CustomFieldModelViewSet):
queryset = VRF.objects.prefetch_related('tenant').prefetch_related( queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
'import_targets', 'export_targets', 'tags' 'import_targets', 'export_targets', 'tags'
@ -42,20 +47,12 @@ class VRFViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VRFFilterSet filterset_class = filtersets.VRFFilterSet
#
# Route targets
#
class RouteTargetViewSet(CustomFieldModelViewSet): class RouteTargetViewSet(CustomFieldModelViewSet):
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags') queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
serializer_class = serializers.RouteTargetSerializer serializer_class = serializers.RouteTargetSerializer
filterset_class = filtersets.RouteTargetFilterSet filterset_class = filtersets.RouteTargetFilterSet
#
# RIRs
#
class RIRViewSet(CustomFieldModelViewSet): class RIRViewSet(CustomFieldModelViewSet):
queryset = RIR.objects.annotate( queryset = RIR.objects.annotate(
aggregate_count=count_related(Aggregate, 'rir') aggregate_count=count_related(Aggregate, 'rir')
@ -64,20 +61,12 @@ class RIRViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.RIRFilterSet filterset_class = filtersets.RIRFilterSet
#
# Aggregates
#
class AggregateViewSet(CustomFieldModelViewSet): class AggregateViewSet(CustomFieldModelViewSet):
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags') queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
serializer_class = serializers.AggregateSerializer serializer_class = serializers.AggregateSerializer
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
#
# Roles
#
class RoleViewSet(CustomFieldModelViewSet): class RoleViewSet(CustomFieldModelViewSet):
queryset = Role.objects.annotate( queryset = Role.objects.annotate(
prefix_count=count_related(Prefix, 'role'), prefix_count=count_related(Prefix, 'role'),
@ -87,11 +76,7 @@ class RoleViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.RoleFilterSet filterset_class = filtersets.RoleFilterSet
# class PrefixViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
# Prefixes
#
class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, CustomFieldModelViewSet):
queryset = Prefix.objects.prefetch_related( queryset = Prefix.objects.prefetch_related(
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags' 'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
) )
@ -106,10 +91,6 @@ class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, Cus
return super().get_serializer_class() return super().get_serializer_class()
#
# IP ranges
#
class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags') queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
serializer_class = serializers.IPRangeSerializer serializer_class = serializers.IPRangeSerializer
@ -118,10 +99,6 @@ class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet):
parent_model = IPRange # AvailableIPsMixin parent_model = IPRange # AvailableIPsMixin
#
# IP addresses
#
class IPAddressViewSet(CustomFieldModelViewSet): class IPAddressViewSet(CustomFieldModelViewSet):
queryset = IPAddress.objects.prefetch_related( queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object' 'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
@ -130,10 +107,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.IPAddressFilterSet filterset_class = filtersets.IPAddressFilterSet
#
# FHRP groups
#
class FHRPGroupViewSet(CustomFieldModelViewSet): class FHRPGroupViewSet(CustomFieldModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags') queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
serializer_class = serializers.FHRPGroupSerializer serializer_class = serializers.FHRPGroupSerializer
@ -147,10 +120,6 @@ class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.FHRPGroupAssignmentFilterSet filterset_class = filtersets.FHRPGroupAssignmentFilterSet
#
# VLAN groups
#
class VLANGroupViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(CustomFieldModelViewSet):
queryset = VLANGroup.objects.annotate( queryset = VLANGroup.objects.annotate(
vlan_count=count_related(VLAN, 'group') vlan_count=count_related(VLAN, 'group')
@ -159,10 +128,6 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VLANGroupFilterSet filterset_class = filtersets.VLANGroupFilterSet
#
# VLANs
#
class VLANViewSet(CustomFieldModelViewSet): class VLANViewSet(CustomFieldModelViewSet):
queryset = VLAN.objects.prefetch_related( queryset = VLAN.objects.prefetch_related(
'site', 'group', 'tenant', 'role', 'tags' 'site', 'group', 'tenant', 'role', 'tags'
@ -173,13 +138,89 @@ class VLANViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VLANFilterSet filterset_class = filtersets.VLANFilterSet
#
# Services
#
class ServiceViewSet(ModelViewSet): class ServiceViewSet(ModelViewSet):
queryset = Service.objects.prefetch_related( queryset = Service.objects.prefetch_related(
'device', 'virtual_machine', 'tags', 'ipaddresses' 'device', 'virtual_machine', 'tags', 'ipaddresses'
) )
serializer_class = serializers.ServiceSerializer serializer_class = serializers.ServiceSerializer
filterset_class = filtersets.ServiceFilterSet 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)

View File

@ -123,11 +123,28 @@ class BulkDestroyModelMixin:
self.perform_destroy(obj) 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 # Viewsets
# #
class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet_):
""" """
Extend DRF's ModelViewSet to support bulk update and delete functions. Extend DRF's ModelViewSet to support bulk update and delete functions.
""" """
@ -211,20 +228,6 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
**kwargs **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): def list(self, request, *args, **kwargs):
""" """
Overrides ListModelMixin to allow processing ExportTemplates. Overrides ListModelMixin to allow processing ExportTemplates.