views.py 11 KB

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