views.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. from copy import deepcopy
  2. from django.contrib.contenttypes.prefetch import GenericPrefetch
  3. from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
  4. from django.db import router, transaction
  5. from django.shortcuts import get_object_or_404
  6. from django.utils.translation import gettext as _
  7. from django_pglocks import advisory_lock
  8. from drf_spectacular.utils import extend_schema
  9. from netaddr import IPSet
  10. from rest_framework import status
  11. from rest_framework.exceptions import ValidationError
  12. from rest_framework.response import Response
  13. from rest_framework.routers import APIRootView
  14. from rest_framework.views import APIView
  15. from dcim.models import Interface
  16. from ipam import filtersets
  17. from ipam.models import *
  18. from ipam.utils import get_next_available_prefix
  19. from netbox.api.viewsets import NetBoxModelViewSet
  20. from netbox.api.viewsets.mixins import ObjectValidationMixin
  21. from netbox.config import get_config
  22. from netbox.constants import ADVISORY_LOCK_KEYS
  23. from utilities.api import get_serializer_for_model
  24. from virtualization.models import VMInterface
  25. from . import serializers
  26. class IPAMRootView(APIRootView):
  27. """
  28. IPAM API root view
  29. """
  30. def get_view_name(self):
  31. return 'IPAM'
  32. #
  33. # Viewsets
  34. #
  35. class ASNRangeViewSet(NetBoxModelViewSet):
  36. queryset = ASNRange.objects.all()
  37. serializer_class = serializers.ASNRangeSerializer
  38. filterset_class = filtersets.ASNRangeFilterSet
  39. class ASNViewSet(NetBoxModelViewSet):
  40. queryset = ASN.objects.all()
  41. serializer_class = serializers.ASNSerializer
  42. filterset_class = filtersets.ASNFilterSet
  43. class VRFViewSet(NetBoxModelViewSet):
  44. queryset = VRF.objects.all()
  45. serializer_class = serializers.VRFSerializer
  46. filterset_class = filtersets.VRFFilterSet
  47. class RouteTargetViewSet(NetBoxModelViewSet):
  48. queryset = RouteTarget.objects.all()
  49. serializer_class = serializers.RouteTargetSerializer
  50. filterset_class = filtersets.RouteTargetFilterSet
  51. class RIRViewSet(NetBoxModelViewSet):
  52. queryset = RIR.objects.all()
  53. serializer_class = serializers.RIRSerializer
  54. filterset_class = filtersets.RIRFilterSet
  55. class AggregateViewSet(NetBoxModelViewSet):
  56. queryset = Aggregate.objects.all()
  57. serializer_class = serializers.AggregateSerializer
  58. filterset_class = filtersets.AggregateFilterSet
  59. class RoleViewSet(NetBoxModelViewSet):
  60. queryset = Role.objects.all()
  61. serializer_class = serializers.RoleSerializer
  62. filterset_class = filtersets.RoleFilterSet
  63. class PrefixViewSet(NetBoxModelViewSet):
  64. queryset = Prefix.objects.prefetch_related("scope")
  65. serializer_class = serializers.PrefixSerializer
  66. filterset_class = filtersets.PrefixFilterSet
  67. parent_model = Prefix # AvailableIPsMixin
  68. def get_serializer_class(self):
  69. if self.action == "available_prefixes" and self.request.method == "POST":
  70. return serializers.PrefixLengthSerializer
  71. return super().get_serializer_class()
  72. class IPRangeViewSet(NetBoxModelViewSet):
  73. queryset = IPRange.objects.all()
  74. serializer_class = serializers.IPRangeSerializer
  75. filterset_class = filtersets.IPRangeFilterSet
  76. parent_model = IPRange # AvailableIPsMixin
  77. class IPAddressViewSet(NetBoxModelViewSet):
  78. queryset = IPAddress.objects.prefetch_related(
  79. GenericPrefetch(
  80. "assigned_object",
  81. [
  82. # serializers are taken according to IPADDRESS_ASSIGNMENT_MODELS
  83. FHRPGroup.objects.all(),
  84. Interface.objects.select_related("cable", "device"),
  85. VMInterface.objects.select_related("virtual_machine"),
  86. ],
  87. ),
  88. )
  89. serializer_class = serializers.IPAddressSerializer
  90. filterset_class = filtersets.IPAddressFilterSet
  91. @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
  92. def create(self, request, *args, **kwargs):
  93. return super().create(request, *args, **kwargs)
  94. @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
  95. def update(self, request, *args, **kwargs):
  96. return super().update(request, *args, **kwargs)
  97. @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
  98. def destroy(self, request, *args, **kwargs):
  99. return super().destroy(request, *args, **kwargs)
  100. class FHRPGroupViewSet(NetBoxModelViewSet):
  101. queryset = FHRPGroup.objects.all()
  102. serializer_class = serializers.FHRPGroupSerializer
  103. filterset_class = filtersets.FHRPGroupFilterSet
  104. class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
  105. queryset = FHRPGroupAssignment.objects.all()
  106. serializer_class = serializers.FHRPGroupAssignmentSerializer
  107. filterset_class = filtersets.FHRPGroupAssignmentFilterSet
  108. class VLANGroupViewSet(NetBoxModelViewSet):
  109. queryset = VLANGroup.objects.annotate_utilization()
  110. serializer_class = serializers.VLANGroupSerializer
  111. filterset_class = filtersets.VLANGroupFilterSet
  112. class VLANViewSet(NetBoxModelViewSet):
  113. queryset = VLAN.objects.prefetch_related(
  114. 'l2vpn_terminations', # Referenced by VLANSerializer.l2vpn_termination
  115. )
  116. serializer_class = serializers.VLANSerializer
  117. filterset_class = filtersets.VLANFilterSet
  118. class VLANTranslationPolicyViewSet(NetBoxModelViewSet):
  119. queryset = VLANTranslationPolicy.objects.all()
  120. serializer_class = serializers.VLANTranslationPolicySerializer
  121. filterset_class = filtersets.VLANTranslationPolicyFilterSet
  122. class VLANTranslationRuleViewSet(NetBoxModelViewSet):
  123. queryset = VLANTranslationRule.objects.all()
  124. serializer_class = serializers.VLANTranslationRuleSerializer
  125. filterset_class = filtersets.VLANTranslationRuleFilterSet
  126. class ServiceTemplateViewSet(NetBoxModelViewSet):
  127. queryset = ServiceTemplate.objects.all()
  128. serializer_class = serializers.ServiceTemplateSerializer
  129. filterset_class = filtersets.ServiceTemplateFilterSet
  130. class ServiceViewSet(NetBoxModelViewSet):
  131. queryset = Service.objects.all()
  132. serializer_class = serializers.ServiceSerializer
  133. filterset_class = filtersets.ServiceFilterSet
  134. #
  135. # Views
  136. #
  137. def get_results_limit(request):
  138. """
  139. Return the lesser of the specified limit (if any) and the configured MAX_PAGE_SIZE.
  140. """
  141. config = get_config()
  142. try:
  143. limit = int(request.query_params.get('limit', config.PAGINATE_COUNT)) or config.MAX_PAGE_SIZE
  144. except ValueError:
  145. limit = config.PAGINATE_COUNT
  146. if config.MAX_PAGE_SIZE:
  147. limit = min(limit, config.MAX_PAGE_SIZE)
  148. return limit
  149. class AvailableObjectsView(ObjectValidationMixin, APIView):
  150. """
  151. Return a list of dicts representing child objects that have not yet been created for a parent object.
  152. """
  153. read_serializer_class = None
  154. write_serializer_class = None
  155. advisory_lock_key = None
  156. def get_parent(self, request, pk):
  157. """
  158. Return the parent object.
  159. """
  160. raise NotImplementedError()
  161. def get_available_objects(self, parent, limit=None):
  162. """
  163. Return all available objects for the parent.
  164. """
  165. raise NotImplementedError()
  166. def get_extra_context(self, parent):
  167. """
  168. Return any extra context data for the serializer.
  169. """
  170. return {}
  171. def check_sufficient_available(self, requested_objects, available_objects):
  172. """
  173. Check if there exist a sufficient number of available objects to satisfy the request.
  174. """
  175. return len(requested_objects) <= len(available_objects)
  176. def prep_object_data(self, requested_objects, available_objects, parent):
  177. """
  178. Prepare data by setting any programmatically determined object attributes (e.g. next available VLAN ID)
  179. on the request data.
  180. """
  181. return requested_objects
  182. def get(self, request, pk):
  183. parent = self.get_parent(request, pk)
  184. limit = get_results_limit(request)
  185. available_objects = self.get_available_objects(parent, limit)
  186. serializer = self.read_serializer_class(available_objects, many=True, context={
  187. 'request': request,
  188. **self.get_extra_context(parent),
  189. })
  190. return Response(serializer.data)
  191. def post(self, request, pk):
  192. self.queryset = self.queryset.restrict(request.user, 'add')
  193. parent = self.get_parent(request, pk)
  194. # Normalize request data to a list of objects
  195. requested_objects = request.data if isinstance(request.data, list) else [request.data]
  196. limit = len(requested_objects)
  197. # Serialize and validate the request data
  198. serializer = self.write_serializer_class(data=requested_objects, many=True, context={
  199. 'request': request,
  200. **self.get_extra_context(parent),
  201. })
  202. if not serializer.is_valid():
  203. return Response(
  204. serializer.errors,
  205. status=status.HTTP_400_BAD_REQUEST
  206. )
  207. with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]):
  208. available_objects = self.get_available_objects(parent, limit)
  209. # Determine if the requested number of objects is available
  210. if not self.check_sufficient_available(serializer.validated_data, available_objects):
  211. return Response(
  212. {"detail": "Insufficient resources are available to satisfy the request"},
  213. status=status.HTTP_409_CONFLICT
  214. )
  215. # Prepare object data for deserialization
  216. requested_objects = self.prep_object_data(deepcopy(requested_objects), available_objects, parent)
  217. # Initialize the serializer with a list or a single object depending on what was requested
  218. serializer_class = get_serializer_for_model(self.queryset.model)
  219. context = {'request': request}
  220. if isinstance(request.data, list):
  221. serializer = serializer_class(data=requested_objects, many=True, context=context)
  222. else:
  223. serializer = serializer_class(data=requested_objects[0], context=context)
  224. if not serializer.is_valid():
  225. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  226. # Create the new IP address(es)
  227. try:
  228. with transaction.atomic(using=router.db_for_write(self.queryset.model)):
  229. created = serializer.save()
  230. self._validate_objects(created)
  231. except ObjectDoesNotExist:
  232. raise PermissionDenied()
  233. return Response(serializer.data, status=status.HTTP_201_CREATED)
  234. class AvailableASNsView(AvailableObjectsView):
  235. queryset = ASN.objects.all()
  236. read_serializer_class = serializers.AvailableASNSerializer
  237. write_serializer_class = serializers.AvailableASNSerializer
  238. advisory_lock_key = 'available-asns'
  239. def get_parent(self, request, pk):
  240. return get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
  241. def get_available_objects(self, parent, limit=None):
  242. return parent.get_available_asns()[:limit]
  243. def get_extra_context(self, parent):
  244. return {
  245. 'range': parent,
  246. }
  247. def prep_object_data(self, requested_objects, available_objects, parent):
  248. for i, request_data in enumerate(requested_objects):
  249. request_data.update({
  250. 'rir': parent.rir.pk,
  251. 'range': parent.pk,
  252. 'asn': available_objects[i],
  253. })
  254. return requested_objects
  255. @extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
  256. def get(self, request, pk):
  257. return super().get(request, pk)
  258. @extend_schema(
  259. methods=["post"],
  260. responses={201: serializers.ASNSerializer(many=True)},
  261. request=serializers.ASNSerializer(many=True),
  262. )
  263. def post(self, request, pk):
  264. return super().post(request, pk)
  265. class AvailablePrefixesView(AvailableObjectsView):
  266. queryset = Prefix.objects.all()
  267. read_serializer_class = serializers.AvailablePrefixSerializer
  268. write_serializer_class = serializers.PrefixLengthSerializer
  269. advisory_lock_key = 'available-prefixes'
  270. def get_parent(self, request, pk):
  271. return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
  272. def get_available_objects(self, parent, limit=None):
  273. return parent.get_available_prefixes().iter_cidrs()
  274. def check_sufficient_available(self, requested_objects, available_objects):
  275. available_prefixes = IPSet(available_objects)
  276. for requested_object in requested_objects:
  277. if not get_next_available_prefix(available_prefixes, requested_object['prefix_length']):
  278. return False
  279. return True
  280. def get_extra_context(self, parent):
  281. return {
  282. 'prefix': parent,
  283. 'vrf': parent.vrf,
  284. }
  285. def prep_object_data(self, requested_objects, available_objects, parent):
  286. available_prefixes = IPSet(available_objects)
  287. for i, request_data in enumerate(requested_objects):
  288. # Find the first available prefix equal to or larger than the requested size
  289. if allocated_prefix := get_next_available_prefix(available_prefixes, request_data['prefix_length']):
  290. request_data.update({
  291. 'prefix': allocated_prefix,
  292. 'vrf': parent.vrf.pk if parent.vrf else None,
  293. })
  294. else:
  295. raise ValidationError(_("Insufficient space is available to accommodate the requested prefix size(s)"))
  296. return requested_objects
  297. @extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
  298. def get(self, request, pk):
  299. return super().get(request, pk)
  300. @extend_schema(
  301. methods=["post"],
  302. responses={201: serializers.PrefixSerializer(many=True)},
  303. request=serializers.PrefixSerializer(many=True),
  304. )
  305. def post(self, request, pk):
  306. return super().post(request, pk)
  307. class AvailableIPAddressesView(AvailableObjectsView):
  308. queryset = IPAddress.objects.all()
  309. read_serializer_class = serializers.AvailableIPSerializer
  310. write_serializer_class = serializers.AvailableIPSerializer
  311. advisory_lock_key = 'available-ips'
  312. def get_available_objects(self, parent, limit=None):
  313. # Calculate available IPs within the parent
  314. ip_list = []
  315. for index, ip in enumerate(parent.get_available_ips(), start=1):
  316. ip_list.append(ip)
  317. if index == limit:
  318. break
  319. return ip_list
  320. def get_extra_context(self, parent):
  321. return {
  322. 'parent': parent,
  323. 'vrf': parent.vrf,
  324. }
  325. def prep_object_data(self, requested_objects, available_objects, parent):
  326. available_ips = iter(available_objects)
  327. for i, request_data in enumerate(requested_objects):
  328. request_data.update({
  329. 'address': f'{next(available_ips)}/{parent.mask_length}',
  330. 'vrf': parent.vrf.pk if parent.vrf else None,
  331. })
  332. return requested_objects
  333. @extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
  334. def get(self, request, pk):
  335. return super().get(request, pk)
  336. @extend_schema(
  337. methods=["post"],
  338. responses={201: serializers.IPAddressSerializer(many=True)},
  339. request=serializers.IPAddressSerializer(many=True),
  340. )
  341. def post(self, request, pk):
  342. return super().post(request, pk)
  343. class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
  344. def get_parent(self, request, pk):
  345. return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
  346. class IPRangeAvailableIPAddressesView(AvailableIPAddressesView):
  347. def get_parent(self, request, pk):
  348. return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk)
  349. class AvailableVLANsView(AvailableObjectsView):
  350. queryset = VLAN.objects.all()
  351. read_serializer_class = serializers.AvailableVLANSerializer
  352. write_serializer_class = serializers.CreateAvailableVLANSerializer
  353. advisory_lock_key = 'available-vlans'
  354. def get_parent(self, request, pk):
  355. return get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
  356. def get_available_objects(self, parent, limit=None):
  357. return parent.get_available_vids()[:limit]
  358. def get_extra_context(self, parent):
  359. return {
  360. 'group': parent,
  361. }
  362. def prep_object_data(self, requested_objects, available_objects, parent):
  363. for i, request_data in enumerate(requested_objects):
  364. request_data.update({
  365. 'vid': available_objects.pop(0),
  366. 'group': parent.pk,
  367. })
  368. return requested_objects
  369. @extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
  370. def get(self, request, pk):
  371. return super().get(request, pk)
  372. @extend_schema(
  373. methods=["post"],
  374. responses={201: serializers.VLANSerializer(many=True)},
  375. request=serializers.VLANSerializer(many=True),
  376. )
  377. def post(self, request, pk):
  378. return super().post(request, pk)