mixins.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. from django.conf import settings
  2. from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
  3. from django.db import transaction
  4. from django.shortcuts import get_object_or_404
  5. from django_pglocks import advisory_lock
  6. from drf_yasg.utils import swagger_auto_schema
  7. from rest_framework import status
  8. from rest_framework.decorators import action
  9. from rest_framework.response import Response
  10. from ipam.models import *
  11. from utilities.constants import ADVISORY_LOCK_KEYS
  12. from . import serializers
  13. class AvailablePrefixesMixin:
  14. @swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)})
  15. @swagger_auto_schema(method='post', responses={201: serializers.PrefixSerializer(many=False)})
  16. @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
  17. @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
  18. def available_prefixes(self, request, pk=None):
  19. """
  20. A convenience method for returning available child prefixes within a parent.
  21. The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
  22. invoked in parallel, which results in a race condition where multiple insertions can occur.
  23. """
  24. prefix = get_object_or_404(self.queryset, pk=pk)
  25. available_prefixes = prefix.get_available_prefixes()
  26. if request.method == 'POST':
  27. # Validate Requested Prefixes' length
  28. serializer = serializers.PrefixLengthSerializer(
  29. data=request.data if isinstance(request.data, list) else [request.data],
  30. many=True,
  31. context={
  32. 'request': request,
  33. 'prefix': prefix,
  34. }
  35. )
  36. if not serializer.is_valid():
  37. return Response(
  38. serializer.errors,
  39. status=status.HTTP_400_BAD_REQUEST
  40. )
  41. requested_prefixes = serializer.validated_data
  42. # Allocate prefixes to the requested objects based on availability within the parent
  43. for i, requested_prefix in enumerate(requested_prefixes):
  44. # Find the first available prefix equal to or larger than the requested size
  45. for available_prefix in available_prefixes.iter_cidrs():
  46. if requested_prefix['prefix_length'] >= available_prefix.prefixlen:
  47. allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length'])
  48. requested_prefix['prefix'] = allocated_prefix
  49. requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None
  50. break
  51. else:
  52. return Response(
  53. {
  54. "detail": "Insufficient space is available to accommodate the requested prefix size(s)"
  55. },
  56. status=status.HTTP_204_NO_CONTENT
  57. )
  58. # Remove the allocated prefix from the list of available prefixes
  59. available_prefixes.remove(allocated_prefix)
  60. # Initialize the serializer with a list or a single object depending on what was requested
  61. context = {'request': request}
  62. if isinstance(request.data, list):
  63. serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
  64. else:
  65. serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
  66. # Create the new Prefix(es)
  67. if serializer.is_valid():
  68. try:
  69. with transaction.atomic():
  70. created = serializer.save()
  71. self._validate_objects(created)
  72. except ObjectDoesNotExist:
  73. raise PermissionDenied()
  74. return Response(serializer.data, status=status.HTTP_201_CREATED)
  75. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  76. else:
  77. serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={
  78. 'request': request,
  79. 'vrf': prefix.vrf,
  80. })
  81. return Response(serializer.data)
  82. class AvailableIPsMixin:
  83. parent_model = Prefix
  84. @swagger_auto_schema(method='get', responses={200: serializers.AvailableIPSerializer(many=True)})
  85. @swagger_auto_schema(method='post', responses={201: serializers.AvailableIPSerializer(many=True)},
  86. request_body=serializers.AvailableIPSerializer(many=True))
  87. @action(detail=True, url_path='available-ips', methods=['get', 'post'], queryset=IPAddress.objects.all())
  88. @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
  89. def available_ips(self, request, pk=None):
  90. """
  91. A convenience method for returning available IP addresses within a Prefix or IPRange. By default, the number of
  92. IPs returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be
  93. passed, however results will not be paginated.
  94. The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
  95. invoked in parallel, which results in a race condition where multiple insertions can occur.
  96. """
  97. parent = get_object_or_404(self.parent_model.objects.restrict(request.user), pk=pk)
  98. # Create the next available IP
  99. if request.method == 'POST':
  100. # Normalize to a list of objects
  101. requested_ips = request.data if isinstance(request.data, list) else [request.data]
  102. # Determine if the requested number of IPs is available
  103. available_ips = parent.get_available_ips()
  104. if available_ips.size < len(requested_ips):
  105. return Response(
  106. {
  107. "detail": f"An insufficient number of IP addresses are available within {parent} "
  108. f"({len(requested_ips)} requested, {len(available_ips)} available)"
  109. },
  110. status=status.HTTP_204_NO_CONTENT
  111. )
  112. # Assign addresses from the list of available IPs and copy VRF assignment from the parent
  113. available_ips = iter(available_ips)
  114. for requested_ip in requested_ips:
  115. requested_ip['address'] = f'{next(available_ips)}/{parent.mask_length}'
  116. requested_ip['vrf'] = parent.vrf.pk if parent.vrf else None
  117. # Initialize the serializer with a list or a single object depending on what was requested
  118. context = {'request': request}
  119. if isinstance(request.data, list):
  120. serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
  121. else:
  122. serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
  123. # Create the new IP address(es)
  124. if serializer.is_valid():
  125. try:
  126. with transaction.atomic():
  127. created = serializer.save()
  128. self._validate_objects(created)
  129. except ObjectDoesNotExist:
  130. raise PermissionDenied()
  131. return Response(serializer.data, status=status.HTTP_201_CREATED)
  132. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  133. # Determine the maximum number of IPs to return
  134. else:
  135. try:
  136. limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
  137. except ValueError:
  138. limit = settings.PAGINATE_COUNT
  139. if settings.MAX_PAGE_SIZE:
  140. limit = min(limit, settings.MAX_PAGE_SIZE)
  141. # Calculate available IPs within the parent
  142. ip_list = []
  143. for index, ip in enumerate(parent.get_available_ips(), start=1):
  144. ip_list.append(ip)
  145. if index == limit:
  146. break
  147. serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
  148. 'request': request,
  149. 'parent': parent,
  150. })
  151. return Response(serializer.data)