views.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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, count_related
  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.prefetch_related('data_source', 'data_file')
  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.annotate(
  118. tagged_items=count_related(TaggedItem, 'tag')
  119. )
  120. serializer_class = serializers.TagSerializer
  121. filterset_class = filtersets.TagFilterSet
  122. #
  123. # Image attachments
  124. #
  125. class ImageAttachmentViewSet(NetBoxModelViewSet):
  126. metadata_class = ContentTypeMetadata
  127. queryset = ImageAttachment.objects.all()
  128. serializer_class = serializers.ImageAttachmentSerializer
  129. filterset_class = filtersets.ImageAttachmentFilterSet
  130. #
  131. # Journal entries
  132. #
  133. class JournalEntryViewSet(NetBoxModelViewSet):
  134. metadata_class = ContentTypeMetadata
  135. queryset = JournalEntry.objects.all()
  136. serializer_class = serializers.JournalEntrySerializer
  137. filterset_class = filtersets.JournalEntryFilterSet
  138. #
  139. # Config contexts
  140. #
  141. class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
  142. queryset = ConfigContext.objects.prefetch_related(
  143. 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
  144. 'data_file',
  145. )
  146. serializer_class = serializers.ConfigContextSerializer
  147. filterset_class = filtersets.ConfigContextFilterSet
  148. #
  149. # Config templates
  150. #
  151. class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
  152. queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
  153. serializer_class = serializers.ConfigTemplateSerializer
  154. filterset_class = filtersets.ConfigTemplateFilterSet
  155. @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
  156. def render(self, request, pk):
  157. """
  158. Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
  159. return the raw rendered content, rather than serialized JSON.
  160. """
  161. configtemplate = self.get_object()
  162. context = request.data
  163. return self.render_configtemplate(request, configtemplate, context)
  164. #
  165. # Scripts
  166. #
  167. class ScriptViewSet(ViewSet):
  168. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  169. _ignore_model_permissions = True
  170. schema = None
  171. lookup_value_regex = '[^/]+' # Allow dots
  172. def _get_script(self, pk):
  173. try:
  174. module_name, script_name = pk.split('.', maxsplit=1)
  175. except ValueError:
  176. raise Http404
  177. module, script = get_module_and_script(module_name, script_name)
  178. if script is None:
  179. raise Http404
  180. return module, script
  181. def list(self, request):
  182. results = {
  183. job.name: job
  184. for job in Job.objects.filter(
  185. object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
  186. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  187. ).order_by('name', '-created').distinct('name').defer('data')
  188. }
  189. script_list = []
  190. for script_module in ScriptModule.objects.restrict(request.user):
  191. script_list.extend(script_module.scripts.values())
  192. # Attach Job objects to each script (if any)
  193. for script in script_list:
  194. script.result = results.get(script.class_name, None)
  195. serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
  196. return Response({'count': len(script_list), 'results': serializer.data})
  197. def retrieve(self, request, pk):
  198. module, script = self._get_script(pk)
  199. object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
  200. script.result = Job.objects.filter(
  201. object_type=object_type,
  202. name=script.class_name,
  203. status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
  204. ).first()
  205. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  206. return Response(serializer.data)
  207. def post(self, request, pk):
  208. """
  209. Run a Script identified as "<module>.<script>" and return the pending Job as the result
  210. """
  211. if not request.user.has_perm('extras.run_script'):
  212. raise PermissionDenied("This user does not have permission to run scripts.")
  213. module, script = self._get_script(pk)
  214. input_serializer = serializers.ScriptInputSerializer(
  215. data=request.data,
  216. context={'script': script}
  217. )
  218. # Check that at least one RQ worker is running
  219. if not Worker.count(get_connection('default')):
  220. raise RQWorkerNotRunningException()
  221. if input_serializer.is_valid():
  222. script.result = Job.enqueue(
  223. run_script,
  224. instance=module,
  225. name=script.class_name,
  226. user=request.user,
  227. data=input_serializer.data['data'],
  228. request=copy_safe_request(request),
  229. commit=input_serializer.data['commit'],
  230. job_timeout=script.job_timeout,
  231. schedule_at=input_serializer.validated_data.get('schedule_at'),
  232. interval=input_serializer.validated_data.get('interval')
  233. )
  234. serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
  235. return Response(serializer.data)
  236. return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  237. #
  238. # Change logging
  239. #
  240. class ObjectChangeViewSet(ReadOnlyModelViewSet):
  241. """
  242. Retrieve a list of recent changes.
  243. """
  244. metadata_class = ContentTypeMetadata
  245. queryset = ObjectChange.objects.valid_models().prefetch_related('user')
  246. serializer_class = serializers.ObjectChangeSerializer
  247. filterset_class = filtersets.ObjectChangeFilterSet
  248. #
  249. # ContentTypes
  250. #
  251. class ContentTypeViewSet(ReadOnlyModelViewSet):
  252. """
  253. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
  254. """
  255. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  256. queryset = ContentType.objects.order_by('app_label', 'model')
  257. serializer_class = serializers.ContentTypeSerializer
  258. filterset_class = filtersets.ContentTypeFilterSet
  259. #
  260. # User dashboard
  261. #
  262. class DashboardView(RetrieveUpdateDestroyAPIView):
  263. queryset = Dashboard.objects.all()
  264. serializer_class = serializers.DashboardSerializer
  265. def get_object(self):
  266. return Dashboard.objects.filter(user=self.request.user).first()