views.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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, discard_events_on_rollback
  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. using = router.db_for_write(self.queryset.model)
  228. try:
  229. with transaction.atomic(using=using), discard_events_on_rollback(self, using=using):
  230. created = serializer.save()
  231. self._validate_objects(created)
  232. except ObjectDoesNotExist:
  233. raise PermissionDenied()
  234. return Response(serializer.data, status=status.HTTP_201_CREATED)
  235. class AvailableASNsView(AvailableObjectsView):
  236. queryset = ASN.objects.all()
  237. read_serializer_class = serializers.AvailableASNSerializer
  238. write_serializer_class = serializers.AvailableASNSerializer
  239. advisory_lock_key = 'available-asns'
  240. def get_parent(self, request, pk):
  241. return get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
  242. def get_available_objects(self, parent, limit=None):
  243. return parent.get_available_asns()[:limit]
  244. def get_extra_context(self, parent):
  245. return {
  246. 'range': parent,
  247. }
  248. def prep_object_data(self, requested_objects, available_objects, parent):
  249. for i, request_data in enumerate(requested_objects):
  250. request_data.update({
  251. 'rir': parent.rir.pk,
  252. 'range': parent.pk,
  253. 'asn': available_objects[i],
  254. })
  255. return requested_objects
  256. @extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
  257. def get(self, request, pk):
  258. return super().get(request, pk)
  259. @extend_schema(
  260. methods=["post"],
  261. responses={201: serializers.ASNSerializer(many=True)},
  262. request=serializers.ASNSerializer(many=True),
  263. )
  264. def post(self, request, pk):
  265. return super().post(request, pk)
  266. class AvailablePrefixesView(AvailableObjectsView):
  267. queryset = Prefix.objects.all()
  268. read_serializer_class = serializers.AvailablePrefixSerializer
  269. write_serializer_class = serializers.PrefixLengthSerializer
  270. advisory_lock_key = 'available-prefixes'
  271. def get_parent(self, request, pk):
  272. return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
  273. def get_available_objects(self, parent, limit=None):
  274. return parent.get_available_prefixes().iter_cidrs()
  275. def check_sufficient_available(self, requested_objects, available_objects):
  276. available_prefixes = IPSet(available_objects)
  277. for requested_object in requested_objects:
  278. if not get_next_available_prefix(available_prefixes, requested_object['prefix_length']):
  279. return False
  280. return True
  281. def get_extra_context(self, parent):
  282. return {
  283. 'prefix': parent,
  284. 'vrf': parent.vrf,
  285. }
  286. def prep_object_data(self, requested_objects, available_objects, parent):
  287. available_prefixes = IPSet(available_objects)
  288. for i, request_data in enumerate(requested_objects):
  289. # Find the first available prefix equal to or larger than the requested size
  290. if allocated_prefix := get_next_available_prefix(available_prefixes, request_data['prefix_length']):
  291. request_data.update({
  292. 'prefix': allocated_prefix,
  293. 'vrf': parent.vrf.pk if parent.vrf else None,
  294. })
  295. else:
  296. raise ValidationError(_("Insufficient space is available to accommodate the requested prefix size(s)"))
  297. return requested_objects
  298. @extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
  299. def get(self, request, pk):
  300. return super().get(request, pk)
  301. @extend_schema(
  302. methods=["post"],
  303. responses={201: serializers.PrefixSerializer(many=True)},
  304. request=serializers.CreateAvailablePrefixSerializer(many=True),
  305. )
  306. def post(self, request, pk):
  307. return super().post(request, pk)
  308. class AvailableIPAddressesView(AvailableObjectsView):
  309. queryset = IPAddress.objects.all()
  310. read_serializer_class = serializers.AvailableIPSerializer
  311. write_serializer_class = serializers.AvailableIPRequestSerializer
  312. advisory_lock_key = 'available-ips'
  313. def get_available_objects(self, parent, limit=None):
  314. # Calculate available IPs within the parent
  315. ip_list = []
  316. for index, ip in enumerate(parent.iter_available_ips(), start=1):
  317. ip_list.append(ip)
  318. if index == limit:
  319. break
  320. return ip_list
  321. def get_extra_context(self, parent):
  322. return {
  323. 'parent': parent,
  324. 'vrf': parent.vrf,
  325. }
  326. def prep_object_data(self, requested_objects, available_objects, parent):
  327. available_ips = iter(available_objects)
  328. for i, request_data in enumerate(requested_objects):
  329. prefix_length = request_data.pop('prefix_length', None) or parent.mask_length
  330. request_data.update({
  331. 'address': f'{next(available_ips)}/{prefix_length}',
  332. 'vrf': parent.vrf.pk if parent.vrf else None,
  333. })
  334. return requested_objects
  335. @extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
  336. def get(self, request, pk):
  337. return super().get(request, pk)
  338. @extend_schema(
  339. methods=["post"],
  340. responses={201: serializers.IPAddressSerializer(many=True)},
  341. request=serializers.AvailableIPRequestSerializer(many=True),
  342. )
  343. def post(self, request, pk):
  344. return super().post(request, pk)
  345. class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
  346. def get_parent(self, request, pk):
  347. return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
  348. class IPRangeAvailableIPAddressesView(AvailableIPAddressesView):
  349. def get_parent(self, request, pk):
  350. return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk)
  351. class AvailableVLANsView(AvailableObjectsView):
  352. queryset = VLAN.objects.all()
  353. read_serializer_class = serializers.AvailableVLANSerializer
  354. write_serializer_class = serializers.CreateAvailableVLANSerializer
  355. advisory_lock_key = 'available-vlans'
  356. def get_parent(self, request, pk):
  357. return get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
  358. def get_available_objects(self, parent, limit=None):
  359. return parent.get_available_vids()[:limit]
  360. def get_extra_context(self, parent):
  361. return {
  362. 'group': parent,
  363. }
  364. def prep_object_data(self, requested_objects, available_objects, parent):
  365. for i, request_data in enumerate(requested_objects):
  366. request_data.update({
  367. 'vid': available_objects.pop(0),
  368. 'group': parent.pk,
  369. })
  370. return requested_objects
  371. @extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
  372. def get(self, request, pk):
  373. return super().get(request, pk)
  374. @extend_schema(
  375. methods=["post"],
  376. responses={201: serializers.VLANSerializer(many=True)},
  377. request=serializers.CreateAvailableVLANSerializer(many=True),
  378. )
  379. def post(self, request, pk):
  380. return super().post(request, pk)