views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. from django.contrib.contenttypes.models import ContentType
  2. from django.http import Http404
  3. from django.shortcuts import get_object_or_404
  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.generics import RetrieveUpdateDestroyAPIView
  9. from rest_framework.permissions import IsAuthenticated
  10. from rest_framework.renderers import JSONRenderer
  11. from rest_framework.response import Response
  12. from rest_framework.routers import APIRootView
  13. from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
  14. from rq import Worker
  15. from core.choices import JobStatusChoices
  16. from core.models import Job
  17. from extras import filtersets
  18. from extras.models import *
  19. from extras.reports import get_module_and_report, run_report
  20. from extras.scripts import get_module_and_script, run_script
  21. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  22. from netbox.api.features import SyncedDataMixin
  23. from netbox.api.metadata import ContentTypeMetadata
  24. from netbox.api.renderers import TextRenderer
  25. from netbox.api.viewsets import NetBoxModelViewSet
  26. from utilities.exceptions import RQWorkerNotRunningException
  27. from utilities.utils import copy_safe_request, count_related
  28. from . import serializers
  29. from .mixins import ConfigTemplateRenderMixin
  30. class ExtrasRootView(APIRootView):
  31. """
  32. Extras API root view
  33. """
  34. def get_view_name(self):
  35. return 'Extras'
  36. #
  37. # Webhooks
  38. #
  39. class WebhookViewSet(NetBoxModelViewSet):
  40. metadata_class = ContentTypeMetadata
  41. queryset = Webhook.objects.all()
  42. serializer_class = serializers.WebhookSerializer
  43. filterset_class = filtersets.WebhookFilterSet
  44. #
  45. # Custom fields
  46. #
  47. class CustomFieldViewSet(NetBoxModelViewSet):
  48. metadata_class = ContentTypeMetadata
  49. queryset = CustomField.objects.all()
  50. serializer_class = serializers.CustomFieldSerializer
  51. filterset_class = filtersets.CustomFieldFilterSet
  52. #
  53. # Custom links
  54. #
  55. class CustomLinkViewSet(NetBoxModelViewSet):
  56. metadata_class = ContentTypeMetadata
  57. queryset = CustomLink.objects.all()
  58. serializer_class = serializers.CustomLinkSerializer
  59. filterset_class = filtersets.CustomLinkFilterSet
  60. #
  61. # Export templates
  62. #
  63. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  64. metadata_class = ContentTypeMetadata
  65. queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
  66. serializer_class = serializers.ExportTemplateSerializer
  67. filterset_class = filtersets.ExportTemplateFilterSet
  68. #
  69. # Saved filters
  70. #
  71. class SavedFilterViewSet(NetBoxModelViewSet):
  72. metadata_class = ContentTypeMetadata
  73. queryset = SavedFilter.objects.all()
  74. serializer_class = serializers.SavedFilterSerializer
  75. filterset_class = filtersets.SavedFilterFilterSet
  76. #
  77. # Tags
  78. #
  79. class TagViewSet(NetBoxModelViewSet):
  80. queryset = Tag.objects.annotate(
  81. tagged_items=count_related(TaggedItem, 'tag')
  82. )
  83. serializer_class = serializers.TagSerializer
  84. filterset_class = filtersets.TagFilterSet
  85. #
  86. # Image attachments
  87. #
  88. class ImageAttachmentViewSet(NetBoxModelViewSet):
  89. metadata_class = ContentTypeMetadata
  90. queryset = ImageAttachment.objects.all()
  91. serializer_class = serializers.ImageAttachmentSerializer
  92. filterset_class = filtersets.ImageAttachmentFilterSet
  93. #
  94. # Journal entries
  95. #
  96. class JournalEntryViewSet(NetBoxModelViewSet):
  97. metadata_class = ContentTypeMetadata
  98. queryset = JournalEntry.objects.all()
  99. serializer_class = serializers.JournalEntrySerializer
  100. filterset_class = filtersets.JournalEntryFilterSet
  101. #
  102. # Config contexts
  103. #
  104. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  105. queryset = ConfigContext.objects.prefetch_related(
  106. 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
  107. 'data_file',
  108. )
  109. serializer_class = serializers.ConfigContextSerializer
  110. filterset_class = filtersets.ConfigContextFilterSet
  111. #
  112. # Config templates
  113. #
  114. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  115. queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
  116. serializer_class = serializers.ConfigTemplateSerializer
  117. filterset_class = filtersets.ConfigTemplateFilterSet
  118. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  119. def render(self, request, pk):
  120. """
  121. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  122. return the raw rendered content, rather than serialized JSON.
  123. """
  124. configtemplate = self.get_object()
  125. context = request.data
  126. return self.render_configtemplate(request, configtemplate, context)
  127. #
  128. # Reports
  129. #
  130. class ReportViewSet(ViewSet):
  131. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  132. _ignore_model_permissions = True
  133. schema = None
  134. lookup_value_regex = '[^/]+' # Allow dots
  135. def _get_report(self, pk):
  136. try:
  137. module_name, report_name = pk.split('.', maxsplit=1)
  138. except ValueError:
  139. raise Http404
  140. module, report = get_module_and_report(module_name, report_name)
  141. if report is None:
  142. raise Http404
  143. return module, 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_content_type = ContentType.objects.get(app_label='extras', model='report')
  149. results = {
  150. r.name: r
  151. for r in Job.objects.filter(
  152. object_type=report_content_type,
  153. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  154. ).order_by('name', '-created').distinct('name').defer('data')
  155. }
  156. report_list = []
  157. for report_module in ReportModule.objects.restrict(request.user):
  158. report_list.extend([report() for report in report_module.reports.values()])
  159. # Attach Job objects to each report (if any)
  160. for report in report_list:
  161. report.result = results.get(report.full_name, None)
  162. serializer = serializers.ReportSerializer(report_list, many=True, context={
  163. 'request': request,
  164. })
  165. return Response(serializer.data)
  166. def retrieve(self, request, pk):
  167. """
  168. Retrieve a single Report identified as "<module>.<report>".
  169. """
  170. module, report = self._get_report(pk)
  171. # Retrieve the Report and Job, if any.
  172. object_type = ContentType.objects.get(app_label='extras', model='reportmodule')
  173. report.result = Job.objects.filter(
  174. object_type=object_type,
  175. name=report.name,
  176. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  177. ).first()
  178. serializer = serializers.ReportDetailSerializer(report, context={
  179. 'request': request
  180. })
  181. return Response(serializer.data)
  182. @action(detail=True, methods=['post'])
  183. def run(self, request, pk):
  184. """
  185. Run a Report identified as "<module>.<script>" and return the pending Job as the result
  186. """
  187. # Check that the user has permission to run reports.
  188. if not request.user.has_perm('extras.run_report'):
  189. raise PermissionDenied("This user does not have permission to run reports.")
  190. # Check that at least one RQ worker is running
  191. if not Worker.count(get_connection('default')):
  192. raise RQWorkerNotRunningException()
  193. # Retrieve and run the Report. This will create a new Job.
  194. module, report = self._get_report(pk)
  195. input_serializer = serializers.ReportInputSerializer(data=request.data)
  196. if input_serializer.is_valid():
  197. report.result = Job.enqueue(
  198. run_report,
  199. instance=module,
  200. name=report.class_name,
  201. user=request.user,
  202. job_timeout=report.job_timeout,
  203. schedule_at=input_serializer.validated_data.get('schedule_at'),
  204. interval=input_serializer.validated_data.get('interval')
  205. )
  206. serializer = serializers.ReportDetailSerializer(report, context={'request': request})
  207. return Response(serializer.data)
  208. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  209. #
  210. # Scripts
  211. #
  212. class ScriptViewSet(ViewSet):
  213. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  214. _ignore_model_permissions = True
  215. schema = None
  216. lookup_value_regex = '[^/]+' # Allow dots
  217. def _get_script(self, pk):
  218. try:
  219. module_name, script_name = pk.split('.', maxsplit=1)
  220. except ValueError:
  221. raise Http404
  222. module, script = get_module_and_script(module_name, script_name)
  223. if script is None:
  224. raise Http404
  225. return module, script
  226. def list(self, request):
  227. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  228. results = {
  229. r.name: r
  230. for r in Job.objects.filter(
  231. object_type=script_content_type,
  232. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  233. ).order_by('name', '-created').distinct('name').defer('data')
  234. }
  235. script_list = []
  236. for script_module in ScriptModule.objects.restrict(request.user):
  237. script_list.extend(script_module.scripts.values())
  238. # Attach Job objects to each script (if any)
  239. for script in script_list:
  240. script.result = results.get(script.full_name, None)
  241. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  242. return Response(serializer.data)
  243. def retrieve(self, request, pk):
  244. module, script = self._get_script(pk)
  245. object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
  246. script.result = Job.objects.filter(
  247. object_type=object_type,
  248. name=script.name,
  249. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  250. ).first()
  251. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  252. return Response(serializer.data)
  253. def post(self, request, pk):
  254. """
  255. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  256. """
  257. if not request.user.has_perm('extras.run_script'):
  258. raise PermissionDenied("This user does not have permission to run scripts.")
  259. module, script = self._get_script(pk)
  260. input_serializer = serializers.ScriptInputSerializer(
  261. data=request.data,
  262. context={'script': script}
  263. )
  264. # Check that at least one RQ worker is running
  265. if not Worker.count(get_connection('default')):
  266. raise RQWorkerNotRunningException()
  267. if input_serializer.is_valid():
  268. script.result = Job.enqueue(
  269. run_script,
  270. instance=module,
  271. name=script.class_name,
  272. user=request.user,
  273. data=input_serializer.data['data'],
  274. request=copy_safe_request(request),
  275. commit=input_serializer.data['commit'],
  276. job_timeout=script.job_timeout,
  277. schedule_at=input_serializer.validated_data.get('schedule_at'),
  278. interval=input_serializer.validated_data.get('interval')
  279. )
  280. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  281. return Response(serializer.data)
  282. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  283. #
  284. # Change logging
  285. #
  286. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  287. """
  288. Retrieve a list of recent changes.
  289. """
  290. metadata_class = ContentTypeMetadata
  291. queryset = ObjectChange.objects.prefetch_related('user')
  292. serializer_class = serializers.ObjectChangeSerializer
  293. filterset_class = filtersets.ObjectChangeFilterSet
  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. permission_classes = (IsAuthenticated,)
  302. queryset = ContentType.objects.order_by('app_label', 'model')
  303. serializer_class = serializers.ContentTypeSerializer
  304. filterset_class = filtersets.ContentTypeFilterSet
  305. #
  306. # User dashboard
  307. #
  308. class DashboardView(RetrieveUpdateDestroyAPIView):
  309. queryset = Dashboard.objects.all()
  310. serializer_class = serializers.DashboardSerializer
  311. def get_object(self):
  312. return Dashboard.objects.filter(user=self.request.user).first()