views.py 11 KB

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