views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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.generics import RetrieveUpdateDestroyAPIView
  8. from rest_framework.permissions import IsAuthenticated
  9. from rest_framework.renderers import JSONRenderer
  10. from rest_framework.response import Response
  11. from rest_framework.routers import APIRootView
  12. from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
  13. from rq import Worker
  14. from core.choices import JobStatusChoices
  15. from core.models import Job
  16. from extras import filtersets
  17. from extras.models import *
  18. from extras.reports import get_report, run_report
  19. from extras.scripts import get_script, run_script
  20. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  21. from netbox.api.features import SyncedDataMixin
  22. from netbox.api.metadata import ContentTypeMetadata
  23. from netbox.api.renderers import TextRenderer
  24. from netbox.api.viewsets import NetBoxModelViewSet
  25. from utilities.exceptions import RQWorkerNotRunningException
  26. from utilities.utils import copy_safe_request, count_related
  27. from . import serializers
  28. from .mixins import ConfigTemplateRenderMixin
  29. class ExtrasRootView(APIRootView):
  30. """
  31. Extras API root view
  32. """
  33. def get_view_name(self):
  34. return 'Extras'
  35. #
  36. # Webhooks
  37. #
  38. class WebhookViewSet(NetBoxModelViewSet):
  39. metadata_class = ContentTypeMetadata
  40. queryset = Webhook.objects.all()
  41. serializer_class = serializers.WebhookSerializer
  42. filterset_class = filtersets.WebhookFilterSet
  43. #
  44. # Custom fields
  45. #
  46. class CustomFieldViewSet(NetBoxModelViewSet):
  47. metadata_class = ContentTypeMetadata
  48. queryset = CustomField.objects.all()
  49. serializer_class = serializers.CustomFieldSerializer
  50. filterset_class = filtersets.CustomFieldFilterSet
  51. #
  52. # Custom links
  53. #
  54. class CustomLinkViewSet(NetBoxModelViewSet):
  55. metadata_class = ContentTypeMetadata
  56. queryset = CustomLink.objects.all()
  57. serializer_class = serializers.CustomLinkSerializer
  58. filterset_class = filtersets.CustomLinkFilterSet
  59. #
  60. # Export templates
  61. #
  62. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  63. metadata_class = ContentTypeMetadata
  64. queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
  65. serializer_class = serializers.ExportTemplateSerializer
  66. filterset_class = filtersets.ExportTemplateFilterSet
  67. #
  68. # Saved filters
  69. #
  70. class SavedFilterViewSet(NetBoxModelViewSet):
  71. metadata_class = ContentTypeMetadata
  72. queryset = SavedFilter.objects.all()
  73. serializer_class = serializers.SavedFilterSerializer
  74. filterset_class = filtersets.SavedFilterFilterSet
  75. #
  76. # Tags
  77. #
  78. class TagViewSet(NetBoxModelViewSet):
  79. queryset = Tag.objects.annotate(
  80. tagged_items=count_related(TaggedItem, 'tag')
  81. )
  82. serializer_class = serializers.TagSerializer
  83. filterset_class = filtersets.TagFilterSet
  84. #
  85. # Image attachments
  86. #
  87. class ImageAttachmentViewSet(NetBoxModelViewSet):
  88. metadata_class = ContentTypeMetadata
  89. queryset = ImageAttachment.objects.all()
  90. serializer_class = serializers.ImageAttachmentSerializer
  91. filterset_class = filtersets.ImageAttachmentFilterSet
  92. #
  93. # Journal entries
  94. #
  95. class JournalEntryViewSet(NetBoxModelViewSet):
  96. metadata_class = ContentTypeMetadata
  97. queryset = JournalEntry.objects.all()
  98. serializer_class = serializers.JournalEntrySerializer
  99. filterset_class = filtersets.JournalEntryFilterSet
  100. #
  101. # Config contexts
  102. #
  103. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  104. queryset = ConfigContext.objects.prefetch_related(
  105. 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
  106. 'data_file',
  107. )
  108. serializer_class = serializers.ConfigContextSerializer
  109. filterset_class = filtersets.ConfigContextFilterSet
  110. #
  111. # Config templates
  112. #
  113. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  114. queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
  115. serializer_class = serializers.ConfigTemplateSerializer
  116. filterset_class = filtersets.ConfigTemplateFilterSet
  117. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  118. def render(self, request, pk):
  119. """
  120. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  121. return the raw rendered content, rather than serialized JSON.
  122. """
  123. configtemplate = self.get_object()
  124. context = request.data
  125. return self.render_configtemplate(request, configtemplate, context)
  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('.', maxsplit=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_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. # Retrieve the Report and Job, if any.
  171. report = self._retrieve_report(pk)
  172. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  173. report.result = Job.objects.filter(
  174. object_type=report_content_type,
  175. name=report.full_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. report = self._retrieve_report(pk)
  195. input_serializer = serializers.ReportInputSerializer(data=request.data)
  196. if input_serializer.is_valid():
  197. report.result = Job.enqueue_job(
  198. run_report,
  199. name=report.full_name,
  200. obj_type=ContentType.objects.get_for_model(Report),
  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. exclude_from_schema = True
  216. lookup_value_regex = '[^/]+' # Allow dots
  217. def _get_script(self, pk):
  218. module_name, script_name = pk.split('.', maxsplit=1)
  219. script = get_script(module_name, script_name)
  220. if script is None:
  221. raise Http404
  222. return script
  223. def list(self, request):
  224. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  225. results = {
  226. r.name: r
  227. for r in Job.objects.filter(
  228. object_type=script_content_type,
  229. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  230. ).order_by('name', '-created').distinct('name').defer('data')
  231. }
  232. script_list = []
  233. for script_module in ScriptModule.objects.restrict(request.user):
  234. script_list.extend(script_module.scripts.values())
  235. # Attach Job objects to each script (if any)
  236. for script in script_list:
  237. script.result = results.get(script.full_name, None)
  238. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  239. return Response(serializer.data)
  240. def retrieve(self, request, pk):
  241. script = self._get_script(pk)
  242. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  243. script.result = Job.objects.filter(
  244. object_type=script_content_type,
  245. name=script.full_name,
  246. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  247. ).first()
  248. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  249. return Response(serializer.data)
  250. def post(self, request, pk):
  251. """
  252. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  253. """
  254. if not request.user.has_perm('extras.run_script'):
  255. raise PermissionDenied("This user does not have permission to run scripts.")
  256. script = self._get_script(pk)()
  257. input_serializer = serializers.ScriptInputSerializer(data=request.data)
  258. # Check that at least one RQ worker is running
  259. if not Worker.count(get_connection('default')):
  260. raise RQWorkerNotRunningException()
  261. if input_serializer.is_valid():
  262. script.result = Job.enqueue_job(
  263. run_script,
  264. name=script.full_name,
  265. obj_type=ContentType.objects.get_for_model(Script),
  266. user=request.user,
  267. data=input_serializer.data['data'],
  268. request=copy_safe_request(request),
  269. commit=input_serializer.data['commit'],
  270. job_timeout=script.job_timeout,
  271. schedule_at=input_serializer.validated_data.get('schedule_at'),
  272. interval=input_serializer.validated_data.get('interval')
  273. )
  274. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  275. return Response(serializer.data)
  276. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  277. #
  278. # Change logging
  279. #
  280. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  281. """
  282. Retrieve a list of recent changes.
  283. """
  284. metadata_class = ContentTypeMetadata
  285. queryset = ObjectChange.objects.prefetch_related('user')
  286. serializer_class = serializers.ObjectChangeSerializer
  287. filterset_class = filtersets.ObjectChangeFilterSet
  288. #
  289. # ContentTypes
  290. #
  291. class ContentTypeViewSet(ReadOnlyModelViewSet):
  292. """
  293. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  294. """
  295. permission_classes = (IsAuthenticated,)
  296. queryset = ContentType.objects.order_by('app_label', 'model')
  297. serializer_class = serializers.ContentTypeSerializer
  298. filterset_class = filtersets.ContentTypeFilterSet
  299. #
  300. # User dashboard
  301. #
  302. class DashboardView(RetrieveUpdateDestroyAPIView):
  303. queryset = Dashboard.objects.all()
  304. serializer_class = serializers.DashboardSerializer
  305. def get_object(self):
  306. return Dashboard.objects.filter(user=self.request.user).first()