2
0

views.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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.scripts import get_module_and_script, run_script
  19. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  20. from netbox.api.features import SyncedDataMixin
  21. from netbox.api.metadata import ContentTypeMetadata
  22. from netbox.api.renderers import TextRenderer
  23. from netbox.api.viewsets import NetBoxModelViewSet
  24. from utilities.exceptions import RQWorkerNotRunningException
  25. from utilities.utils import copy_safe_request
  26. from . import serializers
  27. from .mixins import ConfigTemplateRenderMixin
  28. class ExtrasRootView(APIRootView):
  29. """
  30. Extras API root view
  31. """
  32. def get_view_name(self):
  33. return 'Extras'
  34. #
  35. # EventRules
  36. #
  37. class EventRuleViewSet(NetBoxModelViewSet):
  38. metadata_class = ContentTypeMetadata
  39. queryset = EventRule.objects.all()
  40. serializer_class = serializers.EventRuleSerializer
  41. filterset_class = filtersets.EventRuleFilterSet
  42. #
  43. # Webhooks
  44. #
  45. class WebhookViewSet(NetBoxModelViewSet):
  46. metadata_class = ContentTypeMetadata
  47. queryset = Webhook.objects.all()
  48. serializer_class = serializers.WebhookSerializer
  49. filterset_class = filtersets.WebhookFilterSet
  50. #
  51. # Custom fields
  52. #
  53. class CustomFieldViewSet(NetBoxModelViewSet):
  54. metadata_class = ContentTypeMetadata
  55. queryset = CustomField.objects.select_related('choice_set')
  56. serializer_class = serializers.CustomFieldSerializer
  57. filterset_class = filtersets.CustomFieldFilterSet
  58. class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
  59. queryset = CustomFieldChoiceSet.objects.all()
  60. serializer_class = serializers.CustomFieldChoiceSetSerializer
  61. filterset_class = filtersets.CustomFieldChoiceSetFilterSet
  62. @action(detail=True)
  63. def choices(self, request, pk):
  64. """
  65. Provides an endpoint to iterate through each choice in a set.
  66. """
  67. choiceset = get_object_or_404(self.queryset, pk=pk)
  68. choices = choiceset.choices
  69. # Enable filtering
  70. if q := request.GET.get('q'):
  71. q = q.lower()
  72. choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
  73. # Paginate data
  74. if page := self.paginate_queryset(choices):
  75. data = [
  76. {'id': c[0], 'display': c[1]} for c in page
  77. ]
  78. else:
  79. data = []
  80. return self.get_paginated_response(data)
  81. #
  82. # Custom links
  83. #
  84. class CustomLinkViewSet(NetBoxModelViewSet):
  85. metadata_class = ContentTypeMetadata
  86. queryset = CustomLink.objects.all()
  87. serializer_class = serializers.CustomLinkSerializer
  88. filterset_class = filtersets.CustomLinkFilterSet
  89. #
  90. # Export templates
  91. #
  92. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  93. metadata_class = ContentTypeMetadata
  94. queryset = ExportTemplate.objects.all()
  95. serializer_class = serializers.ExportTemplateSerializer
  96. filterset_class = filtersets.ExportTemplateFilterSet
  97. #
  98. # Saved filters
  99. #
  100. class SavedFilterViewSet(NetBoxModelViewSet):
  101. metadata_class = ContentTypeMetadata
  102. queryset = SavedFilter.objects.all()
  103. serializer_class = serializers.SavedFilterSerializer
  104. filterset_class = filtersets.SavedFilterFilterSet
  105. #
  106. # Bookmarks
  107. #
  108. class BookmarkViewSet(NetBoxModelViewSet):
  109. metadata_class = ContentTypeMetadata
  110. queryset = Bookmark.objects.all()
  111. serializer_class = serializers.BookmarkSerializer
  112. filterset_class = filtersets.BookmarkFilterSet
  113. #
  114. # Tags
  115. #
  116. class TagViewSet(NetBoxModelViewSet):
  117. queryset = Tag.objects.all()
  118. serializer_class = serializers.TagSerializer
  119. filterset_class = filtersets.TagFilterSet
  120. #
  121. # Image attachments
  122. #
  123. class ImageAttachmentViewSet(NetBoxModelViewSet):
  124. metadata_class = ContentTypeMetadata
  125. queryset = ImageAttachment.objects.all()
  126. serializer_class = serializers.ImageAttachmentSerializer
  127. filterset_class = filtersets.ImageAttachmentFilterSet
  128. #
  129. # Journal entries
  130. #
  131. class JournalEntryViewSet(NetBoxModelViewSet):
  132. metadata_class = ContentTypeMetadata
  133. queryset = JournalEntry.objects.all()
  134. serializer_class = serializers.JournalEntrySerializer
  135. filterset_class = filtersets.JournalEntryFilterSet
  136. #
  137. # Config contexts
  138. #
  139. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  140. queryset = ConfigContext.objects.all()
  141. serializer_class = serializers.ConfigContextSerializer
  142. filterset_class = filtersets.ConfigContextFilterSet
  143. #
  144. # Config templates
  145. #
  146. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  147. queryset = ConfigTemplate.objects.all()
  148. serializer_class = serializers.ConfigTemplateSerializer
  149. filterset_class = filtersets.ConfigTemplateFilterSet
  150. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  151. def render(self, request, pk):
  152. """
  153. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  154. return the raw rendered content, rather than serialized JSON.
  155. """
  156. configtemplate = self.get_object()
  157. context = request.data
  158. return self.render_configtemplate(request, configtemplate, context)
  159. #
  160. # Scripts
  161. #
  162. class ScriptViewSet(ViewSet):
  163. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  164. _ignore_model_permissions = True
  165. schema = None
  166. lookup_value_regex = '[^/]+' # Allow dots
  167. def _get_script(self, pk):
  168. try:
  169. module_name, script_name = pk.split('.', maxsplit=1)
  170. except ValueError:
  171. raise Http404
  172. module, script = get_module_and_script(module_name, script_name)
  173. if script is None:
  174. raise Http404
  175. return module, script
  176. def list(self, request):
  177. results = {
  178. job.name: job
  179. for job in Job.objects.filter(
  180. object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
  181. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  182. ).order_by('name', '-created').distinct('name').defer('data')
  183. }
  184. script_list = []
  185. for script_module in ScriptModule.objects.restrict(request.user):
  186. script_list.extend(script_module.scripts.values())
  187. # Attach Job objects to each script (if any)
  188. for script in script_list:
  189. script.result = results.get(script.class_name, None)
  190. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  191. return Response({'count': len(script_list), 'results': serializer.data})
  192. def retrieve(self, request, pk):
  193. module, script = self._get_script(pk)
  194. object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
  195. script.result = Job.objects.filter(
  196. object_type=object_type,
  197. name=script.class_name,
  198. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  199. ).first()
  200. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  201. return Response(serializer.data)
  202. def post(self, request, pk):
  203. """
  204. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  205. """
  206. if not request.user.has_perm('extras.run_script'):
  207. raise PermissionDenied("This user does not have permission to run scripts.")
  208. module, script = self._get_script(pk)
  209. input_serializer = serializers.ScriptInputSerializer(
  210. data=request.data,
  211. context={'script': script}
  212. )
  213. # Check that at least one RQ worker is running
  214. if not Worker.count(get_connection('default')):
  215. raise RQWorkerNotRunningException()
  216. if input_serializer.is_valid():
  217. script.result = Job.enqueue(
  218. run_script,
  219. instance=module,
  220. name=script.class_name,
  221. user=request.user,
  222. data=input_serializer.data['data'],
  223. request=copy_safe_request(request),
  224. commit=input_serializer.data['commit'],
  225. job_timeout=script.job_timeout,
  226. schedule_at=input_serializer.validated_data.get('schedule_at'),
  227. interval=input_serializer.validated_data.get('interval')
  228. )
  229. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  230. return Response(serializer.data)
  231. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  232. #
  233. # Change logging
  234. #
  235. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  236. """
  237. Retrieve a list of recent changes.
  238. """
  239. metadata_class = ContentTypeMetadata
  240. queryset = ObjectChange.objects.valid_models()
  241. serializer_class = serializers.ObjectChangeSerializer
  242. filterset_class = filtersets.ObjectChangeFilterSet
  243. #
  244. # ContentTypes
  245. #
  246. class ContentTypeViewSet(ReadOnlyModelViewSet):
  247. """
  248. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  249. """
  250. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  251. queryset = ContentType.objects.order_by('app_label', 'model')
  252. serializer_class = serializers.ContentTypeSerializer
  253. filterset_class = filtersets.ContentTypeFilterSet
  254. #
  255. # User dashboard
  256. #
  257. class DashboardView(RetrieveUpdateDestroyAPIView):
  258. queryset = Dashboard.objects.all()
  259. serializer_class = serializers.DashboardSerializer
  260. def get_object(self):
  261. return Dashboard.objects.filter(user=self.request.user).first()