views.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  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.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_module_and_report, run_report
  19. from extras.scripts import get_module_and_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.select_related('choice_set')
  49. serializer_class = serializers.CustomFieldSerializer
  50. filterset_class = filtersets.CustomFieldFilterSet
  51. class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
  52. queryset = CustomFieldChoiceSet.objects.all()
  53. serializer_class = serializers.CustomFieldChoiceSetSerializer
  54. filterset_class = filtersets.CustomFieldChoiceSetFilterSet
  55. @action(detail=True)
  56. def choices(self, request, pk):
  57. """
  58. Provides an endpoint to iterate through each choice in a set.
  59. """
  60. choiceset = get_object_or_404(self.queryset, pk=pk)
  61. choices = choiceset.choices
  62. # Enable filtering
  63. if q := request.GET.get('q'):
  64. q = q.lower()
  65. choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
  66. # Paginate data
  67. if page := self.paginate_queryset(choices):
  68. data = [
  69. {'id': c[0], 'display': c[1]} for c in page
  70. ]
  71. else:
  72. data = []
  73. return self.get_paginated_response(data)
  74. #
  75. # Custom links
  76. #
  77. class CustomLinkViewSet(NetBoxModelViewSet):
  78. metadata_class = ContentTypeMetadata
  79. queryset = CustomLink.objects.all()
  80. serializer_class = serializers.CustomLinkSerializer
  81. filterset_class = filtersets.CustomLinkFilterSet
  82. #
  83. # Export templates
  84. #
  85. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  86. metadata_class = ContentTypeMetadata
  87. queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
  88. serializer_class = serializers.ExportTemplateSerializer
  89. filterset_class = filtersets.ExportTemplateFilterSet
  90. #
  91. # Saved filters
  92. #
  93. class SavedFilterViewSet(NetBoxModelViewSet):
  94. metadata_class = ContentTypeMetadata
  95. queryset = SavedFilter.objects.all()
  96. serializer_class = serializers.SavedFilterSerializer
  97. filterset_class = filtersets.SavedFilterFilterSet
  98. #
  99. # Bookmarks
  100. #
  101. class BookmarkViewSet(NetBoxModelViewSet):
  102. metadata_class = ContentTypeMetadata
  103. queryset = Bookmark.objects.all()
  104. serializer_class = serializers.BookmarkSerializer
  105. filterset_class = filtersets.BookmarkFilterSet
  106. #
  107. # Tags
  108. #
  109. class TagViewSet(NetBoxModelViewSet):
  110. queryset = Tag.objects.annotate(
  111. tagged_items=count_related(TaggedItem, 'tag')
  112. )
  113. serializer_class = serializers.TagSerializer
  114. filterset_class = filtersets.TagFilterSet
  115. #
  116. # Image attachments
  117. #
  118. class ImageAttachmentViewSet(NetBoxModelViewSet):
  119. metadata_class = ContentTypeMetadata
  120. queryset = ImageAttachment.objects.all()
  121. serializer_class = serializers.ImageAttachmentSerializer
  122. filterset_class = filtersets.ImageAttachmentFilterSet
  123. #
  124. # Journal entries
  125. #
  126. class JournalEntryViewSet(NetBoxModelViewSet):
  127. metadata_class = ContentTypeMetadata
  128. queryset = JournalEntry.objects.all()
  129. serializer_class = serializers.JournalEntrySerializer
  130. filterset_class = filtersets.JournalEntryFilterSet
  131. #
  132. # Config contexts
  133. #
  134. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  135. queryset = ConfigContext.objects.prefetch_related(
  136. 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
  137. 'data_file',
  138. )
  139. serializer_class = serializers.ConfigContextSerializer
  140. filterset_class = filtersets.ConfigContextFilterSet
  141. #
  142. # Config templates
  143. #
  144. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  145. queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
  146. serializer_class = serializers.ConfigTemplateSerializer
  147. filterset_class = filtersets.ConfigTemplateFilterSet
  148. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  149. def render(self, request, pk):
  150. """
  151. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  152. return the raw rendered content, rather than serialized JSON.
  153. """
  154. configtemplate = self.get_object()
  155. context = request.data
  156. return self.render_configtemplate(request, configtemplate, context)
  157. #
  158. # Reports
  159. #
  160. class ReportViewSet(ViewSet):
  161. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  162. _ignore_model_permissions = True
  163. schema = None
  164. lookup_value_regex = '[^/]+' # Allow dots
  165. def _get_report(self, pk):
  166. try:
  167. module_name, report_name = pk.split('.', maxsplit=1)
  168. except ValueError:
  169. raise Http404
  170. module, report = get_module_and_report(module_name, report_name)
  171. if report is None:
  172. raise Http404
  173. return module, report
  174. def list(self, request):
  175. """
  176. Compile all reports and their related results (if any). Result data is deferred in the list view.
  177. """
  178. results = {
  179. job.name: job
  180. for job in Job.objects.filter(
  181. object_type=ContentType.objects.get(app_label='extras', model='reportmodule'),
  182. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  183. ).order_by('name', '-created').distinct('name').defer('data')
  184. }
  185. report_list = []
  186. for report_module in ReportModule.objects.restrict(request.user):
  187. report_list.extend([report() for report in report_module.reports.values()])
  188. # Attach Job objects to each report (if any)
  189. for report in report_list:
  190. report.result = results.get(report.name, None)
  191. serializer = serializers.ReportSerializer(report_list, many=True, context={
  192. 'request': request,
  193. })
  194. return Response({'count': len(report_list), 'results': serializer.data})
  195. def retrieve(self, request, pk):
  196. """
  197. Retrieve a single Report identified as "<module>.<report>".
  198. """
  199. module, report = self._get_report(pk)
  200. # Retrieve the Report and Job, if any.
  201. object_type = ContentType.objects.get(app_label='extras', model='reportmodule')
  202. report.result = Job.objects.filter(
  203. object_type=object_type,
  204. name=report.name,
  205. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  206. ).first()
  207. serializer = serializers.ReportDetailSerializer(report, context={
  208. 'request': request
  209. })
  210. return Response(serializer.data)
  211. @action(detail=True, methods=['post'])
  212. def run(self, request, pk):
  213. """
  214. Run a Report identified as "<module>.<script>" and return the pending Job as the result
  215. """
  216. # Check that the user has permission to run reports.
  217. if not request.user.has_perm('extras.run_report'):
  218. raise PermissionDenied("This user does not have permission to run reports.")
  219. # Check that at least one RQ worker is running
  220. if not Worker.count(get_connection('default')):
  221. raise RQWorkerNotRunningException()
  222. # Retrieve and run the Report. This will create a new Job.
  223. module, report_cls = self._get_report(pk)
  224. report = report_cls
  225. input_serializer = serializers.ReportInputSerializer(
  226. data=request.data,
  227. context={'report': report}
  228. )
  229. if input_serializer.is_valid():
  230. report.result = Job.enqueue(
  231. run_report,
  232. instance=module,
  233. name=report.class_name,
  234. user=request.user,
  235. job_timeout=report.job_timeout,
  236. schedule_at=input_serializer.validated_data.get('schedule_at'),
  237. interval=input_serializer.validated_data.get('interval')
  238. )
  239. serializer = serializers.ReportDetailSerializer(report, context={'request': request})
  240. return Response(serializer.data)
  241. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  242. #
  243. # Scripts
  244. #
  245. class ScriptViewSet(ViewSet):
  246. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  247. _ignore_model_permissions = True
  248. schema = None
  249. lookup_value_regex = '[^/]+' # Allow dots
  250. def _get_script(self, pk):
  251. try:
  252. module_name, script_name = pk.split('.', maxsplit=1)
  253. except ValueError:
  254. raise Http404
  255. module, script = get_module_and_script(module_name, script_name)
  256. if script is None:
  257. raise Http404
  258. return module, script
  259. def list(self, request):
  260. results = {
  261. job.name: job
  262. for job in Job.objects.filter(
  263. object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
  264. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  265. ).order_by('name', '-created').distinct('name').defer('data')
  266. }
  267. script_list = []
  268. for script_module in ScriptModule.objects.restrict(request.user):
  269. script_list.extend(script_module.scripts.values())
  270. # Attach Job objects to each script (if any)
  271. for script in script_list:
  272. script.result = results.get(script.class_name, None)
  273. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  274. return Response({'count': len(script_list), 'results': serializer.data})
  275. def retrieve(self, request, pk):
  276. module, script = self._get_script(pk)
  277. object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
  278. script.result = Job.objects.filter(
  279. object_type=object_type,
  280. name=script.class_name,
  281. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  282. ).first()
  283. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  284. return Response(serializer.data)
  285. def post(self, request, pk):
  286. """
  287. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  288. """
  289. if not request.user.has_perm('extras.run_script'):
  290. raise PermissionDenied("This user does not have permission to run scripts.")
  291. module, script = self._get_script(pk)
  292. input_serializer = serializers.ScriptInputSerializer(
  293. data=request.data,
  294. context={'script': script}
  295. )
  296. # Check that at least one RQ worker is running
  297. if not Worker.count(get_connection('default')):
  298. raise RQWorkerNotRunningException()
  299. if input_serializer.is_valid():
  300. script.result = Job.enqueue(
  301. run_script,
  302. instance=module,
  303. name=script.class_name,
  304. user=request.user,
  305. data=input_serializer.data['data'],
  306. request=copy_safe_request(request),
  307. commit=input_serializer.data['commit'],
  308. job_timeout=script.job_timeout,
  309. schedule_at=input_serializer.validated_data.get('schedule_at'),
  310. interval=input_serializer.validated_data.get('interval')
  311. )
  312. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  313. return Response(serializer.data)
  314. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  315. #
  316. # Change logging
  317. #
  318. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  319. """
  320. Retrieve a list of recent changes.
  321. """
  322. metadata_class = ContentTypeMetadata
  323. queryset = ObjectChange.objects.valid_models().prefetch_related('user')
  324. serializer_class = serializers.ObjectChangeSerializer
  325. filterset_class = filtersets.ObjectChangeFilterSet
  326. #
  327. # ContentTypes
  328. #
  329. class ContentTypeViewSet(ReadOnlyModelViewSet):
  330. """
  331. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  332. """
  333. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  334. queryset = ContentType.objects.order_by('app_label', 'model')
  335. serializer_class = serializers.ContentTypeSerializer
  336. filterset_class = filtersets.ContentTypeFilterSet
  337. #
  338. # User dashboard
  339. #
  340. class DashboardView(RetrieveUpdateDestroyAPIView):
  341. queryset = Dashboard.objects.all()
  342. serializer_class = serializers.DashboardSerializer
  343. def get_object(self):
  344. return Dashboard.objects.filter(user=self.request.user).first()