views.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. from django.conf import settings
  2. from django.http import Http404
  3. from django.shortcuts import get_object_or_404
  4. from django.views.static import serve
  5. from django_rq.queues import get_connection
  6. from drf_spectacular.utils import extend_schema, extend_schema_view
  7. from rest_framework import status
  8. from rest_framework.decorators import action
  9. from rest_framework.exceptions import PermissionDenied
  10. from rest_framework.generics import RetrieveUpdateDestroyAPIView
  11. from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
  12. from rest_framework.renderers import JSONRenderer
  13. from rest_framework.response import Response
  14. from rest_framework.routers import APIRootView
  15. from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
  16. from rq import Worker
  17. from core.models import ObjectType
  18. from extras import filtersets
  19. from extras.jobs import ScriptJob
  20. from extras.models import *
  21. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  22. from netbox.api.features import SyncedDataMixin
  23. from netbox.api.metadata import ContentTypeMetadata
  24. from netbox.api.renderers import TextRenderer
  25. from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
  26. from utilities.exceptions import RQWorkerNotRunningException
  27. from utilities.request import copy_safe_request
  28. from . import serializers
  29. from .mixins import ConfigTemplateRenderMixin
  30. class ExtrasRootView(APIRootView):
  31. """
  32. Extras API root view
  33. """
  34. def get_view_name(self):
  35. return 'Extras'
  36. #
  37. # EventRules
  38. #
  39. class EventRuleViewSet(NetBoxModelViewSet):
  40. metadata_class = ContentTypeMetadata
  41. queryset = EventRule.objects.all()
  42. serializer_class = serializers.EventRuleSerializer
  43. filterset_class = filtersets.EventRuleFilterSet
  44. #
  45. # Webhooks
  46. #
  47. class WebhookViewSet(NetBoxModelViewSet):
  48. metadata_class = ContentTypeMetadata
  49. queryset = Webhook.objects.all()
  50. serializer_class = serializers.WebhookSerializer
  51. filterset_class = filtersets.WebhookFilterSet
  52. #
  53. # Custom fields
  54. #
  55. class CustomFieldViewSet(NetBoxModelViewSet):
  56. metadata_class = ContentTypeMetadata
  57. queryset = CustomField.objects.select_related('choice_set')
  58. serializer_class = serializers.CustomFieldSerializer
  59. filterset_class = filtersets.CustomFieldFilterSet
  60. class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
  61. queryset = CustomFieldChoiceSet.objects.all()
  62. serializer_class = serializers.CustomFieldChoiceSetSerializer
  63. filterset_class = filtersets.CustomFieldChoiceSetFilterSet
  64. @action(detail=True)
  65. def choices(self, request, pk):
  66. """
  67. Provides an endpoint to iterate through each choice in a set.
  68. """
  69. choiceset = get_object_or_404(self.queryset, pk=pk)
  70. choices = choiceset.choices
  71. # Enable filtering
  72. if q := request.GET.get('q'):
  73. q = q.lower()
  74. choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
  75. # Paginate data
  76. if page := self.paginate_queryset(choices):
  77. data = [
  78. {'id': c[0], 'display': c[1]} for c in page
  79. ]
  80. else:
  81. data = []
  82. return self.get_paginated_response(data)
  83. #
  84. # Custom links
  85. #
  86. class CustomLinkViewSet(NetBoxModelViewSet):
  87. metadata_class = ContentTypeMetadata
  88. queryset = CustomLink.objects.all()
  89. serializer_class = serializers.CustomLinkSerializer
  90. filterset_class = filtersets.CustomLinkFilterSet
  91. #
  92. # Export templates
  93. #
  94. class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
  95. metadata_class = ContentTypeMetadata
  96. queryset = ExportTemplate.objects.all()
  97. serializer_class = serializers.ExportTemplateSerializer
  98. filterset_class = filtersets.ExportTemplateFilterSet
  99. #
  100. # Saved filters
  101. #
  102. class SavedFilterViewSet(NetBoxModelViewSet):
  103. metadata_class = ContentTypeMetadata
  104. queryset = SavedFilter.objects.all()
  105. serializer_class = serializers.SavedFilterSerializer
  106. filterset_class = filtersets.SavedFilterFilterSet
  107. #
  108. # Table Configs
  109. #
  110. class TableConfigViewSet(NetBoxModelViewSet):
  111. metadata_class = ContentTypeMetadata
  112. queryset = TableConfig.objects.all()
  113. serializer_class = serializers.TableConfigSerializer
  114. filterset_class = filtersets.TableConfigFilterSet
  115. #
  116. # Bookmarks
  117. #
  118. class BookmarkViewSet(NetBoxModelViewSet):
  119. metadata_class = ContentTypeMetadata
  120. queryset = Bookmark.objects.all()
  121. serializer_class = serializers.BookmarkSerializer
  122. filterset_class = filtersets.BookmarkFilterSet
  123. #
  124. # Notifications & subscriptions
  125. #
  126. class NotificationViewSet(NetBoxModelViewSet):
  127. metadata_class = ContentTypeMetadata
  128. queryset = Notification.objects.all()
  129. serializer_class = serializers.NotificationSerializer
  130. class NotificationGroupViewSet(NetBoxModelViewSet):
  131. queryset = NotificationGroup.objects.all()
  132. serializer_class = serializers.NotificationGroupSerializer
  133. class SubscriptionViewSet(NetBoxModelViewSet):
  134. metadata_class = ContentTypeMetadata
  135. queryset = Subscription.objects.all()
  136. serializer_class = serializers.SubscriptionSerializer
  137. #
  138. # Tags
  139. #
  140. class TagViewSet(NetBoxModelViewSet):
  141. queryset = Tag.objects.all()
  142. serializer_class = serializers.TagSerializer
  143. filterset_class = filtersets.TagFilterSet
  144. class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
  145. queryset = TaggedItem.objects.prefetch_related('content_type', 'content_object', 'tag')
  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. @action(
  157. methods=['GET'],
  158. detail=True,
  159. url_path='download',
  160. url_name='download',
  161. )
  162. def download(self, request, pk, *args, **kwargs):
  163. obj = get_object_or_404(self.queryset, pk=pk)
  164. # Render and return the elevation as an SVG drawing with the correct content type
  165. return serve(request, obj.image.path, document_root=settings.MEDIA_ROOT)
  166. #
  167. # Journal entries
  168. #
  169. class JournalEntryViewSet(NetBoxModelViewSet):
  170. metadata_class = ContentTypeMetadata
  171. queryset = JournalEntry.objects.all()
  172. serializer_class = serializers.JournalEntrySerializer
  173. filterset_class = filtersets.JournalEntryFilterSet
  174. #
  175. # Config contexts
  176. #
  177. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  178. queryset = ConfigContext.objects.all()
  179. serializer_class = serializers.ConfigContextSerializer
  180. filterset_class = filtersets.ConfigContextFilterSet
  181. #
  182. # Config templates
  183. #
  184. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  185. queryset = ConfigTemplate.objects.all()
  186. serializer_class = serializers.ConfigTemplateSerializer
  187. filterset_class = filtersets.ConfigTemplateFilterSet
  188. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  189. def render(self, request, pk):
  190. """
  191. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  192. return the raw rendered content, rather than serialized JSON.
  193. """
  194. configtemplate = self.get_object()
  195. context = request.data
  196. return self.render_configtemplate(request, configtemplate, context)
  197. #
  198. # Scripts
  199. #
  200. @extend_schema_view(
  201. update=extend_schema(request=serializers.ScriptInputSerializer),
  202. partial_update=extend_schema(request=serializers.ScriptInputSerializer),
  203. )
  204. class ScriptViewSet(ModelViewSet):
  205. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  206. queryset = Script.objects.all()
  207. serializer_class = serializers.ScriptSerializer
  208. filterset_class = filtersets.ScriptFilterSet
  209. _ignore_model_permissions = True
  210. lookup_value_regex = '[^/]+' # Allow dots
  211. def _get_script(self, pk):
  212. # If pk is numeric, retrieve script by ID
  213. if pk.isnumeric():
  214. return get_object_or_404(self.queryset, pk=pk)
  215. # Default to retrieval by module & name
  216. try:
  217. module_name, script_name = pk.split('.', maxsplit=1)
  218. except ValueError:
  219. raise Http404
  220. return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
  221. def retrieve(self, request, pk):
  222. script = self._get_script(pk)
  223. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  224. return Response(serializer.data)
  225. def post(self, request, pk):
  226. """
  227. Run a Script identified by its numeric PK or module & name and return the pending Job as the result
  228. """
  229. if not request.user.has_perm('extras.run_script'):
  230. raise PermissionDenied("This user does not have permission to run scripts.")
  231. script = self._get_script(pk)
  232. input_serializer = serializers.ScriptInputSerializer(
  233. data=request.data,
  234. context={'script': script}
  235. )
  236. # Check that at least one RQ worker is running
  237. if not Worker.count(get_connection('default')):
  238. raise RQWorkerNotRunningException()
  239. if input_serializer.is_valid():
  240. ScriptJob.enqueue(
  241. instance=script,
  242. user=request.user,
  243. data=input_serializer.data['data'],
  244. request=copy_safe_request(request),
  245. commit=input_serializer.data['commit'],
  246. job_timeout=script.python_class.job_timeout,
  247. schedule_at=input_serializer.validated_data.get('schedule_at'),
  248. interval=input_serializer.validated_data.get('interval')
  249. )
  250. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  251. return Response(serializer.data)
  252. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  253. #
  254. # Object types
  255. #
  256. class ObjectTypeViewSet(ReadOnlyModelViewSet):
  257. """
  258. Read-only list of ObjectTypes.
  259. """
  260. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  261. queryset = ObjectType.objects.order_by('app_label', 'model')
  262. serializer_class = serializers.ObjectTypeSerializer
  263. filterset_class = filtersets.ObjectTypeFilterSet
  264. #
  265. # User dashboard
  266. #
  267. class DashboardView(RetrieveUpdateDestroyAPIView):
  268. queryset = Dashboard.objects.all()
  269. serializer_class = serializers.DashboardSerializer
  270. def get_object(self):
  271. return Dashboard.objects.filter(user=self.request.user).first()