1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00
Matt Olenik 2e83ce76ed Fix race condition in available-prefix/ip APIs
Implement advisory lock to prevent duplicate records being inserted
when making simultaneous calls. Fixes #2519
2020-02-11 13:36:52 -08:00

314 lines
12 KiB
Python

from django.conf import settings
from django.db.models import Count
from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from extras.api.views import CustomFieldModelViewSet
from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery
from . import serializers
#
# Field choices
#
class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(serializers.AggregateSerializer, ['family']),
(serializers.PrefixSerializer, ['family', 'status']),
(serializers.IPAddressSerializer, ['family', 'status', 'role']),
(serializers.VLANSerializer, ['status']),
(serializers.ServiceSerializer, ['protocol']),
)
#
# VRFs
#
class VRFViewSet(CustomFieldModelViewSet):
queryset = VRF.objects.prefetch_related('tenant').prefetch_related('tags').annotate(
ipaddress_count=get_subquery(IPAddress, 'vrf'),
prefix_count=get_subquery(Prefix, 'vrf')
)
serializer_class = serializers.VRFSerializer
filterset_class = filters.VRFFilterSet
#
# RIRs
#
class RIRViewSet(ModelViewSet):
queryset = RIR.objects.annotate(
aggregate_count=Count('aggregates')
)
serializer_class = serializers.RIRSerializer
filterset_class = filters.RIRFilterSet
#
# Aggregates
#
class AggregateViewSet(CustomFieldModelViewSet):
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
serializer_class = serializers.AggregateSerializer
filterset_class = filters.AggregateFilterSet
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.annotate(
prefix_count=get_subquery(Prefix, 'role'),
vlan_count=get_subquery(VLAN, 'role')
)
serializer_class = serializers.RoleSerializer
filterset_class = filters.RoleFilterSet
#
# Prefixes
#
class PrefixViewSet(CustomFieldModelViewSet):
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags')
serializer_class = serializers.PrefixSerializer
filterset_class = filters.PrefixFilterSet
@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(Prefix, pk=pk)
available_prefixes = prefix.get_available_prefixes()
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_prefix'):
raise PermissionDenied()
# Normalize to a list of objects
requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
# Allocate prefixes to the requested objects based on availability within the parent
for i, requested_prefix in enumerate(requested_prefixes):
# Validate requested prefix size
prefix_length = requested_prefix.get('prefix_length')
if prefix_length is None:
return Response(
{
"detail": "Item {}: prefix_length field missing".format(i)
},
status=status.HTTP_400_BAD_REQUEST
)
try:
prefix_length = int(prefix_length)
except ValueError:
return Response(
{
"detail": "Item {}: Invalid prefix length ({})".format(i, prefix_length),
},
status=status.HTTP_400_BAD_REQUEST
)
if prefix.family == 4 and prefix_length > 32:
return Response(
{
"detail": "Item {}: Invalid prefix length ({}) for IPv4".format(i, prefix_length),
},
status=status.HTTP_400_BAD_REQUEST
)
elif prefix.family == 6 and prefix_length > 128:
return Response(
{
"detail": "Item {}: Invalid prefix length ({}) for IPv6".format(i, prefix_length),
},
status=status.HTTP_400_BAD_REQUEST
)
# 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():
serializer.save()
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)
@action(detail=True, url_path='available-ips', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def available_ips(self, request, pk=None):
"""
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated.
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(Prefix, pk=pk)
# Create the next available IP within the prefix
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_ipaddress'):
raise PermissionDenied()
# 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 = prefix.get_available_ips()
if available_ips.size < len(requested_ips):
return Response(
{
"detail": "An insufficient number of IP addresses are available within the prefix {} ({} "
"requested, {} available)".format(prefix, len(requested_ips), len(available_ips))
},
status=status.HTTP_204_NO_CONTENT
)
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
available_ips = iter(available_ips)
prefix_length = prefix.prefix.prefixlen
for requested_ip in requested_ips:
requested_ip['address'] = '{}/{}'.format(next(available_ips), prefix_length)
requested_ip['vrf'] = prefix.vrf.pk if prefix.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():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Determine the maximum number of IPs to return
else:
try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
except ValueError:
limit = settings.PAGINATE_COUNT
if settings.MAX_PAGE_SIZE:
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = []
for index, ip in enumerate(prefix.get_available_ips(), start=1):
ip_list.append(ip)
if index == limit:
break
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,
'vrf': prefix.vrf,
})
return Response(serializer.data)
#
# IP addresses
#
class IPAddressViewSet(CustomFieldModelViewSet):
queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine',
'nat_outside', 'tags',
)
serializer_class = serializers.IPAddressSerializer
filterset_class = filters.IPAddressFilterSet
#
# VLAN groups
#
class VLANGroupViewSet(ModelViewSet):
queryset = VLANGroup.objects.prefetch_related('site').annotate(
vlan_count=Count('vlans')
)
serializer_class = serializers.VLANGroupSerializer
filterset_class = filters.VLANGroupFilterSet
#
# VLANs
#
class VLANViewSet(CustomFieldModelViewSet):
queryset = VLAN.objects.prefetch_related(
'site', 'group', 'tenant', 'role', 'tags'
).annotate(
prefix_count=get_subquery(Prefix, 'role')
)
serializer_class = serializers.VLANSerializer
filterset_class = filters.VLANFilterSet
#
# Services
#
class ServiceViewSet(ModelViewSet):
queryset = Service.objects.prefetch_related('device').prefetch_related('tags')
serializer_class = serializers.ServiceSerializer
filterset_class = filters.ServiceFilterSet