views.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. from django.http import Http404
  2. from django.shortcuts import get_object_or_404
  3. from django.utils.module_loading import import_string
  4. from django_rq.queues import get_connection
  5. from drf_spectacular.utils import extend_schema, extend_schema_view
  6. from rest_framework import status
  7. from rest_framework.decorators import action
  8. from rest_framework.exceptions import PermissionDenied
  9. from rest_framework.generics import RetrieveUpdateDestroyAPIView
  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, ReadOnlyModelViewSet
  14. from rq import Worker
  15. from core.models import ObjectType
  16. from extras import filtersets
  17. from extras.models import *
  18. from extras.jobs import ScriptJob
  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.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. # 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. # Notifications & subscriptions
  115. #
  116. class NotificationViewSet(NetBoxModelViewSet):
  117. metadata_class = ContentTypeMetadata
  118. queryset = Notification.objects.all()
  119. serializer_class = serializers.NotificationSerializer
  120. class NotificationGroupViewSet(NetBoxModelViewSet):
  121. queryset = NotificationGroup.objects.all()
  122. serializer_class = serializers.NotificationGroupSerializer
  123. class SubscriptionViewSet(NetBoxModelViewSet):
  124. metadata_class = ContentTypeMetadata
  125. queryset = Subscription.objects.all()
  126. serializer_class = serializers.SubscriptionSerializer
  127. #
  128. # Tags
  129. #
  130. class TagViewSet(NetBoxModelViewSet):
  131. queryset = Tag.objects.all()
  132. serializer_class = serializers.TagSerializer
  133. filterset_class = filtersets.TagFilterSet
  134. #
  135. # Image attachments
  136. #
  137. class ImageAttachmentViewSet(NetBoxModelViewSet):
  138. metadata_class = ContentTypeMetadata
  139. queryset = ImageAttachment.objects.all()
  140. serializer_class = serializers.ImageAttachmentSerializer
  141. filterset_class = filtersets.ImageAttachmentFilterSet
  142. #
  143. # Journal entries
  144. #
  145. class JournalEntryViewSet(NetBoxModelViewSet):
  146. metadata_class = ContentTypeMetadata
  147. queryset = JournalEntry.objects.all()
  148. serializer_class = serializers.JournalEntrySerializer
  149. filterset_class = filtersets.JournalEntryFilterSet
  150. #
  151. # Config contexts
  152. #
  153. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  154. queryset = ConfigContext.objects.all()
  155. serializer_class = serializers.ConfigContextSerializer
  156. filterset_class = filtersets.ConfigContextFilterSet
  157. #
  158. # Config templates
  159. #
  160. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  161. queryset = ConfigTemplate.objects.all()
  162. serializer_class = serializers.ConfigTemplateSerializer
  163. filterset_class = filtersets.ConfigTemplateFilterSet
  164. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  165. def render(self, request, pk):
  166. """
  167. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  168. return the raw rendered content, rather than serialized JSON.
  169. """
  170. configtemplate = self.get_object()
  171. context = request.data
  172. return self.render_configtemplate(request, configtemplate, context)
  173. #
  174. # Scripts
  175. #
  176. @extend_schema_view(
  177. update=extend_schema(request=serializers.ScriptInputSerializer),
  178. partial_update=extend_schema(request=serializers.ScriptInputSerializer),
  179. )
  180. class ScriptViewSet(ModelViewSet):
  181. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  182. queryset = Script.objects.all()
  183. serializer_class = serializers.ScriptSerializer
  184. filterset_class = filtersets.ScriptFilterSet
  185. _ignore_model_permissions = True
  186. lookup_value_regex = '[^/]+' # Allow dots
  187. def _get_script(self, pk):
  188. # If pk is numeric, retrieve script by ID
  189. if pk.isnumeric():
  190. return get_object_or_404(self.queryset, pk=pk)
  191. # Default to retrieval by module & name
  192. try:
  193. module_name, script_name = pk.split('.', maxsplit=1)
  194. except ValueError:
  195. raise Http404
  196. return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
  197. def retrieve(self, request, pk):
  198. script = self._get_script(pk)
  199. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  200. return Response(serializer.data)
  201. def post(self, request, pk):
  202. """
  203. Run a Script identified by its numeric PK or module & name and return the pending Job as the result
  204. """
  205. if not request.user.has_perm('extras.run_script'):
  206. raise PermissionDenied("This user does not have permission to run scripts.")
  207. script = self._get_script(pk)
  208. input_serializer = serializers.ScriptInputSerializer(
  209. data=request.data,
  210. context={'script': script}
  211. )
  212. # Check that at least one RQ worker is running
  213. if not Worker.count(get_connection('default')):
  214. raise RQWorkerNotRunningException()
  215. if input_serializer.is_valid():
  216. ScriptJob.enqueue(
  217. instance=script,
  218. user=request.user,
  219. data=input_serializer.data['data'],
  220. request=copy_safe_request(request),
  221. commit=input_serializer.data['commit'],
  222. job_timeout=script.python_class.job_timeout,
  223. schedule_at=input_serializer.validated_data.get('schedule_at'),
  224. interval=input_serializer.validated_data.get('interval')
  225. )
  226. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  227. return Response(serializer.data)
  228. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  229. #
  230. # Object types
  231. #
  232. class ObjectTypeViewSet(ReadOnlyModelViewSet):
  233. """
  234. Read-only list of ObjectTypes.
  235. """
  236. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  237. queryset = ObjectType.objects.order_by('app_label', 'model')
  238. serializer_class = serializers.ObjectTypeSerializer
  239. filterset_class = filtersets.ObjectTypeFilterSet
  240. #
  241. # User dashboard
  242. #
  243. class DashboardView(RetrieveUpdateDestroyAPIView):
  244. queryset = Dashboard.objects.all()
  245. serializer_class = serializers.DashboardSerializer
  246. def get_object(self):
  247. return Dashboard.objects.filter(user=self.request.user).first()