import time from django import template from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Prefetch, Q from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import View from django_tables2 import RequestConfig from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.utils import copy_safe_request, shallow_compare_dict from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, ContentTypePermissionRequiredMixin, ) from virtualization.models import Cluster, ClusterGroup from . import filters, forms, tables from .choices import JobResultStatusChoices from .models import ConfigContext, ImageAttachment, ObjectChange, Report, JobResult, Script, Tag, TaggedItem from .reports import get_report, get_reports, run_report from .scripts import get_scripts, run_script # # Tags # class TagListView(ObjectListView): queryset = Tag.restricted_objects.annotate( items=Count('extras_taggeditem_items', distinct=True) ).order_by( 'name' ) filterset = filters.TagFilterSet filterset_form = forms.TagFilterForm table = tables.TagTable class TagEditView(ObjectEditView): queryset = Tag.restricted_objects.all() model_form = forms.TagForm template_name = 'extras/tag_edit.html' class TagDeleteView(ObjectDeleteView): queryset = Tag.restricted_objects.all() class TagBulkImportView(BulkImportView): queryset = Tag.restricted_objects.all() model_form = forms.TagCSVForm table = tables.TagTable class TagBulkEditView(BulkEditView): queryset = Tag.restricted_objects.annotate( items=Count('extras_taggeditem_items', distinct=True) ).order_by( 'name' ) table = tables.TagTable form = forms.TagBulkEditForm class TagBulkDeleteView(BulkDeleteView): queryset = Tag.restricted_objects.annotate( items=Count('extras_taggeditem_items') ).order_by( 'name' ) table = tables.TagTable # # Config contexts # class ConfigContextListView(ObjectListView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet filterset_form = forms.ConfigContextFilterForm table = tables.ConfigContextTable action_buttons = ('add',) class ConfigContextView(ObjectView): queryset = ConfigContext.objects.all() def get(self, request, pk): # Extend queryset to prefetch related objects self.queryset = self.queryset.prefetch_related( Prefetch('regions', queryset=Region.objects.restrict(request.user)), Prefetch('sites', queryset=Site.objects.restrict(request.user)), Prefetch('roles', queryset=DeviceRole.objects.restrict(request.user)), Prefetch('platforms', queryset=Platform.objects.restrict(request.user)), Prefetch('clusters', queryset=Cluster.objects.restrict(request.user)), Prefetch('cluster_groups', queryset=ClusterGroup.objects.restrict(request.user)), Prefetch('tenants', queryset=Tenant.objects.restrict(request.user)), Prefetch('tenant_groups', queryset=TenantGroup.objects.restrict(request.user)), ) configcontext = get_object_or_404(self.queryset, pk=pk) # Determine user's preferred output format if request.GET.get('format') in ['json', 'yaml']: format = request.GET.get('format') if request.user.is_authenticated: request.user.config.set('extras.configcontext.format', format, commit=True) elif request.user.is_authenticated: format = request.user.config.get('extras.configcontext.format', 'json') else: format = 'json' return render(request, 'extras/configcontext.html', { 'configcontext': configcontext, 'format': format, }) class ConfigContextEditView(ObjectEditView): queryset = ConfigContext.objects.all() model_form = forms.ConfigContextForm template_name = 'extras/configcontext_edit.html' class ConfigContextBulkEditView(BulkEditView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet table = tables.ConfigContextTable form = forms.ConfigContextBulkEditForm class ConfigContextDeleteView(ObjectDeleteView): queryset = ConfigContext.objects.all() class ConfigContextBulkDeleteView(BulkDeleteView): queryset = ConfigContext.objects.all() table = tables.ConfigContextTable class ObjectConfigContextView(ObjectView): base_template = None def get(self, request, pk): obj = get_object_or_404(self.queryset, pk=pk) source_contexts = ConfigContext.objects.restrict(request.user, 'view').get_for_object(obj) model_name = self.queryset.model._meta.model_name # Determine user's preferred output format if request.GET.get('format') in ['json', 'yaml']: format = request.GET.get('format') if request.user.is_authenticated: request.user.config.set('extras.configcontext.format', format, commit=True) elif request.user.is_authenticated: format = request.user.config.get('extras.configcontext.format', 'json') else: format = 'json' return render(request, 'extras/object_configcontext.html', { model_name: obj, 'obj': obj, 'rendered_context': obj.get_config_context(), 'source_contexts': source_contexts, 'format': format, 'base_template': self.base_template, 'active_tab': 'config-context', }) # # Change logging # class ObjectChangeListView(ObjectListView): queryset = ObjectChange.objects.prefetch_related('user', 'changed_object_type') filterset = filters.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable template_name = 'extras/objectchange_list.html' action_buttons = ('export',) class ObjectChangeView(ObjectView): queryset = ObjectChange.objects.all() def get(self, request, pk): objectchange = get_object_or_404(self.queryset, pk=pk) related_changes = ObjectChange.objects.restrict(request.user, 'view').filter( request_id=objectchange.request_id ).exclude( pk=objectchange.pk ) related_changes_table = tables.ObjectChangeTable( data=related_changes[:50], orderable=False ) objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter( changed_object_type=objectchange.changed_object_type, changed_object_id=objectchange.changed_object_id, ) next_change = objectchanges.filter(time__gt=objectchange.time).order_by('time').first() prev_change = objectchanges.filter(time__lt=objectchange.time).order_by('-time').first() if prev_change: diff_added = shallow_compare_dict( prev_change.object_data, objectchange.object_data, exclude=['last_updated'], ) diff_removed = {x: prev_change.object_data.get(x) for x in diff_added} else: # No previous change; this is the initial change that added the object diff_added = diff_removed = objectchange.object_data return render(request, 'extras/objectchange.html', { 'objectchange': objectchange, 'diff_added': diff_added, 'diff_removed': diff_removed, 'next_change': next_change, 'prev_change': prev_change, 'related_changes_table': related_changes_table, 'related_changes_count': related_changes.count() }) class ObjectChangeLogView(View): """ Present a history of changes made to a particular object. """ def get(self, request, model, **kwargs): # Handle QuerySet restriction of parent object if needed if hasattr(model.objects, 'restrict'): obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) else: obj = get_object_or_404(model, **kwargs) # Gather all changes for this object (and its related objects) content_type = ContentType.objects.get_for_model(model) objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related( 'user', 'changed_object_type' ).filter( Q(changed_object_type=content_type, changed_object_id=obj.pk) | Q(related_object_type=content_type, related_object_id=obj.pk) ) objectchanges_table = tables.ObjectChangeTable( data=objectchanges, orderable=False ) # Apply the request context paginate = { 'paginator_class': EnhancedPaginator, 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) } RequestConfig(request, paginate).configure(objectchanges_table) # Check whether a header template exists for this model base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name) try: template.loader.get_template(base_template) object_var = model._meta.model_name except template.TemplateDoesNotExist: base_template = 'base.html' object_var = 'obj' return render(request, 'extras/object_changelog.html', { object_var: obj, 'instance': obj, # We'll eventually standardize on 'instance` for the object variable name 'table': objectchanges_table, 'base_template': base_template, 'active_tab': 'changelog', }) # # Image attachments # class ImageAttachmentEditView(ObjectEditView): queryset = ImageAttachment.objects.all() model_form = forms.ImageAttachmentForm def alter_obj(self, imageattachment, request, args, kwargs): if not imageattachment.pk: # Assign the parent object based on URL kwargs model = kwargs.get('model') imageattachment.parent = get_object_or_404(model, pk=kwargs['object_id']) return imageattachment def get_return_url(self, request, imageattachment): return imageattachment.parent.get_absolute_url() class ImageAttachmentDeleteView(ObjectDeleteView): queryset = ImageAttachment.objects.all() def get_return_url(self, request, imageattachment): return imageattachment.parent.get_absolute_url() # # Reports # class ReportListView(ContentTypePermissionRequiredMixin, View): """ Retrieve all of the available reports from disk and the recorded JobResult (if any) for each. """ def get_required_permission(self): return 'extras.view_reportresult' def get(self, request): reports = get_reports() report_content_type = ContentType.objects.get(app_label='extras', model='report') results = { r.name: r for r in JobResult.objects.filter( obj_type=report_content_type, status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES ).defer('data') } ret = [] for module, report_list in reports: module_reports = [] for report in report_list: report.result = results.get(report.full_name, None) module_reports.append(report) ret.append((module, module_reports)) return render(request, 'extras/report_list.html', { 'reports': ret, }) class GetReportMixin: def _get_report(self, name, module=None): if module is None: module, name = name.split('.', 1) report = get_report(module, name) if report is None: raise Http404 return report class ReportView(GetReportMixin, ContentTypePermissionRequiredMixin, View): """ Display a single Report and its associated JobResult (if any). """ def get_required_permission(self): return 'extras.view_reportresult' def get(self, request, module, name): report = self._get_report(name, module) report_content_type = ContentType.objects.get(app_label='extras', model='report') report.result = JobResult.objects.filter( obj_type=report_content_type, name=report.full_name, status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES ).first() return render(request, 'extras/report.html', { 'report': report, 'run_form': ConfirmationForm(), }) def post(self, request, module, name): # Permissions check if not request.user.has_perm('extras.run_report'): return HttpResponseForbidden() report = self._get_report(name, module) form = ConfirmationForm(request.POST) if form.is_valid(): # Run the Report. A new JobResult is created. report_content_type = ContentType.objects.get(app_label='extras', model='report') job_result = JobResult.enqueue_job( run_report, report.full_name, report_content_type, request.user ) return redirect('extras:report_result', job_result_pk=job_result.pk) return render(request, 'extras/report.html', { 'report': report, 'run_form': form, }) class ReportResultView(ContentTypePermissionRequiredMixin, GetReportMixin, View): def get_required_permission(self): return 'extras.view_report' def get(self, request, job_result_pk): result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk) report_content_type = ContentType.objects.get(app_label='extras', model='report') if result.obj_type != report_content_type: raise Http404 report = self._get_report(result.name) return render(request, 'extras/report_result.html', { 'report': report, 'result': result, 'class_name': report.name, 'run_form': ConfirmationForm(), }) # # Scripts # class GetScriptMixin: def _get_script(self, name, module=None): if module is None: module, name = name.split('.', 1) scripts = get_scripts() try: return scripts[module][name]() except KeyError: raise Http404 class ScriptListView(ContentTypePermissionRequiredMixin, View): def get_required_permission(self): return 'extras.view_script' def get(self, request): scripts = get_scripts(use_names=True) script_content_type = ContentType.objects.get(app_label='extras', model='script') results = { r.name: r for r in JobResult.objects.filter( obj_type=script_content_type, status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES ).defer('data') } for _scripts in scripts.values(): for script in _scripts.values(): script.result = results.get(script.full_name) return render(request, 'extras/script_list.html', { 'scripts': scripts, }) class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): def get_required_permission(self): return 'extras.view_script' def get(self, request, module, name): script = self._get_script(name, module) form = script.as_form(initial=request.GET) # Look for a pending JobResult (use the latest one by creation timestamp) script_content_type = ContentType.objects.get(app_label='extras', model='script') script.result = JobResult.objects.filter( obj_type=script_content_type, name=script.full_name, ).exclude( status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES ).first() return render(request, 'extras/script.html', { 'module': module, 'script': script, 'form': form, }) def post(self, request, module, name): # Permissions check if not request.user.has_perm('extras.run_script'): return HttpResponseForbidden() script = self._get_script(name, module) form = script.as_form(request.POST, request.FILES) if form.is_valid(): commit = form.cleaned_data.pop('_commit') script_content_type = ContentType.objects.get(app_label='extras', model='script') job_result = JobResult.enqueue_job( run_script, script.full_name, script_content_type, request.user, data=form.cleaned_data, request=copy_safe_request(request), commit=commit ) return redirect('extras:script_result', job_result_pk=job_result.pk) return render(request, 'extras/script.html', { 'module': module, 'script': script, 'form': form, }) class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): def get_required_permission(self): return 'extras.view_script' def get(self, request, job_result_pk): result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk) script_content_type = ContentType.objects.get(app_label='extras', model='script') if result.obj_type != script_content_type: raise Http404 script = self._get_script(result.name) return render(request, 'extras/script_result.html', { 'script': script, 'result': result, 'class_name': script.__class__.__name__ })