views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. from django.contrib.contenttypes.models import ContentType
  2. from django.http import Http404
  3. from django_rq.queues import get_connection
  4. from rest_framework import status
  5. from rest_framework.decorators import action
  6. from rest_framework.exceptions import PermissionDenied
  7. from rest_framework.response import Response
  8. from rest_framework.routers import APIRootView
  9. from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
  10. from rq import Worker
  11. from extras import filters
  12. from extras.choices import JobResultStatusChoices
  13. from extras.models import (
  14. ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem,
  15. )
  16. from extras.models import CustomField
  17. from extras.reports import get_report, get_reports, run_report
  18. from extras.scripts import get_script, get_scripts, run_script
  19. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  20. from netbox.api.metadata import ContentTypeMetadata
  21. from netbox.api.views import ModelViewSet
  22. from utilities.exceptions import RQWorkerNotRunningException
  23. from utilities.utils import copy_safe_request, count_related
  24. from . import serializers
  25. class ExtrasRootView(APIRootView):
  26. """
  27. Extras API root view
  28. """
  29. def get_view_name(self):
  30. return 'Extras'
  31. class ConfigContextQuerySetMixin:
  32. """
  33. Used by views that work with config context models (device and virtual machine).
  34. Provides a get_queryset() method which deals with adding the config context
  35. data annotation or not.
  36. """
  37. def get_queryset(self):
  38. """
  39. Build the proper queryset based on the request context
  40. If the `brief` query param equates to True or the `exclude` query param
  41. includes `config_context` as a value, return the base queryset.
  42. Else, return the queryset annotated with config context data
  43. """
  44. request = self.get_serializer_context()['request']
  45. if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []):
  46. return self.queryset
  47. return self.queryset.annotate_config_context_data()
  48. #
  49. # Custom fields
  50. #
  51. class CustomFieldViewSet(ModelViewSet):
  52. metadata_class = ContentTypeMetadata
  53. queryset = CustomField.objects.all()
  54. serializer_class = serializers.CustomFieldSerializer
  55. filterset_class = filters.CustomFieldFilterSet
  56. class CustomFieldModelViewSet(ModelViewSet):
  57. """
  58. Include the applicable set of CustomFields in the ModelViewSet context.
  59. """
  60. def get_serializer_context(self):
  61. # Gather all custom fields for the model
  62. content_type = ContentType.objects.get_for_model(self.queryset.model)
  63. custom_fields = content_type.custom_fields.all()
  64. context = super().get_serializer_context()
  65. context.update({
  66. 'custom_fields': custom_fields,
  67. })
  68. return context
  69. #
  70. # Export templates
  71. #
  72. class ExportTemplateViewSet(ModelViewSet):
  73. metadata_class = ContentTypeMetadata
  74. queryset = ExportTemplate.objects.all()
  75. serializer_class = serializers.ExportTemplateSerializer
  76. filterset_class = filters.ExportTemplateFilterSet
  77. #
  78. # Tags
  79. #
  80. class TagViewSet(ModelViewSet):
  81. queryset = Tag.objects.annotate(
  82. tagged_items=count_related(TaggedItem, 'tag')
  83. )
  84. serializer_class = serializers.TagSerializer
  85. filterset_class = filters.TagFilterSet
  86. #
  87. # Image attachments
  88. #
  89. class ImageAttachmentViewSet(ModelViewSet):
  90. metadata_class = ContentTypeMetadata
  91. queryset = ImageAttachment.objects.all()
  92. serializer_class = serializers.ImageAttachmentSerializer
  93. filterset_class = filters.ImageAttachmentFilterSet
  94. #
  95. # Config contexts
  96. #
  97. class ConfigContextViewSet(ModelViewSet):
  98. queryset = ConfigContext.objects.prefetch_related(
  99. 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
  100. )
  101. serializer_class = serializers.ConfigContextSerializer
  102. filterset_class = filters.ConfigContextFilterSet
  103. #
  104. # Reports
  105. #
  106. class ReportViewSet(ViewSet):
  107. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  108. _ignore_model_permissions = True
  109. exclude_from_schema = True
  110. lookup_value_regex = '[^/]+' # Allow dots
  111. def _retrieve_report(self, pk):
  112. # Read the PK as "<module>.<report>"
  113. if '.' not in pk:
  114. raise Http404
  115. module_name, report_name = pk.split('.', 1)
  116. # Raise a 404 on an invalid Report module/name
  117. report = get_report(module_name, report_name)
  118. if report is None:
  119. raise Http404
  120. return report
  121. def list(self, request):
  122. """
  123. Compile all reports and their related results (if any). Result data is deferred in the list view.
  124. """
  125. report_list = []
  126. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  127. results = {
  128. r.name: r
  129. for r in JobResult.objects.filter(
  130. obj_type=report_content_type,
  131. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  132. ).defer('data')
  133. }
  134. # Iterate through all available Reports.
  135. for module_name, reports in get_reports():
  136. for report in reports:
  137. # Attach the relevant JobResult (if any) to each Report.
  138. report.result = results.get(report.full_name, None)
  139. report_list.append(report)
  140. serializer = serializers.ReportSerializer(report_list, many=True, context={
  141. 'request': request,
  142. })
  143. return Response(serializer.data)
  144. def retrieve(self, request, pk):
  145. """
  146. Retrieve a single Report identified as "<module>.<report>".
  147. """
  148. # Retrieve the Report and JobResult, if any.
  149. report = self._retrieve_report(pk)
  150. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  151. report.result = JobResult.objects.filter(
  152. obj_type=report_content_type,
  153. name=report.full_name,
  154. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  155. ).first()
  156. serializer = serializers.ReportDetailSerializer(report, context={
  157. 'request': request
  158. })
  159. return Response(serializer.data)
  160. @action(detail=True, methods=['post'])
  161. def run(self, request, pk):
  162. """
  163. Run a Report identified as "<module>.<script>" and return the pending JobResult as the result
  164. """
  165. # Check that the user has permission to run reports.
  166. if not request.user.has_perm('extras.run_script'):
  167. raise PermissionDenied("This user does not have permission to run reports.")
  168. # Check that at least one RQ worker is running
  169. if not Worker.count(get_connection('default')):
  170. raise RQWorkerNotRunningException()
  171. # Retrieve and run the Report. This will create a new JobResult.
  172. report = self._retrieve_report(pk)
  173. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  174. job_result = JobResult.enqueue_job(
  175. run_report,
  176. report.full_name,
  177. report_content_type,
  178. request.user
  179. )
  180. report.result = job_result
  181. serializer = serializers.ReportDetailSerializer(report, context={'request': request})
  182. return Response(serializer.data)
  183. #
  184. # Scripts
  185. #
  186. class ScriptViewSet(ViewSet):
  187. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  188. _ignore_model_permissions = True
  189. exclude_from_schema = True
  190. lookup_value_regex = '[^/]+' # Allow dots
  191. def _get_script(self, pk):
  192. module_name, script_name = pk.split('.')
  193. script = get_script(module_name, script_name)
  194. if script is None:
  195. raise Http404
  196. return script
  197. def list(self, request):
  198. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  199. results = {
  200. r.name: r
  201. for r in JobResult.objects.filter(
  202. obj_type=script_content_type,
  203. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  204. ).defer('data').order_by('created')
  205. }
  206. flat_list = []
  207. for script_list in get_scripts().values():
  208. flat_list.extend(script_list.values())
  209. # Attach JobResult objects to each script (if any)
  210. for script in flat_list:
  211. script.result = results.get(script.full_name, None)
  212. serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
  213. return Response(serializer.data)
  214. def retrieve(self, request, pk):
  215. script = self._get_script(pk)
  216. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  217. script.result = JobResult.objects.filter(
  218. obj_type=script_content_type,
  219. name=script.full_name,
  220. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  221. ).first()
  222. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  223. return Response(serializer.data)
  224. def post(self, request, pk):
  225. """
  226. Run a Script identified as "<module>.<script>" and return the pending JobResult as the result
  227. """
  228. script = self._get_script(pk)()
  229. input_serializer = serializers.ScriptInputSerializer(data=request.data)
  230. # Check that at least one RQ worker is running
  231. if not Worker.count(get_connection('default')):
  232. raise RQWorkerNotRunningException()
  233. if input_serializer.is_valid():
  234. data = input_serializer.data['data']
  235. commit = input_serializer.data['commit']
  236. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  237. job_result = JobResult.enqueue_job(
  238. run_script,
  239. script.full_name,
  240. script_content_type,
  241. request.user,
  242. data=data,
  243. request=copy_safe_request(request),
  244. commit=commit
  245. )
  246. script.result = job_result
  247. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  248. return Response(serializer.data)
  249. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  250. #
  251. # Change logging
  252. #
  253. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  254. """
  255. Retrieve a list of recent changes.
  256. """
  257. metadata_class = ContentTypeMetadata
  258. queryset = ObjectChange.objects.prefetch_related('user')
  259. serializer_class = serializers.ObjectChangeSerializer
  260. filterset_class = filters.ObjectChangeFilterSet
  261. #
  262. # Job Results
  263. #
  264. class JobResultViewSet(ReadOnlyModelViewSet):
  265. """
  266. Retrieve a list of job results
  267. """
  268. queryset = JobResult.objects.prefetch_related('user')
  269. serializer_class = serializers.JobResultSerializer
  270. filterset_class = filters.JobResultFilterSet
  271. #
  272. # ContentTypes
  273. #
  274. class ContentTypeViewSet(ReadOnlyModelViewSet):
  275. """
  276. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  277. """
  278. queryset = ContentType.objects.order_by('app_label', 'model')
  279. serializer_class = serializers.ContentTypeSerializer
  280. filterset_class = filters.ContentTypeFilterSet