views.py 18 KB

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