views.py 12 KB

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