views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 drf_spectacular.utils import extend_schema, extend_schema_view
  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.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin
  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 ModelViewSet
  14. from rq import Worker
  15. from extras import filtersets
  16. from extras.jobs import ScriptJob
  17. from extras.models import *
  18. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired, TokenWritePermission
  19. from netbox.api.features import SyncedDataMixin
  20. from netbox.api.metadata import ContentTypeMetadata
  21. from netbox.api.renderers import TextRenderer
  22. from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
  23. from netbox.api.viewsets.mixins import ObjectValidationMixin
  24. from utilities.exceptions import RQWorkerNotRunningException
  25. from utilities.request 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. # Table Configs
  107. #
  108. class TableConfigViewSet(NetBoxModelViewSet):
  109. metadata_class = ContentTypeMetadata
  110. queryset = TableConfig.objects.all()
  111. serializer_class = serializers.TableConfigSerializer
  112. filterset_class = filtersets.TableConfigFilterSet
  113. #
  114. # Bookmarks
  115. #
  116. class BookmarkViewSet(NetBoxModelViewSet):
  117. metadata_class = ContentTypeMetadata
  118. queryset = Bookmark.objects.all()
  119. serializer_class = serializers.BookmarkSerializer
  120. filterset_class = filtersets.BookmarkFilterSet
  121. #
  122. # Notifications & subscriptions
  123. #
  124. class NotificationViewSet(NetBoxModelViewSet):
  125. metadata_class = ContentTypeMetadata
  126. queryset = Notification.objects.all()
  127. serializer_class = serializers.NotificationSerializer
  128. class NotificationGroupViewSet(NetBoxModelViewSet):
  129. queryset = NotificationGroup.objects.all()
  130. serializer_class = serializers.NotificationGroupSerializer
  131. class SubscriptionViewSet(NetBoxModelViewSet):
  132. metadata_class = ContentTypeMetadata
  133. queryset = Subscription.objects.all()
  134. serializer_class = serializers.SubscriptionSerializer
  135. #
  136. # Tags
  137. #
  138. class TagViewSet(NetBoxModelViewSet):
  139. queryset = Tag.objects.all()
  140. serializer_class = serializers.TagSerializer
  141. filterset_class = filtersets.TagFilterSet
  142. class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
  143. queryset = TaggedItem.objects.prefetch_related(
  144. 'content_type', 'content_object', 'tag'
  145. ).order_by('tag__weight', 'tag__name')
  146. serializer_class = serializers.TaggedItemSerializer
  147. filterset_class = filtersets.TaggedItemFilterSet
  148. #
  149. # Image attachments
  150. #
  151. class ImageAttachmentViewSet(NetBoxModelViewSet):
  152. metadata_class = ContentTypeMetadata
  153. queryset = ImageAttachment.objects.all()
  154. serializer_class = serializers.ImageAttachmentSerializer
  155. filterset_class = filtersets.ImageAttachmentFilterSet
  156. #
  157. # Journal entries
  158. #
  159. class JournalEntryViewSet(NetBoxModelViewSet):
  160. metadata_class = ContentTypeMetadata
  161. queryset = JournalEntry.objects.all()
  162. serializer_class = serializers.JournalEntrySerializer
  163. filterset_class = filtersets.JournalEntryFilterSet
  164. #
  165. # Config contexts
  166. #
  167. class ConfigContextProfileViewSet(SyncedDataMixin, NetBoxModelViewSet):
  168. queryset = ConfigContextProfile.objects.all()
  169. serializer_class = serializers.ConfigContextProfileSerializer
  170. filterset_class = filtersets.ConfigContextProfileFilterSet
  171. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  172. queryset = ConfigContext.objects.all()
  173. serializer_class = serializers.ConfigContextSerializer
  174. filterset_class = filtersets.ConfigContextFilterSet
  175. #
  176. # Config templates
  177. #
  178. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  179. queryset = ConfigTemplate.objects.all()
  180. serializer_class = serializers.ConfigTemplateSerializer
  181. filterset_class = filtersets.ConfigTemplateFilterSet
  182. def get_permissions(self):
  183. # For render action, check only token write ability (not model permissions)
  184. if self.action == 'render':
  185. return [TokenWritePermission()]
  186. return super().get_permissions()
  187. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  188. def render(self, request, pk):
  189. """
  190. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  191. return the raw rendered content, rather than serialized JSON.
  192. """
  193. # Override restrict() on the default queryset to enforce the render & view actions
  194. self.queryset = self.queryset.model.objects.restrict(request.user, 'render').restrict(request.user, 'view')
  195. configtemplate = self.get_object()
  196. context = request.data
  197. return self.render_configtemplate(request, configtemplate, context)
  198. #
  199. # Scripts
  200. #
  201. class ScriptModuleViewSet(ObjectValidationMixin, CreateModelMixin, BaseViewSet):
  202. queryset = ScriptModule.objects.all()
  203. serializer_class = serializers.ScriptModuleSerializer
  204. @extend_schema_view(
  205. update=extend_schema(request=serializers.ScriptInputSerializer),
  206. partial_update=extend_schema(request=serializers.ScriptInputSerializer),
  207. )
  208. class ScriptViewSet(ModelViewSet):
  209. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  210. queryset = Script.objects.all()
  211. serializer_class = serializers.ScriptSerializer
  212. filterset_class = filtersets.ScriptFilterSet
  213. _ignore_model_permissions = True
  214. lookup_value_regex = '[^/]+' # Allow dots
  215. def initial(self, request, *args, **kwargs):
  216. super().initial(request, *args, **kwargs)
  217. # Restrict the view's QuerySet to allow only the permitted objects
  218. if request.user.is_authenticated:
  219. action = 'run' if request.method == 'POST' else 'view'
  220. self.queryset = self.queryset.restrict(request.user, action)
  221. def _get_script(self, pk):
  222. # If pk is numeric, retrieve script by ID
  223. if pk.isnumeric():
  224. return get_object_or_404(self.queryset, pk=pk)
  225. # Default to retrieval by module & name
  226. try:
  227. module_name, script_name = pk.split('.', maxsplit=1)
  228. except ValueError:
  229. raise Http404
  230. return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
  231. def retrieve(self, request, pk):
  232. script = self._get_script(pk)
  233. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  234. return Response(serializer.data)
  235. def post(self, request, pk):
  236. """
  237. Run a Script identified by its numeric PK or module & name and return the pending Job as the result
  238. """
  239. script = self._get_script(pk)
  240. if not request.user.has_perm('extras.run_script', obj=script):
  241. raise PermissionDenied("This user does not have permission to run this script.")
  242. input_serializer = serializers.ScriptInputSerializer(
  243. data=request.data,
  244. context={'script': script}
  245. )
  246. # Check that at least one RQ worker is running
  247. if not Worker.count(get_connection('default')):
  248. raise RQWorkerNotRunningException()
  249. if input_serializer.is_valid():
  250. ScriptJob.enqueue(
  251. instance=script,
  252. user=request.user,
  253. data=input_serializer.data['data'],
  254. request=copy_safe_request(request),
  255. commit=input_serializer.data['commit'],
  256. job_timeout=script.python_class.job_timeout,
  257. schedule_at=input_serializer.validated_data.get('schedule_at'),
  258. interval=input_serializer.validated_data.get('interval')
  259. )
  260. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  261. return Response(serializer.data)
  262. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  263. #
  264. # User dashboard
  265. #
  266. class DashboardView(RetrieveUpdateDestroyAPIView):
  267. queryset = Dashboard.objects.all()
  268. serializer_class = serializers.DashboardSerializer
  269. def get_object(self):
  270. return Dashboard.objects.filter(user=self.request.user).first()