views.py 18 KB

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