views.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. from django.http import Http404, HttpResponse
  2. from django.shortcuts import get_object_or_404
  3. from django.utils.translation import gettext_lazy as _
  4. from django_rq.queues import get_redis_connection
  5. from django_rq.settings import QUEUES_LIST
  6. from django_rq.utils import get_statistics
  7. from drf_spectacular.types import OpenApiTypes
  8. from drf_spectacular.utils import OpenApiParameter, extend_schema
  9. from rest_framework import viewsets
  10. from rest_framework.decorators import action
  11. from rest_framework.exceptions import PermissionDenied
  12. from rest_framework.response import Response
  13. from rest_framework.routers import APIRootView
  14. from rq.job import Job as RQ_Job
  15. from rq.worker import Worker
  16. from core import filtersets
  17. from core.jobs import SyncDataSourceJob
  18. from core.models import *
  19. from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
  20. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  21. from netbox.api.metadata import ContentTypeMetadata
  22. from netbox.api.pagination import LimitOffsetListPagination
  23. from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
  24. from utilities.api import IsSuperuser
  25. from . import serializers
  26. class CoreRootView(APIRootView):
  27. """
  28. Core API root view
  29. """
  30. def get_view_name(self):
  31. return 'Core'
  32. class DataSourceViewSet(NetBoxModelViewSet):
  33. queryset = DataSource.objects.all()
  34. serializer_class = serializers.DataSourceSerializer
  35. filterset_class = filtersets.DataSourceFilterSet
  36. @action(detail=True, methods=['post'])
  37. def sync(self, request, pk):
  38. """
  39. Enqueue a job to synchronize the DataSource.
  40. """
  41. datasource = get_object_or_404(DataSource, pk=pk)
  42. if not request.user.has_perm('core.sync_datasource', obj=datasource):
  43. raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
  44. # Enqueue the sync job
  45. SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
  46. serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
  47. return Response(serializer.data)
  48. class DataFileViewSet(NetBoxReadOnlyModelViewSet):
  49. queryset = DataFile.objects.defer('data')
  50. serializer_class = serializers.DataFileSerializer
  51. filterset_class = filtersets.DataFileFilterSet
  52. class JobViewSet(NetBoxReadOnlyModelViewSet):
  53. """
  54. Retrieve a list of job results
  55. """
  56. queryset = Job.objects.all()
  57. serializer_class = serializers.JobSerializer
  58. filterset_class = filtersets.JobFilterSet
  59. class ObjectChangeViewSet(NetBoxReadOnlyModelViewSet):
  60. """
  61. Retrieve a list of recent changes.
  62. """
  63. metadata_class = ContentTypeMetadata
  64. queryset = ObjectChange.objects.all()
  65. serializer_class = serializers.ObjectChangeSerializer
  66. filterset_class = filtersets.ObjectChangeFilterSet
  67. def get_queryset(self):
  68. return super().get_queryset().valid_models()
  69. class ObjectTypeViewSet(NetBoxReadOnlyModelViewSet):
  70. """
  71. Read-only list of ObjectTypes.
  72. """
  73. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  74. queryset = ObjectType.objects.order_by('app_label', 'model')
  75. serializer_class = serializers.ObjectTypeSerializer
  76. filterset_class = filtersets.ObjectTypeFilterSet
  77. def initial(self, request, *args, **kwargs):
  78. """
  79. Override initial() to skip the restrict() call since ObjectType (a ContentType proxy)
  80. doesn't use RestrictedQuerySet and is publicly accessible metadata.
  81. """
  82. # Call GenericViewSet.initial() directly, skipping BaseViewSet.initial()
  83. # which would try to call restrict() on the queryset
  84. from rest_framework.viewsets import GenericViewSet
  85. GenericViewSet.initial(self, request, *args, **kwargs)
  86. class BaseRQViewSet(viewsets.ViewSet):
  87. """
  88. Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
  89. """
  90. permission_classes = [IsSuperuser]
  91. serializer_class = None
  92. def get_data(self):
  93. raise NotImplementedError()
  94. @extend_schema(responses={200: OpenApiTypes.OBJECT})
  95. def list(self, request):
  96. data = self.get_data()
  97. paginator = LimitOffsetListPagination()
  98. data = paginator.paginate_list(data, request)
  99. serializer = self.serializer_class(data, many=True, context={'request': request})
  100. return paginator.get_paginated_response(serializer.data)
  101. def get_serializer(self, *args, **kwargs):
  102. """
  103. Return the serializer instance that should be used for validating and
  104. deserializing input and for serializing output.
  105. """
  106. serializer_class = self.get_serializer_class()
  107. kwargs['context'] = self.get_serializer_context()
  108. return serializer_class(*args, **kwargs)
  109. def get_serializer_class(self):
  110. """
  111. Return the class to use for the serializer.
  112. """
  113. return self.serializer_class
  114. def get_serializer_context(self):
  115. """
  116. Extra context provided to the serializer class.
  117. """
  118. return {
  119. 'request': self.request,
  120. 'format': self.format_kwarg,
  121. 'view': self,
  122. }
  123. class BackgroundQueueViewSet(BaseRQViewSet):
  124. """
  125. Retrieve a list of RQ Queues.
  126. Note: Queue names are not URL safe, so not returning a detail view.
  127. """
  128. serializer_class = serializers.BackgroundQueueSerializer
  129. lookup_field = 'name'
  130. lookup_value_regex = r'[\w.@+-]+'
  131. def get_view_name(self):
  132. return 'Background Queues'
  133. def get_data(self):
  134. return get_statistics(run_maintenance_tasks=True)['queues']
  135. @extend_schema(
  136. operation_id='core_background_queues_retrieve_by_name',
  137. parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
  138. responses={200: OpenApiTypes.OBJECT},
  139. )
  140. def retrieve(self, request, name):
  141. data = self.get_data()
  142. if not data:
  143. raise Http404
  144. for queue in data:
  145. if queue['name'] == name:
  146. serializer = self.serializer_class(queue, context={'request': request})
  147. return Response(serializer.data)
  148. raise Http404
  149. class BackgroundWorkerViewSet(BaseRQViewSet):
  150. """
  151. Retrieve a list of RQ Workers.
  152. """
  153. serializer_class = serializers.BackgroundWorkerSerializer
  154. lookup_field = 'name'
  155. def get_view_name(self):
  156. return 'Background Workers'
  157. def get_data(self):
  158. config = QUEUES_LIST[0]
  159. return Worker.all(get_redis_connection(config['connection_config']))
  160. @extend_schema(
  161. operation_id='core_background_workers_retrieve_by_name',
  162. parameters=[OpenApiParameter(name='name', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
  163. responses={200: OpenApiTypes.OBJECT},
  164. )
  165. def retrieve(self, request, name):
  166. # all the RQ queues should use the same connection
  167. config = QUEUES_LIST[0]
  168. workers = Worker.all(get_redis_connection(config['connection_config']))
  169. worker = next((item for item in workers if item.name == name), None)
  170. if not worker:
  171. raise Http404
  172. serializer = serializers.BackgroundWorkerSerializer(worker, context={'request': request})
  173. return Response(serializer.data)
  174. class BackgroundTaskViewSet(BaseRQViewSet):
  175. """
  176. Retrieve a list of RQ Tasks.
  177. """
  178. serializer_class = serializers.BackgroundTaskSerializer
  179. lookup_field = 'id'
  180. def get_view_name(self):
  181. return 'Background Tasks'
  182. def get_data(self):
  183. return get_rq_jobs()
  184. def get_task_from_id(self, task_id):
  185. config = QUEUES_LIST[0]
  186. task = RQ_Job.fetch(task_id, connection=get_redis_connection(config['connection_config']))
  187. if not task:
  188. raise Http404
  189. return task
  190. @extend_schema(
  191. operation_id='core_background_tasks_retrieve_by_id',
  192. parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)],
  193. responses={200: OpenApiTypes.OBJECT},
  194. )
  195. def retrieve(self, request, id):
  196. """
  197. Retrieve the details of the specified RQ Task.
  198. """
  199. task = self.get_task_from_id(id)
  200. serializer = self.serializer_class(task, context={'request': request})
  201. return Response(serializer.data)
  202. @extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
  203. @action(methods=['POST'], detail=True)
  204. def delete(self, request, id):
  205. """
  206. Delete the specified RQ Task.
  207. """
  208. delete_rq_job(id)
  209. return HttpResponse(status=200)
  210. @extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
  211. @action(methods=['POST'], detail=True)
  212. def requeue(self, request, id):
  213. """
  214. Requeues the specified RQ Task.
  215. """
  216. requeue_rq_job(id)
  217. return HttpResponse(status=200)
  218. @extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
  219. @action(methods=['POST'], detail=True)
  220. def enqueue(self, request, id):
  221. """
  222. Enqueues the specified RQ Task.
  223. """
  224. enqueue_rq_job(id)
  225. return HttpResponse(status=200)
  226. @extend_schema(parameters=[OpenApiParameter(name='id', type=OpenApiTypes.STR, location=OpenApiParameter.PATH)])
  227. @action(methods=['POST'], detail=True)
  228. def stop(self, request, id):
  229. """
  230. Stops the specified RQ Task.
  231. """
  232. stopped_jobs = stop_rq_job(id)
  233. if len(stopped_jobs) == 1:
  234. return HttpResponse(status=200)
  235. else:
  236. return HttpResponse(status=204)