1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on reports API

This commit is contained in:
Jeremy Stretch
2017-09-21 16:32:05 -04:00
parent 16d1f9aca8
commit b5ab498e75
9 changed files with 199 additions and 44 deletions

View File

@ -28,6 +28,9 @@ router.register(r'topology-maps', views.TopologyMapViewSet)
# Image attachments # Image attachments
router.register(r'image-attachments', views.ImageAttachmentViewSet) router.register(r'image-attachments', views.ImageAttachmentViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, base_name='report')
# Recent activity # Recent activity
router.register(r'recent-activity', views.RecentActivityViewSet) router.register(r'recent-activity', views.RecentActivityViewSet)

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse from django.http import HttpResponse
@ -9,6 +11,7 @@ from django.shortcuts import get_object_or_404
from extras import filters from extras import filters
from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction from extras.models import ExportTemplate, Graph, ImageAttachment, TopologyMap, UserAction
from extras.reports import get_reports
from utilities.api import WritableSerializerMixin from utilities.api import WritableSerializerMixin
from . import serializers from . import serializers
@ -88,6 +91,28 @@ class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
write_serializer_class = serializers.WritableImageAttachmentSerializer write_serializer_class = serializers.WritableImageAttachmentSerializer
class ReportViewSet(ViewSet):
_ignore_model_permissions = True
exclude_from_schema = True
def list(self, request):
ret_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)
return Response(ret_list)
class RecentActivityViewSet(ReadOnlyModelViewSet): class RecentActivityViewSet(ReadOnlyModelViewSet):
""" """
List all UserActions to provide a log of recent activity. List all UserActions to provide a log of recent activity.

View File

@ -1,11 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import importlib
import inspect
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from extras.reports import Report from extras.models import ReportResult
from extras.reports import get_reports
class Command(BaseCommand): class Command(BaseCommand):
@ -18,43 +17,34 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
# Gather all reports to be run # Gather all reports to be run
reports = [] reports = get_reports()
for module_name in options['reports']:
# Split the report name off if one has been provided.
report_name = None
if '.' in module_name:
module_name, report_name = module_name.split('.', 1)
# Import the report module
try:
report_module = importlib.import_module('reports.report_{}'.format(module_name))
except ImportError:
self.stdout.write(
"Report module '{}' not found. Ensure that the report has been saved as 'report_{}.py' in the "
"reports directory.".format(module_name, module_name)
)
return
# If the name of a particular report has been given, run that. Otherwise, run all reports in the module.
if report_name is not None:
report_cls = getattr(report_module, report_name)
reports = [(report_name, report_cls)]
else:
for name, report_cls in inspect.getmembers(report_module, inspect.isclass):
if report_cls in Report.__subclasses__():
reports.append((name, report_cls))
# Run reports # Run reports
for name, report_cls in reports: for module_name, report in reports:
self.stdout.write("[{:%H:%M:%S}] Running {}...".format(timezone.now(), name)) for report_name, report_cls in report:
report = report_cls() report_name_full = '{}.{}'.format(module_name, report_name)
results = report.run() if module_name in options['reports'] or report_name_full in options['reports']:
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
self.stdout.write("[{:%H:%M:%S}] {}: {}".format(timezone.now(), name, status))
for test_name, attrs in results.items():
self.stdout.write(" {}: {} success, {} info, {} warning, {} failed".format(
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failed']
))
self.stdout.write("[{:%H:%M:%S}] Finished".format(timezone.now())) # Run the report
self.stdout.write(
"[{:%H:%M:%S}] Running {}.{}...".format(timezone.now(), module_name, report_name)
)
report = report_cls()
results = report.run()
# Report on success/failure
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
for test_name, attrs in results.items():
self.stdout.write(
"\t{}: {} success, {} info, {} warning, {} failed".format(
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failed']
)
)
self.stdout.write(
"[{:%H:%M:%S}] {}.{}: {}".format(timezone.now(), module_name, report_name, status)
)
# Wrap things up
self.stdout.write(
"[{:%H:%M:%S}] Finished".format(timezone.now())
)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-21 20:31
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('extras', '0007_unicode_literals'),
]
operations = [
migrations.CreateModel(
name='ReportResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_created=True)),
('report', models.CharField(max_length=255, unique=True)),
('data', django.contrib.postgres.fields.jsonb.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['report'],
'permissions': (('run_report', 'Run a report and save the results'),),
},
),
]

View File

@ -6,6 +6,7 @@ import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@ -388,6 +389,26 @@ class ImageAttachment(models.Model):
return None return None
#
# Report results
#
class ReportResult(models.Model):
"""
This model stores the results from running a user-defined report.
"""
report = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(auto_created=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', blank=True, null=True)
data = JSONField()
class Meta:
ordering = ['report']
permissions = (
('run_report', 'Run a report and save the results'),
)
# #
# User actions # User actions
# #

View File

@ -1,8 +1,41 @@
from collections import OrderedDict from collections import OrderedDict
import importlib
import inspect
import pkgutil
from django.utils import timezone from django.utils import timezone
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
import reports as user_reports
def is_report(obj):
"""
Returns True if the given object is a Report.
"""
if obj in Report.__subclasses__():
return True
return False
def get_reports():
"""
Compile a list of all reports available across all modules in the reports path.
"""
module_list = []
# Iterate through all modules within the reports path
for importer, module_name, is_pkg in pkgutil.walk_packages(user_reports.__path__):
module = importlib.import_module('reports.{}'.format(module_name))
report_list = []
# Iterate through all Report classes within the module
for report_name, report_cls in inspect.getmembers(module, is_report):
report_list.append((report_name, report_cls))
module_list.append((module_name, report_list))
return module_list
class Report(object): class Report(object):
@ -29,6 +62,7 @@ class Report(object):
} }
} }
""" """
description = None
def __init__(self): def __init__(self):

View File

@ -12,4 +12,7 @@ urlpatterns = [
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
# Reports
url(r'^reports/$', views.ReportListView.as_view(), name='report_list'),
] ]

View File

@ -1,13 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import View
from . import reports
from utilities.views import ObjectDeleteView, ObjectEditView from utilities.views import ObjectDeleteView, ObjectEditView
from .forms import ImageAttachmentForm from .forms import ImageAttachmentForm
from .models import ImageAttachment from .models import ImageAttachment, ReportResult
from .reports import get_reports
#
# Image attachments
#
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView): class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_imageattachment' permission_required = 'extras.change_imageattachment'
model = ImageAttachment model = ImageAttachment
@ -30,3 +38,23 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
def get_return_url(self, request, imageattachment): def get_return_url(self, request, imageattachment):
return imageattachment.parent.get_absolute_url() return imageattachment.parent.get_absolute_url()
#
# Reports
#
class ReportListView(View):
"""
Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
"""
def get(self, request):
reports = get_reports()
results = {r.name: r for r in ReportResult.objects.all()}
return render(request, 'extras/report_list.html', {
'reports': reports,
'results': results,
})

View File

@ -0,0 +1,18 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<h1>Reports</h1>
<div class="row">
<div class="col-md-9">
{% for module, report_list in reports %}
<h2>{{ module|bettertitle }}</h2>
<ul>
{% for name, cls in report_list %}
<li>{{ name }}</li>
{% endfor %}
</ul>
{% endfor %}
</div>
</div>
{% endblock %}