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.utils.safestring import mark_safe 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 shallow_compare_dict from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin, ) from virtualization.models import Cluster, ClusterGroup from . import filters, forms, tables from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem from .reports import get_report, get_reports from .scripts import get_scripts, run_script # # Tags # class TagListView(ObjectListView): queryset = Tag.restricted.annotate( items=Count('extras_taggeditem_items', distinct=True) ).order_by( 'name' ) filterset = filters.TagFilterSet filterset_form = forms.TagFilterForm table = tables.TagTable class TagView(ObjectView): queryset = Tag.restricted.all() def get(self, request, slug): tag = get_object_or_404(self.queryset, slug=slug) tagged_items = TaggedItem.objects.filter( tag=tag ).prefetch_related( 'content_type', 'content_object' ) # Generate a table of all items tagged with this Tag items_table = tables.TaggedItemTable(tagged_items) paginate = { 'paginator_class': EnhancedPaginator, 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) } RequestConfig(request, paginate).configure(items_table) return render(request, 'extras/tag.html', { 'tag': tag, 'items_count': tagged_items.count(), 'items_table': items_table, }) class TagEditView(ObjectEditView): queryset = Tag.restricted.all() model_form = forms.TagForm default_return_url = 'extras:tag_list' template_name = 'extras/tag_edit.html' class TagDeleteView(ObjectDeleteView): queryset = Tag.restricted.all() default_return_url = 'extras:tag_list' class TagBulkImportView(BulkImportView): queryset = Tag.restricted.all() model_form = forms.TagCSVForm table = tables.TagTable default_return_url = 'extras:tag_list' class TagBulkEditView(BulkEditView): queryset = Tag.restricted.annotate( items=Count('extras_taggeditem_items', distinct=True) ).order_by( 'name' ) table = tables.TagTable form = forms.TagBulkEditForm default_return_url = 'extras:tag_list' class TagBulkDeleteView(BulkDeleteView): queryset = Tag.restricted.annotate( items=Count('extras_taggeditem_items') ).order_by( 'name' ) table = tables.TagTable default_return_url = 'extras:tag_list' # # 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 default_return_url = 'extras:configcontext_list' template_name = 'extras/configcontext_edit.html' class ConfigContextBulkEditView(BulkEditView): queryset = ConfigContext.objects.all() filterset = filters.ConfigContextFilterSet table = tables.ConfigContextTable form = forms.ConfigContextBulkEditForm default_return_url = 'extras:configcontext_list' class ConfigContextDeleteView(ObjectDeleteView): queryset = ConfigContext.objects.all() default_return_url = 'extras:configcontext_list' class ConfigContextBulkDeleteView(BulkDeleteView): queryset = ConfigContext.objects.all() table = tables.ConfigContextTable default_return_url = 'extras:configcontext_list' 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(ObjectPermissionRequiredMixin, View): """ Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each. """ def get_required_permission(self): return 'extras.view_reportresult' def get(self, request): reports = get_reports() results = {r.report: r for r in ReportResult.objects.all()} 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 ReportView(ObjectPermissionRequiredMixin, View): """ Display a single Report and its associated ReportResult (if any). """ def get_required_permission(self): return 'extras.view_reportresult' def get(self, request, name): # Retrieve the Report by "." module_name, report_name = name.split('.') report = get_report(module_name, report_name) if report is None: raise Http404 # Attach the ReportResult (if any) report.result = ReportResult.objects.filter(report=report.full_name).first() return render(request, 'extras/report.html', { 'report': report, 'run_form': ConfirmationForm(), }) class ReportRunView(ObjectPermissionRequiredMixin, View): """ Run a Report and record a new ReportResult. """ def get_required_permission(self): return 'extras.add_reportresult' def post(self, request, name): # Retrieve the Report by "." module_name, report_name = name.split('.') report = get_report(module_name, report_name) if report is None: raise Http404 form = ConfirmationForm(request.POST) if form.is_valid(): # Run the Report. A new ReportResult is created. report.run() result = 'failed' if report.failed else 'passed' msg = "Ran report {} ({})".format(report.full_name, result) messages.success(request, mark_safe(msg)) return redirect('extras:report', name=report.full_name) # # Scripts # class ScriptListView(ObjectPermissionRequiredMixin, View): def get_required_permission(self): return 'extras.view_script' def get(self, request): return render(request, 'extras/script_list.html', { 'scripts': get_scripts(use_names=True), }) class ScriptView(ObjectPermissionRequiredMixin, View): def get_required_permission(self): return 'extras.view_script' def _get_script(self, module, name): scripts = get_scripts() try: return scripts[module][name]() except KeyError: raise Http404 def get(self, request, module, name): script = self._get_script(module, name) form = script.as_form(initial=request.GET) 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(module, name) form = script.as_form(request.POST, request.FILES) output = None execution_time = None if form.is_valid(): commit = form.cleaned_data.pop('_commit') output, execution_time = run_script(script, form.cleaned_data, request, commit) return render(request, 'extras/script.html', { 'module': module, 'script': script, 'form': form, 'output': output, 'execution_time': execution_time, })