views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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. results = {
  149. job.name: job
  150. for job in Job.objects.filter(
  151. object_type=ContentType.objects.get(app_label='extras', model='reportmodule'),
  152. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  153. ).order_by('name', '-created').distinct('name').defer('data')
  154. }
  155. report_list = []
  156. for report_module in ReportModule.objects.restrict(request.user):
  157. report_list.extend([report() for report in report_module.reports.values()])
  158. # Attach Job objects to each report (if any)
  159. for report in report_list:
  160. report.result = results.get(report.name, None)
  161. serializer = serializers.ReportSerializer(report_list, many=True, context={
  162. 'request': request,
  163. })
  164. return Response(serializer.data)
  165. def retrieve(self, request, pk):
  166. """
  167. Retrieve a single Report identified as "<module>.<report>".
  168. """
  169. module, report = self._get_report(pk)
  170. # Retrieve the Report and Job, if any.
  171. object_type = ContentType.objects.get(app_label='extras', model='reportmodule')
  172. report.result = Job.objects.filter(
  173. object_type=object_type,
  174. name=report.name,
  175. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  176. ).first()
  177. serializer = serializers.ReportDetailSerializer(report, context={
  178. 'request': request
  179. })
  180. return Response(serializer.data)
  181. @action(detail=True, methods=['post'])
  182. def run(self, request, pk):
  183. """
  184. Run a Report identified as "<module>.<script>" and return the pending Job as the result
  185. """
  186. # Check that the user has permission to run reports.
  187. if not request.user.has_perm('extras.run_report'):
  188. raise PermissionDenied("This user does not have permission to run reports.")
  189. # Check that at least one RQ worker is running
  190. if not Worker.count(get_connection('default')):
  191. raise RQWorkerNotRunningException()
  192. # Retrieve and run the Report. This will create a new Job.
  193. module, report_cls = self._get_report(pk)
  194. report = report_cls()
  195. input_serializer = serializers.ReportInputSerializer(
  196. data=request.data,
  197. context={'report': report}
  198. )
  199. if input_serializer.is_valid():
  200. report.result = Job.enqueue(
  201. run_report,
  202. instance=module,
  203. name=report.class_name,
  204. user=request.user,
  205. job_timeout=report.job_timeout,
  206. schedule_at=input_serializer.validated_data.get('schedule_at'),
  207. interval=input_serializer.validated_data.get('interval')
  208. )
  209. serializer = serializers.ReportDetailSerializer(report, context={'request': request})
  210. return Response(serializer.data)
  211. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  212. #
  213. # Scripts
  214. #
  215. class ScriptViewSet(ViewSet):
  216. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  217. _ignore_model_permissions = True
  218. schema = None
  219. lookup_value_regex = '[^/]+' # Allow dots
  220. def _get_script(self, pk):
  221. try:
  222. module_name, script_name = pk.split('.', maxsplit=1)
  223. except ValueError:
  224. raise Http404
  225. module, script = get_module_and_script(module_name, script_name)
  226. if script is None:
  227. raise Http404
  228. return module, script
  229. def list(self, request):
  230. results = {
  231. job.name: job
  232. for job in Job.objects.filter(
  233. object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
  234. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  235. ).order_by('name', '-created').distinct('name').defer('data')
  236. }
  237. script_list = []
  238. for script_module in ScriptModule.objects.restrict(request.user):
  239. script_list.extend(script_module.scripts.values())
  240. # Attach Job objects to each script (if any)
  241. for script in script_list:
  242. script.result = results.get(script.name, None)
  243. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  244. return Response(serializer.data)
  245. def retrieve(self, request, pk):
  246. module, script = self._get_script(pk)
  247. object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
  248. script.result = Job.objects.filter(
  249. object_type=object_type,
  250. name=script.name,
  251. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  252. ).first()
  253. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  254. return Response(serializer.data)
  255. def post(self, request, pk):
  256. """
  257. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  258. """
  259. if not request.user.has_perm('extras.run_script'):
  260. raise PermissionDenied("This user does not have permission to run scripts.")
  261. module, script = self._get_script(pk)
  262. input_serializer = serializers.ScriptInputSerializer(
  263. data=request.data,
  264. context={'script': script}
  265. )
  266. # Check that at least one RQ worker is running
  267. if not Worker.count(get_connection('default')):
  268. raise RQWorkerNotRunningException()
  269. if input_serializer.is_valid():
  270. script.result = Job.enqueue(
  271. run_script,
  272. instance=module,
  273. name=script.class_name,
  274. user=request.user,
  275. data=input_serializer.data['data'],
  276. request=copy_safe_request(request),
  277. commit=input_serializer.data['commit'],
  278. job_timeout=script.job_timeout,
  279. schedule_at=input_serializer.validated_data.get('schedule_at'),
  280. interval=input_serializer.validated_data.get('interval')
  281. )
  282. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  283. return Response(serializer.data)
  284. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  285. #
  286. # Change logging
  287. #
  288. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  289. """
  290. Retrieve a list of recent changes.
  291. """
  292. metadata_class = ContentTypeMetadata
  293. queryset = ObjectChange.objects.valid_models().prefetch_related('user')
  294. serializer_class = serializers.ObjectChangeSerializer
  295. filterset_class = filtersets.ObjectChangeFilterSet
  296. #
  297. # ContentTypes
  298. #
  299. class ContentTypeViewSet(ReadOnlyModelViewSet):
  300. """
  301. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  302. """
  303. permission_classes = (IsAuthenticated,)
  304. queryset = ContentType.objects.order_by('app_label', 'model')
  305. serializer_class = serializers.ContentTypeSerializer
  306. filterset_class = filtersets.ContentTypeFilterSet
  307. #
  308. # User dashboard
  309. #
  310. class DashboardView(RetrieveUpdateDestroyAPIView):
  311. queryset = Dashboard.objects.all()
  312. serializer_class = serializers.DashboardSerializer
  313. def get_object(self):
  314. return Dashboard.objects.filter(user=self.request.user).first()