diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 0eeab49ec..34e4312ac 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.models import Device, Rack, Site from extras.models import ( - ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction, + ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction, ) from users.api.serializers import NestedUserSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer @@ -127,6 +127,36 @@ class WritableImageAttachmentSerializer(ValidatedModelSerializer): return data +# +# Reports +# + +class ReportResultSerializer(serializers.ModelSerializer): + + class Meta: + model = ReportResult + fields = ['created', 'user', 'failed', 'data'] + + +class NestedReportResultSerializer(serializers.ModelSerializer): + + class Meta: + model = ReportResult + fields = ['created', 'user', 'failed'] + + +class ReportSerializer(serializers.Serializer): + module = serializers.CharField(max_length=255) + name = serializers.CharField(max_length=255) + description = serializers.CharField(max_length=255, required=False) + test_methods = serializers.ListField(child=serializers.CharField(max_length=255)) + result = NestedReportResultSerializer() + + +class ReportDetailSerializer(ReportSerializer): + result = ReportResultSerializer() + + # # User actions # diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index bf54a3220..27a806155 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,17 +1,16 @@ from __future__ import unicode_literals -from collections import OrderedDict from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet from django.contrib.contenttypes.models import ContentType -from django.http import HttpResponse +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from extras import filters -from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction -from extras.reports import get_reports +from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction +from extras.reports import get_report, get_reports from utilities.api import WritableSerializerMixin from . import serializers @@ -94,23 +93,76 @@ class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet): class ReportViewSet(ViewSet): _ignore_model_permissions = True exclude_from_schema = True + lookup_value_regex = '[^/]+' # Allow dots def list(self, request): - ret_list = [] + # Compile all reports + report_list = [] for module_name, reports in get_reports(): for report_name, report_cls in reports: - report = OrderedDict(( - ('module', module_name), - ('name', report_name), - ('description', report_cls.description), - ('test_methods', report_cls().test_methods), - )) - ret_list.append(report) + data = { + 'module': module_name, + 'name': report_name, + 'description': report_cls.description, + 'test_methods': report_cls().test_methods, + 'result': None, + } + try: + result = ReportResult.objects.defer('data').get(report='{}.{}'.format(module_name, report_name)) + data['result'] = result + except ReportResult.DoesNotExist: + pass + report_list.append(data) - return Response(ret_list) + serializer = serializers.ReportSerializer(report_list, many=True, context={'request': request}) + return Response(serializer.data) + def retrieve(self, request, pk): + + # Retrieve report by . + if '.' not in pk: + raise Http404 + module_name, report_name = pk.split('.', 1) + report_cls = get_report(module_name, report_name) + data = { + 'module': module_name, + 'name': report_name, + 'description': report_cls.description, + 'test_methods': report_cls().test_methods, + 'result': None, + } + + # Attach report result + try: + result = ReportResult.objects.get(report='{}.{}'.format(module_name, report_name)) + data['result'] = result + except ReportResult.DoesNotExist: + pass + + serializer = serializers.ReportDetailSerializer(data) + + return Response(serializer.data) + + @detail_route() + def run(self, request, pk): + + # Retrieve report by . + if '.' not in pk: + raise Http404 + module_name, report_name = pk.split('.', 1) + report_cls = get_report(module_name, report_name) + + # Run the report + report = report_cls() + result = report.run() + + # Save the ReportResult + ReportResult.objects.filter(report=pk).delete() + ReportResult(report=pk, failed=report.failed, data=result).save() + + return Response('Report completed.') class RecentActivityViewSet(ReadOnlyModelViewSet): diff --git a/netbox/extras/management/commands/runreport.py b/netbox/extras/management/commands/runreport.py index acffdc54d..70893b934 100644 --- a/netbox/extras/management/commands/runreport.py +++ b/netbox/extras/management/commands/runreport.py @@ -30,15 +30,15 @@ class Command(BaseCommand): "[{:%H:%M:%S}] Running {}.{}...".format(timezone.now(), module_name, report_name) ) report = report_cls() - results = report.run() + result = report.run() # Record the results ReportResult.objects.filter(report=report_name_full).delete() - ReportResult(report=report_name_full, failed=report.failed, data=results).save() + ReportResult(report=report_name_full, failed=report.failed, data=result).save() # Report on success/failure status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS') - for test_name, attrs in results.items(): + for test_name, attrs in result.items(): self.stdout.write( "\t{}: {} success, {} info, {} warning, {} failed".format( test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failed'] diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 99f42d9b8..1ded211ed 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -18,6 +18,14 @@ def is_report(obj): return False +def get_report(module_name, report_name): + """ + Return a specific report from within a module. + """ + module = importlib.import_module('reports.{}'.format(module_name)) + return getattr(module, report_name) + + def get_reports(): """ Compile a list of all reports available across all modules in the reports path. Returns a list of tuples: