from django import template from django.conf import settings from django.contrib import messages 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_rq.queues import get_connection from django_tables2 import RequestConfig from rq import Worker 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, JobResult, Tag 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) # Allow execution only if RQ worker process is running if not Worker.count(get_connection('default')): messages.error(request, "Unable to run report: RQ worker process not running.") elif 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) # Allow execution only if RQ worker process is running if not Worker.count(get_connection('default')): messages.error(request, "Unable to run script: RQ worker process not running.") elif 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__ })