views.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. from django.http import Http404
  2. from django.shortcuts import get_object_or_404
  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.renderers import JSONRenderer
  9. from rest_framework.response import Response
  10. from rest_framework.routers import APIRootView
  11. from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
  12. from rq import Worker
  13. from core.models import Job, ObjectType
  14. from extras import filtersets
  15. from extras.models import *
  16. from extras.scripts import run_script
  17. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  18. from netbox.api.features import SyncedDataMixin
  19. from netbox.api.metadata import ContentTypeMetadata
  20. from netbox.api.renderers import TextRenderer
  21. from netbox.api.viewsets import NetBoxModelViewSet
  22. from utilities.exceptions import RQWorkerNotRunningException
  23. from utilities.request import copy_safe_request
  24. from . import serializers
  25. from .mixins import ConfigTemplateRenderMixin
  26. class ExtrasRootView(APIRootView):
  27. """
  28. Extras API root view
  29. """
  30. def get_view_name(self):
  31. return 'Extras'
  32. #
  33. # EventRules
  34. #
  35. class EventRuleViewSet(NetBoxModelViewSet):
  36. metadata_class = ContentTypeMetadata
  37. queryset = EventRule.objects.all()
  38. serializer_class = serializers.EventRuleSerializer
  39. filterset_class = filtersets.EventRuleFilterSet
  40. #
  41. # Webhooks
  42. #
  43. class WebhookViewSet(NetBoxModelViewSet):
  44. metadata_class = ContentTypeMetadata
  45. queryset = Webhook.objects.all()
  46. serializer_class = serializers.WebhookSerializer
  47. filterset_class = filtersets.WebhookFilterSet
  48. #
  49. # Custom fields
  50. #
  51. class CustomFieldViewSet(NetBoxModelViewSet):
  52. metadata_class = ContentTypeMetadata
  53. queryset = CustomField.objects.select_related('choice_set')
  54. serializer_class = serializers.CustomFieldSerializer
  55. filterset_class = filtersets.CustomFieldFilterSet
  56. class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
  57. queryset = CustomFieldChoiceSet.objects.all()
  58. serializer_class = serializers.CustomFieldChoiceSetSerializer
  59. filterset_class = filtersets.CustomFieldChoiceSetFilterSet
  60. @action(detail=True)
  61. def choices(self, request, pk):
  62. """
  63. Provides an endpoint to iterate through each choice in a set.
  64. """
  65. choiceset = get_object_or_404(self.queryset, pk=pk)
  66. choices = choiceset.choices
  67. # Enable filtering
  68. if q := request.GET.get('q'):
  69. q = q.lower()
  70. choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
  71. # Paginate data
  72. if page := self.paginate_queryset(choices):
  73. data = [
  74. {'id': c[0], 'display': c[1]} for c in page
  75. ]
  76. else:
  77. data = []
  78. return self.get_paginated_response(data)
  79. #
  80. # Custom links
  81. #
  82. class CustomLinkViewSet(NetBoxModelViewSet):
  83. metadata_class = ContentTypeMetadata
  84. queryset = CustomLink.objects.all()
  85. serializer_class = serializers.CustomLinkSerializer
  86. filterset_class = filtersets.CustomLinkFilterSet
  87. #
  88. # Export templates
  89. #
  90. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  91. metadata_class = ContentTypeMetadata
  92. queryset = ExportTemplate.objects.all()
  93. serializer_class = serializers.ExportTemplateSerializer
  94. filterset_class = filtersets.ExportTemplateFilterSet
  95. #
  96. # Saved filters
  97. #
  98. class SavedFilterViewSet(NetBoxModelViewSet):
  99. metadata_class = ContentTypeMetadata
  100. queryset = SavedFilter.objects.all()
  101. serializer_class = serializers.SavedFilterSerializer
  102. filterset_class = filtersets.SavedFilterFilterSet
  103. #
  104. # Bookmarks
  105. #
  106. class BookmarkViewSet(NetBoxModelViewSet):
  107. metadata_class = ContentTypeMetadata
  108. queryset = Bookmark.objects.all()
  109. serializer_class = serializers.BookmarkSerializer
  110. filterset_class = filtersets.BookmarkFilterSet
  111. #
  112. # Tags
  113. #
  114. class TagViewSet(NetBoxModelViewSet):
  115. queryset = Tag.objects.all()
  116. serializer_class = serializers.TagSerializer
  117. filterset_class = filtersets.TagFilterSet
  118. #
  119. # Image attachments
  120. #
  121. class ImageAttachmentViewSet(NetBoxModelViewSet):
  122. metadata_class = ContentTypeMetadata
  123. queryset = ImageAttachment.objects.all()
  124. serializer_class = serializers.ImageAttachmentSerializer
  125. filterset_class = filtersets.ImageAttachmentFilterSet
  126. #
  127. # Journal entries
  128. #
  129. class JournalEntryViewSet(NetBoxModelViewSet):
  130. metadata_class = ContentTypeMetadata
  131. queryset = JournalEntry.objects.all()
  132. serializer_class = serializers.JournalEntrySerializer
  133. filterset_class = filtersets.JournalEntryFilterSet
  134. #
  135. # Config contexts
  136. #
  137. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  138. queryset = ConfigContext.objects.all()
  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.all()
  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. # Scripts
  159. #
  160. class ScriptViewSet(ModelViewSet):
  161. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  162. queryset = Script.objects.prefetch_related('jobs')
  163. serializer_class = serializers.ScriptSerializer
  164. filterset_class = filtersets.ScriptFilterSet
  165. _ignore_model_permissions = True
  166. lookup_value_regex = '[^/]+' # Allow dots
  167. def _get_script(self, pk):
  168. # If pk is numeric, retrieve script by ID
  169. if pk.isnumeric():
  170. return get_object_or_404(self.queryset, pk=pk)
  171. # Default to retrieval by module & name
  172. try:
  173. module_name, script_name = pk.split('.', maxsplit=1)
  174. except ValueError:
  175. raise Http404
  176. return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
  177. def retrieve(self, request, pk):
  178. script = self._get_script(pk)
  179. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  180. return Response(serializer.data)
  181. def post(self, request, pk):
  182. """
  183. Run a Script identified by its numeric PK or module & name and return the pending Job as the result
  184. """
  185. if not request.user.has_perm('extras.run_script'):
  186. raise PermissionDenied("This user does not have permission to run scripts.")
  187. script = self._get_script(pk)
  188. input_serializer = serializers.ScriptInputSerializer(
  189. data=request.data,
  190. context={'script': script}
  191. )
  192. # Check that at least one RQ worker is running
  193. if not Worker.count(get_connection('default')):
  194. raise RQWorkerNotRunningException()
  195. if input_serializer.is_valid():
  196. Job.enqueue(
  197. run_script,
  198. instance=script,
  199. name=script.python_class.class_name,
  200. user=request.user,
  201. data=input_serializer.data['data'],
  202. request=copy_safe_request(request),
  203. commit=input_serializer.data['commit'],
  204. job_timeout=script.python_class.job_timeout,
  205. schedule_at=input_serializer.validated_data.get('schedule_at'),
  206. interval=input_serializer.validated_data.get('interval')
  207. )
  208. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  209. return Response(serializer.data)
  210. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  211. #
  212. # Object types
  213. #
  214. class ObjectTypeViewSet(ReadOnlyModelViewSet):
  215. """
  216. Read-only list of ObjectTypes.
  217. """
  218. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  219. queryset = ObjectType.objects.order_by('app_label', 'model')
  220. serializer_class = serializers.ObjectTypeSerializer
  221. filterset_class = filtersets.ObjectTypeFilterSet
  222. #
  223. # User dashboard
  224. #
  225. class DashboardView(RetrieveUpdateDestroyAPIView):
  226. queryset = Dashboard.objects.all()
  227. serializer_class = serializers.DashboardSerializer
  228. def get_object(self):
  229. return Dashboard.objects.filter(user=self.request.user).first()