views.py 18 KB

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