views.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. from django import template
  2. from django.contrib import messages
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.db.models import Prefetch, Q
  5. from django.http import Http404, HttpResponseForbidden
  6. from django.shortcuts import get_object_or_404, redirect, render
  7. from django.views.generic import View
  8. from django_rq.queues import get_connection
  9. from django_tables2 import RequestConfig
  10. from rq import Worker
  11. from dcim.models import DeviceRole, Platform, Region, Site
  12. from tenancy.models import Tenant, TenantGroup
  13. from utilities.forms import ConfirmationForm
  14. from utilities.paginator import EnhancedPaginator, get_paginate_count
  15. from utilities.utils import copy_safe_request, get_subquery, shallow_compare_dict
  16. from utilities.views import (
  17. BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
  18. ContentTypePermissionRequiredMixin,
  19. )
  20. from virtualization.models import Cluster, ClusterGroup
  21. from . import filters, forms, tables
  22. from .choices import JobResultStatusChoices
  23. from .models import ConfigContext, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem
  24. from .reports import get_report, get_reports, run_report
  25. from .scripts import get_scripts, run_script
  26. #
  27. # Tags
  28. #
  29. class TagListView(ObjectListView):
  30. queryset = Tag.objects.annotate(
  31. items=get_subquery(TaggedItem, 'tag')
  32. )
  33. filterset = filters.TagFilterSet
  34. filterset_form = forms.TagFilterForm
  35. table = tables.TagTable
  36. class TagEditView(ObjectEditView):
  37. queryset = Tag.objects.all()
  38. model_form = forms.TagForm
  39. template_name = 'extras/tag_edit.html'
  40. class TagDeleteView(ObjectDeleteView):
  41. queryset = Tag.objects.all()
  42. class TagBulkImportView(BulkImportView):
  43. queryset = Tag.objects.all()
  44. model_form = forms.TagCSVForm
  45. table = tables.TagTable
  46. class TagBulkEditView(BulkEditView):
  47. queryset = Tag.objects.annotate(
  48. items=get_subquery(TaggedItem, 'tag')
  49. )
  50. table = tables.TagTable
  51. form = forms.TagBulkEditForm
  52. class TagBulkDeleteView(BulkDeleteView):
  53. queryset = Tag.objects.annotate(
  54. items=get_subquery(TaggedItem, 'tag')
  55. )
  56. table = tables.TagTable
  57. #
  58. # Config contexts
  59. #
  60. class ConfigContextListView(ObjectListView):
  61. queryset = ConfigContext.objects.all()
  62. filterset = filters.ConfigContextFilterSet
  63. filterset_form = forms.ConfigContextFilterForm
  64. table = tables.ConfigContextTable
  65. action_buttons = ('add',)
  66. class ConfigContextView(ObjectView):
  67. queryset = ConfigContext.objects.all()
  68. def get(self, request, pk):
  69. # Extend queryset to prefetch related objects
  70. self.queryset = self.queryset.prefetch_related(
  71. Prefetch('regions', queryset=Region.objects.restrict(request.user)),
  72. Prefetch('sites', queryset=Site.objects.restrict(request.user)),
  73. Prefetch('roles', queryset=DeviceRole.objects.restrict(request.user)),
  74. Prefetch('platforms', queryset=Platform.objects.restrict(request.user)),
  75. Prefetch('clusters', queryset=Cluster.objects.restrict(request.user)),
  76. Prefetch('cluster_groups', queryset=ClusterGroup.objects.restrict(request.user)),
  77. Prefetch('tenants', queryset=Tenant.objects.restrict(request.user)),
  78. Prefetch('tenant_groups', queryset=TenantGroup.objects.restrict(request.user)),
  79. )
  80. configcontext = get_object_or_404(self.queryset, pk=pk)
  81. # Determine user's preferred output format
  82. if request.GET.get('format') in ['json', 'yaml']:
  83. format = request.GET.get('format')
  84. if request.user.is_authenticated:
  85. request.user.config.set('extras.configcontext.format', format, commit=True)
  86. elif request.user.is_authenticated:
  87. format = request.user.config.get('extras.configcontext.format', 'json')
  88. else:
  89. format = 'json'
  90. return render(request, 'extras/configcontext.html', {
  91. 'configcontext': configcontext,
  92. 'format': format,
  93. })
  94. class ConfigContextEditView(ObjectEditView):
  95. queryset = ConfigContext.objects.all()
  96. model_form = forms.ConfigContextForm
  97. template_name = 'extras/configcontext_edit.html'
  98. class ConfigContextBulkEditView(BulkEditView):
  99. queryset = ConfigContext.objects.all()
  100. filterset = filters.ConfigContextFilterSet
  101. table = tables.ConfigContextTable
  102. form = forms.ConfigContextBulkEditForm
  103. class ConfigContextDeleteView(ObjectDeleteView):
  104. queryset = ConfigContext.objects.all()
  105. class ConfigContextBulkDeleteView(BulkDeleteView):
  106. queryset = ConfigContext.objects.all()
  107. table = tables.ConfigContextTable
  108. class ObjectConfigContextView(ObjectView):
  109. base_template = None
  110. def get(self, request, pk):
  111. obj = get_object_or_404(self.queryset, pk=pk)
  112. source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(obj)
  113. model_name = self.queryset.model._meta.model_name
  114. # Determine user's preferred output format
  115. if request.GET.get('format') in ['json', 'yaml']:
  116. format = request.GET.get('format')
  117. if request.user.is_authenticated:
  118. request.user.config.set('extras.configcontext.format', format, commit=True)
  119. elif request.user.is_authenticated:
  120. format = request.user.config.get('extras.configcontext.format', 'json')
  121. else:
  122. format = 'json'
  123. return render(request, 'extras/object_configcontext.html', {
  124. model_name: obj,
  125. 'obj': obj,
  126. 'rendered_context': obj.get_config_context(),
  127. 'source_contexts': source_contexts,
  128. 'format': format,
  129. 'base_template': self.base_template,
  130. 'active_tab': 'config-context',
  131. })
  132. #
  133. # Change logging
  134. #
  135. class ObjectChangeListView(ObjectListView):
  136. queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type')
  137. filterset = filters.ObjectChangeFilterSet
  138. filterset_form = forms.ObjectChangeFilterForm
  139. table = tables.ObjectChangeTable
  140. template_name = 'extras/objectchange_list.html'
  141. action_buttons = ('export',)
  142. class ObjectChangeView(ObjectView):
  143. queryset = ObjectChange.objects.all()
  144. def get(self, request, pk):
  145. objectchange = get_object_or_404(self.queryset, pk=pk)
  146. related_changes = ObjectChange.objects.restrict(request.user, 'view').filter(
  147. request_id=objectchange.request_id
  148. ).exclude(
  149. pk=objectchange.pk
  150. )
  151. related_changes_table = tables.ObjectChangeTable(
  152. data=related_changes[:50],
  153. orderable=False
  154. )
  155. objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter(
  156. changed_object_type=objectchange.changed_object_type,
  157. changed_object_id=objectchange.changed_object_id,
  158. )
  159. next_change = objectchanges.filter(time__gt=objectchange.time).order_by('time').first()
  160. prev_change = objectchanges.filter(time__lt=objectchange.time).order_by('-time').first()
  161. if prev_change:
  162. diff_added = shallow_compare_dict(
  163. prev_change.object_data,
  164. objectchange.object_data,
  165. exclude=['last_updated'],
  166. )
  167. diff_removed = {x: prev_change.object_data.get(x) for x in diff_added}
  168. else:
  169. # No previous change; this is the initial change that added the object
  170. diff_added = diff_removed = objectchange.object_data
  171. return render(request, 'extras/objectchange.html', {
  172. 'objectchange': objectchange,
  173. 'diff_added': diff_added,
  174. 'diff_removed': diff_removed,
  175. 'next_change': next_change,
  176. 'prev_change': prev_change,
  177. 'related_changes_table': related_changes_table,
  178. 'related_changes_count': related_changes.count()
  179. })
  180. class ObjectChangeLogView(View):
  181. """
  182. Present a history of changes made to a particular object.
  183. """
  184. def get(self, request, model, **kwargs):
  185. # Handle QuerySet restriction of parent object if needed
  186. if hasattr(model.objects, 'restrict'):
  187. obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
  188. else:
  189. obj = get_object_or_404(model, **kwargs)
  190. # Gather all changes for this object (and its related objects)
  191. content_type = ContentType.objects.get_for_model(model)
  192. objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related(
  193. 'user', 'changed_object_type'
  194. ).filter(
  195. Q(changed_object_type=content_type, changed_object_id=obj.pk) |
  196. Q(related_object_type=content_type, related_object_id=obj.pk)
  197. )
  198. objectchanges_table = tables.ObjectChangeTable(
  199. data=objectchanges,
  200. orderable=False
  201. )
  202. # Apply the request context
  203. paginate = {
  204. 'paginator_class': EnhancedPaginator,
  205. 'per_page': get_paginate_count(request)
  206. }
  207. RequestConfig(request, paginate).configure(objectchanges_table)
  208. # Check whether a header template exists for this model
  209. base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
  210. try:
  211. template.loader.get_template(base_template)
  212. object_var = model._meta.model_name
  213. except template.TemplateDoesNotExist:
  214. base_template = 'base.html'
  215. object_var = 'obj'
  216. return render(request, 'extras/object_changelog.html', {
  217. object_var: obj,
  218. 'instance': obj, # We'll eventually standardize on 'instance` for the object variable name
  219. 'table': objectchanges_table,
  220. 'base_template': base_template,
  221. 'active_tab': 'changelog',
  222. })
  223. #
  224. # Image attachments
  225. #
  226. class ImageAttachmentEditView(ObjectEditView):
  227. queryset = ImageAttachment.objects.all()
  228. model_form = forms.ImageAttachmentForm
  229. def alter_obj(self, imageattachment, request, args, kwargs):
  230. if not imageattachment.pk:
  231. # Assign the parent object based on URL kwargs
  232. model = kwargs.get('model')
  233. imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id'])
  234. return imageattachment
  235. def get_return_url(self, request, imageattachment):
  236. return imageattachment.parent.get_absolute_url()
  237. class ImageAttachmentDeleteView(ObjectDeleteView):
  238. queryset = ImageAttachment.objects.all()
  239. def get_return_url(self, request, imageattachment):
  240. return imageattachment.parent.get_absolute_url()
  241. #
  242. # Reports
  243. #
  244. class ReportListView(ContentTypePermissionRequiredMixin, View):
  245. """
  246. Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
  247. """
  248. def get_required_permission(self):
  249. return 'extras.view_report'
  250. def get(self, request):
  251. reports = get_reports()
  252. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  253. results = {
  254. r.name: r
  255. for r in JobResult.objects.filter(
  256. obj_type=report_content_type,
  257. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  258. ).defer('data')
  259. }
  260. ret = []
  261. for module, report_list in reports:
  262. module_reports = []
  263. for report in report_list:
  264. report.result = results.get(report.full_name, None)
  265. module_reports.append(report)
  266. ret.append((module, module_reports))
  267. return render(request, 'extras/report_list.html', {
  268. 'reports': ret,
  269. })
  270. class ReportView(ContentTypePermissionRequiredMixin, View):
  271. """
  272. Display a single Report and its associated JobResult (if any).
  273. """
  274. def get_required_permission(self):
  275. return 'extras.view_report'
  276. def get(self, request, module, name):
  277. report = get_report(module, name)
  278. if report is None:
  279. raise Http404
  280. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  281. report.result = JobResult.objects.filter(
  282. obj_type=report_content_type,
  283. name=report.full_name,
  284. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  285. ).first()
  286. return render(request, 'extras/report.html', {
  287. 'report': report,
  288. 'run_form': ConfirmationForm(),
  289. })
  290. def post(self, request, module, name):
  291. # Permissions check
  292. if not request.user.has_perm('extras.run_report'):
  293. return HttpResponseForbidden()
  294. report = get_report(module, name)
  295. if report is None:
  296. raise Http404
  297. # Allow execution only if RQ worker process is running
  298. if not Worker.count(get_connection('default')):
  299. messages.error(request, "Unable to run report: RQ worker process not running.")
  300. return render(request, 'extras/report.html', {
  301. 'report': report,
  302. })
  303. # Run the Report. A new JobResult is created.
  304. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  305. job_result = JobResult.enqueue_job(
  306. run_report,
  307. report.full_name,
  308. report_content_type,
  309. request.user
  310. )
  311. return redirect('extras:report_result', job_result_pk=job_result.pk)
  312. class ReportResultView(ContentTypePermissionRequiredMixin, View):
  313. """
  314. Display a JobResult pertaining to the execution of a Report.
  315. """
  316. def get_required_permission(self):
  317. return 'extras.view_report'
  318. def get(self, request, job_result_pk):
  319. report_content_type = ContentType.objects.get(app_label='extras', model='report')
  320. jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
  321. # Retrieve the Report and attach the JobResult to it
  322. module, report_name = jobresult.name.split('.')
  323. report = get_report(module, report_name)
  324. report.result = jobresult
  325. return render(request, 'extras/report_result.html', {
  326. 'report': report,
  327. 'result': jobresult,
  328. })
  329. #
  330. # Scripts
  331. #
  332. class GetScriptMixin:
  333. def _get_script(self, name, module=None):
  334. if module is None:
  335. module, name = name.split('.', 1)
  336. scripts = get_scripts()
  337. try:
  338. return scripts[module][name]()
  339. except KeyError:
  340. raise Http404
  341. class ScriptListView(ContentTypePermissionRequiredMixin, View):
  342. def get_required_permission(self):
  343. return 'extras.view_script'
  344. def get(self, request):
  345. scripts = get_scripts(use_names=True)
  346. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  347. results = {
  348. r.name: r
  349. for r in JobResult.objects.filter(
  350. obj_type=script_content_type,
  351. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  352. ).defer('data')
  353. }
  354. for _scripts in scripts.values():
  355. for script in _scripts.values():
  356. script.result = results.get(script.full_name)
  357. return render(request, 'extras/script_list.html', {
  358. 'scripts': scripts,
  359. })
  360. class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
  361. def get_required_permission(self):
  362. return 'extras.view_script'
  363. def get(self, request, module, name):
  364. script = self._get_script(name, module)
  365. form = script.as_form(initial=request.GET)
  366. # Look for a pending JobResult (use the latest one by creation timestamp)
  367. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  368. script.result = JobResult.objects.filter(
  369. obj_type=script_content_type,
  370. name=script.full_name,
  371. ).exclude(
  372. status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
  373. ).first()
  374. return render(request, 'extras/script.html', {
  375. 'module': module,
  376. 'script': script,
  377. 'form': form,
  378. })
  379. def post(self, request, module, name):
  380. # Permissions check
  381. if not request.user.has_perm('extras.run_script'):
  382. return HttpResponseForbidden()
  383. script = self._get_script(name, module)
  384. form = script.as_form(request.POST, request.FILES)
  385. # Allow execution only if RQ worker process is running
  386. if not Worker.count(get_connection('default')):
  387. messages.error(request, "Unable to run script: RQ worker process not running.")
  388. elif form.is_valid():
  389. commit = form.cleaned_data.pop('_commit')
  390. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  391. job_result = JobResult.enqueue_job(
  392. run_script,
  393. script.full_name,
  394. script_content_type,
  395. request.user,
  396. data=form.cleaned_data,
  397. request=copy_safe_request(request),
  398. commit=commit
  399. )
  400. return redirect('extras:script_result', job_result_pk=job_result.pk)
  401. return render(request, 'extras/script.html', {
  402. 'module': module,
  403. 'script': script,
  404. 'form': form,
  405. })
  406. class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View):
  407. def get_required_permission(self):
  408. return 'extras.view_script'
  409. def get(self, request, job_result_pk):
  410. result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk)
  411. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  412. if result.obj_type != script_content_type:
  413. raise Http404
  414. script = self._get_script(result.name)
  415. return render(request, 'extras/script_result.html', {
  416. 'script': script,
  417. 'result': result,
  418. 'class_name': script.__class__.__name__
  419. })