views.py 10 KB

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