From 3cad6494ee830fe3c3aa9c8092669de94464499a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 19 Jun 2023 14:30:12 -0400 Subject: [PATCH 1/4] Introduce AvailableObjectsView and refactor 'available objects' API views --- netbox/ipam/api/serializers.py | 2 + netbox/ipam/api/views.py | 406 ++++++++++++++------------------- 2 files changed, 173 insertions(+), 235 deletions(-) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 064452667a7..f59850aa289 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -58,6 +58,7 @@ class AvailableASNSerializer(serializers.Serializer): Representation of an ASN which does not exist in the database. """ asn = serializers.IntegerField(read_only=True) + description = serializers.CharField(required=False) def to_representation(self, asn): rir = NestedRIRSerializer(self.context['range'].rir, context={ @@ -432,6 +433,7 @@ class AvailableIPSerializer(serializers.Serializer): family = serializers.IntegerField(read_only=True) address = serializers.CharField(read_only=True) vrf = NestedVRFSerializer(read_only=True) + description = serializers.CharField(required=False) def to_representation(self, instance): if self.context.get('vrf'): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index f432e0e6b06..63d097f720c 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,8 +2,9 @@ from django.db import transaction from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock -from drf_spectacular.utils import extend_schema +from netaddr import IPSet from rest_framework import status +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.views import APIView @@ -16,6 +17,7 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin from netbox.config import get_config from netbox.constants import ADVISORY_LOCK_KEYS +from utilities.api import get_serializer_for_model from utilities.utils import count_related from . import serializers from ipam.models import L2VPN, L2VPNTermination @@ -207,57 +209,101 @@ def get_results_limit(request): return limit -class AvailableASNsView(ObjectValidationMixin, APIView): - queryset = ASN.objects.all() +class AvailableObjectsView(ObjectValidationMixin, APIView): + """ + Return a list of dicts representing child objects that have not yet been created for a parent object. + """ + read_serializer_class = None + write_serializer_class = None - @extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)}) + def get_parent(self, request, pk): + """ + Return the parent object. + """ + raise NotImplemented() + + def get_available_objects(self, parent, limit=None): + """ + Return all available objects for the parent. + """ + raise NotImplemented() + + def get_extra_context(self, parent): + """ + Return any extra context data for the serializer. + """ + return {} + + def check_sufficient_available(self, requested_objects, available_objects): + """ + Check if there exist a sufficient number of available objects to satisfy the request. + """ + return len(requested_objects) <= len(available_objects) + + def prep_object_data(self, requested_objects, available_objects, parent): + """ + Prepare data by setting any programmatically determined object attributes (e.g. next available VLAN ID) + on the request data. + """ + return requested_objects + + # TODO: Fix OpenAPI schema + # @extend_schema(methods=["get"], responses={200: serializer(many=True)}) def get(self, request, pk): - asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk) + parent = self.get_parent(request, pk) limit = get_results_limit(request) + available_objects = self.get_available_objects(parent, limit) - available_asns = asnrange.get_available_asns()[:limit] - - serializer = serializers.AvailableASNSerializer(available_asns, many=True, context={ + serializer = self.read_serializer_class(available_objects, many=True, context={ 'request': request, - 'range': asnrange, + **self.get_extra_context(parent), }) return Response(serializer.data) - @extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)}) - @advisory_lock(ADVISORY_LOCK_KEYS['available-asns']) + # TODO: Fix OpenAPI schema + # @extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)}) + # TODO: Restore advisory lock + # @advisory_lock(ADVISORY_LOCK_KEYS['available-asns']) def post(self, request, pk): self.queryset = self.queryset.restrict(request.user, 'add') - asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk) + parent = self.get_parent(request, pk) + available_objects = self.get_available_objects(parent) # Normalize to a list of objects - requested_asns = request.data if isinstance(request.data, list) else [request.data] + requested_objects = request.data if isinstance(request.data, list) else [request.data] - # Determine if the requested number of IPs is available - available_asns = asnrange.get_available_asns() - if len(available_asns) < len(requested_asns): + # Serialize and validate the request data + serializer = self.write_serializer_class(data=requested_objects, many=True, context={ + 'request': request, + **self.get_extra_context(parent), + }) + if not serializer.is_valid(): + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + # Determine if the requested number of objects is available + if not self.check_sufficient_available(serializer.validated_data, available_objects): + # TODO: Raise exception instead? return Response( { - "detail": f"An insufficient number of ASNs are available within {asnrange} " - f"({len(requested_asns)} requested, {len(available_asns)} available)" + "detail": f"Insufficient resources are available to satisfy the request" }, status=status.HTTP_409_CONFLICT ) - # Assign ASNs from the list of available IPs and copy VRF assignment from the parent - for i, requested_asn in enumerate(requested_asns): - requested_asn.update({ - 'rir': asnrange.rir.pk, - 'range': asnrange.pk, - 'asn': available_asns[i], - }) + # Prepare object data for deserialization + requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent) # Initialize the serializer with a list or a single object depending on what was requested + serializer_class = get_serializer_for_model(self.queryset.model) context = {'request': request} if isinstance(request.data, list): - serializer = serializers.ASNSerializer(data=requested_asns, many=True, context=context) + serializer = serializer_class(data=requested_objects, many=True, context=context) else: - serializer = serializers.ASNSerializer(data=requested_asns[0], context=context) + serializer = serializer_class(data=requested_objects[0], context=context) # Create the new IP address(es) if serializer.is_valid(): @@ -271,173 +317,113 @@ def post(self, request, pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def get_serializer_class(self): - if self.request.method == "GET": - return serializers.AvailableASNSerializer - return serializers.ASNSerializer +class AvailableASNsView(AvailableObjectsView): + queryset = ASN.objects.all() + read_serializer_class = serializers.AvailableASNSerializer + write_serializer_class = serializers.AvailableASNSerializer + def get_parent(self, request, pk): + return get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk) + + def get_available_objects(self, parent, limit=None): + return parent.get_available_asns()[:limit] + + def get_extra_context(self, parent): + return { + 'range': parent, + } + + def prep_object_data(self, requested_objects, available_objects, parent): + for i, request_data in enumerate(requested_objects): + request_data.update({ + 'rir': parent.rir.pk, + 'range': parent.pk, + 'asn': available_objects[i], + }) -class AvailablePrefixesView(ObjectValidationMixin, APIView): - queryset = Prefix.objects.all() + return requested_objects - @extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)}) - def get(self, request, pk): - prefix = get_object_or_404(Prefix.objects.restrict(request.user), 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) +def get_next_available(ipset, prefix_size): + for available_prefix in ipset.iter_cidrs(): + if prefix_size >= available_prefix.prefixlen: + allocated_prefix = f"{available_prefix.network}/{prefix_size}" + ipset.remove(allocated_prefix) + return allocated_prefix + return None - @extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)}) - @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) - def post(self, request, pk): - self.queryset = self.queryset.restrict(request.user, 'add') - prefix = get_object_or_404(Prefix.objects.restrict(request.user), 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): +class AvailablePrefixesView(AvailableObjectsView): + queryset = Prefix.objects.all() + read_serializer_class = serializers.AvailablePrefixSerializer + write_serializer_class = serializers.PrefixLengthSerializer - # 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_409_CONFLICT - ) + def get_parent(self, request, pk): + return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) - # Remove the allocated prefix from the list of available prefixes - available_prefixes.remove(allocated_prefix) + def get_available_objects(self, parent, limit=None): + return parent.get_available_prefixes().iter_cidrs() - # 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) + def check_sufficient_available(self, requested_objects, available_objects): + available_prefixes = IPSet(available_objects) + for requested_object in requested_objects: + if not get_next_available(available_prefixes, requested_object['prefix_length']): + return False + return True - # 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) + def get_extra_context(self, parent): + return { + 'prefix': parent, + 'vrf': parent.vrf, + } - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def prep_object_data(self, requested_objects, available_objects, parent): + available_prefixes = IPSet(available_objects) + for i, request_data in enumerate(requested_objects): - def get_serializer_class(self): - if self.request.method == "GET": - return serializers.AvailablePrefixSerializer + # Find the first available prefix equal to or larger than the requested size + if allocated_prefix := get_next_available(available_prefixes, request_data['prefix_length']): + request_data.update({ + 'prefix': allocated_prefix, + 'vrf': parent.vrf.pk if parent.vrf else None, + }) + else: + # TODO: Handle this + raise ValidationError("Insufficient space is available to accommodate the requested prefix size(s)") - return serializers.PrefixLengthSerializer + return requested_objects -class AvailableIPAddressesView(ObjectValidationMixin, APIView): +class AvailableIPAddressesView(AvailableObjectsView): queryset = IPAddress.objects.all() + read_serializer_class = serializers.AvailableIPSerializer + write_serializer_class = serializers.AvailableIPSerializer - def get_parent(self, request, pk): - raise NotImplemented() - - @extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)}) - def get(self, request, pk): - parent = self.get_parent(request, pk) - limit = get_results_limit(request) - + def get_available_objects(self, parent, limit=None): # Calculate available IPs within the parent ip_list = [] for index, ip in enumerate(parent.get_available_ips(), start=1): ip_list.append(ip) if index == limit: break - serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ - 'request': request, + return ip_list + + def get_extra_context(self, parent): + return { 'parent': parent, 'vrf': parent.vrf, - }) - - return Response(serializer.data) - - @extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)}) - @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) - def post(self, request, pk): - self.queryset = self.queryset.restrict(request.user, 'add') - parent = self.get_parent(request, pk) - - # Normalize to a list of objects - requested_ips = request.data if isinstance(request.data, list) else [request.data] - - # Determine if the requested number of IPs is available - available_ips = parent.get_available_ips() - if available_ips.size < len(requested_ips): - return Response( - { - "detail": f"An insufficient number of IP addresses are available within {parent} " - f"({len(requested_ips)} requested, {len(available_ips)} available)" - }, - status=status.HTTP_409_CONFLICT - ) - - # Assign addresses from the list of available IPs and copy VRF assignment from the parent - available_ips = iter(available_ips) - for requested_ip in requested_ips: - requested_ip['address'] = f'{next(available_ips)}/{parent.mask_length}' - requested_ip['vrf'] = parent.vrf.pk if parent.vrf else None - - # 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.IPAddressSerializer(data=requested_ips, many=True, context=context) - else: - serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context) - - # Create the new IP address(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) - - def get_serializer_class(self): - if self.request.method == "GET": - return serializers.AvailableIPSerializer + } + + def prep_object_data(self, requested_objects, available_objects, parent): + available_ips = iter(available_objects) + for i, request_data in enumerate(requested_objects): + request_data.update({ + 'address': f'{next(available_ips)}/{parent.mask_length}', + 'vrf': parent.vrf.pk if parent.vrf else None, + }) - return serializers.IPAddressSerializer + return requested_objects class PrefixAvailableIPAddressesView(AvailableIPAddressesView): @@ -452,77 +438,27 @@ def get_parent(self, request, pk): return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk) -class AvailableVLANsView(ObjectValidationMixin, APIView): +class AvailableVLANsView(AvailableObjectsView): queryset = VLAN.objects.all() + read_serializer_class = serializers.AvailableVLANSerializer + write_serializer_class = serializers.CreateAvailableVLANSerializer - @extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)}) - def get(self, request, pk): - vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk) - limit = get_results_limit(request) - - available_vlans = vlangroup.get_available_vids()[:limit] - serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={ - 'request': request, - 'group': vlangroup, - }) - - return Response(serializer.data) - - @extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)}) - @advisory_lock(ADVISORY_LOCK_KEYS['available-vlans']) - def post(self, request, pk): - self.queryset = self.queryset.restrict(request.user, 'add') - vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk) - available_vlans = vlangroup.get_available_vids() - many = isinstance(request.data, list) - - # Validate requested VLANs - serializer = serializers.CreateAvailableVLANSerializer( - data=request.data if many else [request.data], - many=True, - context={ - 'request': request, - 'group': vlangroup, - } - ) - if not serializer.is_valid(): - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST - ) - - requested_vlans = serializer.validated_data - - for i, requested_vlan in enumerate(requested_vlans): - try: - requested_vlan['vid'] = available_vlans.pop(0) - requested_vlan['group'] = vlangroup.pk - except IndexError: - return Response({ - "detail": "The requested number of VLANs is not available" - }, status=status.HTTP_409_CONFLICT) + def get_parent(self, request, pk): + return get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk) - # Initialize the serializer with a list or a single object depending on what was requested - context = {'request': request} - if many: - serializer = serializers.VLANSerializer(data=requested_vlans, many=True, context=context) - else: - serializer = serializers.VLANSerializer(data=requested_vlans[0], context=context) + def get_available_objects(self, parent, limit=None): + return parent.get_available_vids()[:limit] - # Create the new VLAN(s) - 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) + def get_extra_context(self, parent): + return { + 'group': parent, + } - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get_serializer_class(self): - if self.request.method == "GET": - return serializers.AvailableVLANSerializer + def prep_object_data(self, requested_objects, available_objects, parent): + for i, request_data in enumerate(requested_objects): + request_data.update({ + 'vid': available_objects.pop(0), + 'group': parent.pk, + }) - return serializers.VLANSerializer + return requested_objects From 0a9dcd31d9ad6ba8452a23f23ea532cb68934af9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 19 Jun 2023 14:53:04 -0400 Subject: [PATCH 2/4] Restore advisory PostgreSQL locks --- netbox/ipam/api/views.py | 64 ++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 63d097f720c..fa973ffcb47 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,6 +2,7 @@ from django.db import transaction from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock +from drf_spectacular.utils import extend_schema from netaddr import IPSet from rest_framework import status from rest_framework.exceptions import ValidationError @@ -215,6 +216,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView): """ read_serializer_class = None write_serializer_class = None + advisory_lock_key = None def get_parent(self, request, pk): """ @@ -262,15 +264,12 @@ def get(self, request, pk): return Response(serializer.data) # TODO: Fix OpenAPI schema - # @extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)}) - # TODO: Restore advisory lock - # @advisory_lock(ADVISORY_LOCK_KEYS['available-asns']) + # @extend_schema(methods=["post"], responses={201: serializer(many=True)}) def post(self, request, pk): self.queryset = self.queryset.restrict(request.user, 'add') parent = self.get_parent(request, pk) - available_objects = self.get_available_objects(parent) - # Normalize to a list of objects + # Normalize request data to a list of objects requested_objects = request.data if isinstance(request.data, list) else [request.data] # Serialize and validate the request data @@ -284,44 +283,49 @@ def post(self, request, pk): status=status.HTTP_400_BAD_REQUEST ) - # Determine if the requested number of objects is available - if not self.check_sufficient_available(serializer.validated_data, available_objects): - # TODO: Raise exception instead? - return Response( - { - "detail": f"Insufficient resources are available to satisfy the request" - }, - status=status.HTTP_409_CONFLICT - ) - - # Prepare object data for deserialization - requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent) + with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]): + available_objects = self.get_available_objects(parent) + + # Determine if the requested number of objects is available + if not self.check_sufficient_available(serializer.validated_data, available_objects): + # TODO: Raise exception instead? + return Response( + { + "detail": f"Insufficient resources are available to satisfy the request" + }, + status=status.HTTP_409_CONFLICT + ) + + # Prepare object data for deserialization + requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent) + + # Initialize the serializer with a list or a single object depending on what was requested + serializer_class = get_serializer_for_model(self.queryset.model) + context = {'request': request} + if isinstance(request.data, list): + serializer = serializer_class(data=requested_objects, many=True, context=context) + else: + serializer = serializer_class(data=requested_objects[0], context=context) - # Initialize the serializer with a list or a single object depending on what was requested - serializer_class = get_serializer_for_model(self.queryset.model) - context = {'request': request} - if isinstance(request.data, list): - serializer = serializer_class(data=requested_objects, many=True, context=context) - else: - serializer = serializer_class(data=requested_objects[0], context=context) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - # Create the new IP address(es) - if serializer.is_valid(): + # Create the new IP address(es) 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) + return Response(serializer.data, status=status.HTTP_201_CREATED) class AvailableASNsView(AvailableObjectsView): queryset = ASN.objects.all() read_serializer_class = serializers.AvailableASNSerializer write_serializer_class = serializers.AvailableASNSerializer + advisory_lock_key = 'available-asns' def get_parent(self, request, pk): return get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk) @@ -345,6 +349,7 @@ def prep_object_data(self, requested_objects, available_objects, parent): return requested_objects +# TODO: Move me def get_next_available(ipset, prefix_size): for available_prefix in ipset.iter_cidrs(): if prefix_size >= available_prefix.prefixlen: @@ -358,6 +363,7 @@ class AvailablePrefixesView(AvailableObjectsView): queryset = Prefix.objects.all() read_serializer_class = serializers.AvailablePrefixSerializer write_serializer_class = serializers.PrefixLengthSerializer + advisory_lock_key = 'available-prefixes' def get_parent(self, request, pk): return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) @@ -399,6 +405,7 @@ class AvailableIPAddressesView(AvailableObjectsView): queryset = IPAddress.objects.all() read_serializer_class = serializers.AvailableIPSerializer write_serializer_class = serializers.AvailableIPSerializer + advisory_lock_key = 'available-ips' def get_available_objects(self, parent, limit=None): # Calculate available IPs within the parent @@ -442,6 +449,7 @@ class AvailableVLANsView(AvailableObjectsView): queryset = VLAN.objects.all() read_serializer_class = serializers.AvailableVLANSerializer write_serializer_class = serializers.CreateAvailableVLANSerializer + advisory_lock_key = 'available-vlans' def get_parent(self, request, pk): return get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk) From 76236945c84944dd1c4f4f1eb197d2eb8013c3da Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 19 Jun 2023 14:56:43 -0400 Subject: [PATCH 3/4] Move get_next_available_prefix() --- netbox/ipam/api/views.py | 15 +++------------ netbox/ipam/utils.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index fa973ffcb47..4c46ecd440b 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -14,6 +14,7 @@ from dcim.models import Site from ipam import filtersets from ipam.models import * +from ipam.utils import get_next_available_prefix from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets.mixins import ObjectValidationMixin from netbox.config import get_config @@ -349,16 +350,6 @@ def prep_object_data(self, requested_objects, available_objects, parent): return requested_objects -# TODO: Move me -def get_next_available(ipset, prefix_size): - for available_prefix in ipset.iter_cidrs(): - if prefix_size >= available_prefix.prefixlen: - allocated_prefix = f"{available_prefix.network}/{prefix_size}" - ipset.remove(allocated_prefix) - return allocated_prefix - return None - - class AvailablePrefixesView(AvailableObjectsView): queryset = Prefix.objects.all() read_serializer_class = serializers.AvailablePrefixSerializer @@ -374,7 +365,7 @@ def get_available_objects(self, parent, limit=None): def check_sufficient_available(self, requested_objects, available_objects): available_prefixes = IPSet(available_objects) for requested_object in requested_objects: - if not get_next_available(available_prefixes, requested_object['prefix_length']): + if not get_next_available_prefix(available_prefixes, requested_object['prefix_length']): return False return True @@ -389,7 +380,7 @@ def prep_object_data(self, requested_objects, available_objects, parent): for i, request_data in enumerate(requested_objects): # Find the first available prefix equal to or larger than the requested size - if allocated_prefix := get_next_available(available_prefixes, request_data['prefix_length']): + if allocated_prefix := get_next_available_prefix(available_prefixes, request_data['prefix_length']): request_data.update({ 'prefix': allocated_prefix, 'vrf': parent.vrf.pk if parent.vrf else None, diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 93a40e5a0c5..f54c7d41de0 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -1,7 +1,15 @@ import netaddr from .constants import * -from .models import ASN, Prefix, VLAN +from .models import Prefix, VLAN + +__all__ = ( + 'add_available_ipaddresses', + 'add_available_vlans', + 'add_requested_prefixes', + 'get_next_available_prefix', + 'rebuild_prefixes', +) def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True): @@ -184,3 +192,15 @@ def push_to_stack(prefix): # Final flush of any remaining Prefixes Prefix.objects.bulk_update(update_queue, ['_depth', '_children']) + + +def get_next_available_prefix(ipset, prefix_size): + """ + Given a prefix length, allocate the next available prefix from an IPSet. + """ + for available_prefix in ipset.iter_cidrs(): + if prefix_size >= available_prefix.prefixlen: + allocated_prefix = f"{available_prefix.network}/{prefix_size}" + ipset.remove(allocated_prefix) + return allocated_prefix + return None From 0bc1849d05da29af027ce474c85958e29bd621ab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 20 Jun 2023 14:46:46 -0400 Subject: [PATCH 4/4] Apply OpenAPI decorators for get() and post() --- netbox/ipam/api/views.py | 42 +++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 4c46ecd440b..c895a706bc7 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -250,8 +250,6 @@ def prep_object_data(self, requested_objects, available_objects, parent): """ return requested_objects - # TODO: Fix OpenAPI schema - # @extend_schema(methods=["get"], responses={200: serializer(many=True)}) def get(self, request, pk): parent = self.get_parent(request, pk) limit = get_results_limit(request) @@ -264,8 +262,6 @@ def get(self, request, pk): return Response(serializer.data) - # TODO: Fix OpenAPI schema - # @extend_schema(methods=["post"], responses={201: serializer(many=True)}) def post(self, request, pk): self.queryset = self.queryset.restrict(request.user, 'add') parent = self.get_parent(request, pk) @@ -289,11 +285,8 @@ def post(self, request, pk): # Determine if the requested number of objects is available if not self.check_sufficient_available(serializer.validated_data, available_objects): - # TODO: Raise exception instead? return Response( - { - "detail": f"Insufficient resources are available to satisfy the request" - }, + {"detail": f"Insufficient resources are available to satisfy the request"}, status=status.HTTP_409_CONFLICT ) @@ -349,6 +342,14 @@ def prep_object_data(self, requested_objects, available_objects, parent): return requested_objects + @extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)}) + def get(self, request, pk): + return super().get(request, pk) + + @extend_schema(methods=["post"], responses={201: serializers.AvailableASNSerializer(many=True)}) + def post(self, request, pk): + return super().post(request, pk) + class AvailablePrefixesView(AvailableObjectsView): queryset = Prefix.objects.all() @@ -386,11 +387,18 @@ def prep_object_data(self, requested_objects, available_objects, parent): 'vrf': parent.vrf.pk if parent.vrf else None, }) else: - # TODO: Handle this raise ValidationError("Insufficient space is available to accommodate the requested prefix size(s)") return requested_objects + @extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)}) + def get(self, request, pk): + return super().get(request, pk) + + @extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)}) + def post(self, request, pk): + return super().post(request, pk) + class AvailableIPAddressesView(AvailableObjectsView): queryset = IPAddress.objects.all() @@ -423,6 +431,14 @@ def prep_object_data(self, requested_objects, available_objects, parent): return requested_objects + @extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)}) + def get(self, request, pk): + return super().get(request, pk) + + @extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)}) + def post(self, request, pk): + return super().post(request, pk) + class PrefixAvailableIPAddressesView(AvailableIPAddressesView): @@ -461,3 +477,11 @@ def prep_object_data(self, requested_objects, available_objects, parent): }) return requested_objects + + @extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)}) + def get(self, request, pk): + return super().get(request, pk) + + @extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)}) + def post(self, request, pk): + return super().post(request, pk)