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
router.register(r'image-attachments', views.ImageAttachmentViewSet)
# Reports
router.register(r'reports', views.ReportViewSet, base_name='report')
# Recent activity
router.register(r'recent-activity', views.RecentActivityViewSet)

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals
from collections import OrderedDict
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.http import HttpResponse
@ -9,6 +11,7 @@ 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 utilities.api import WritableSerializerMixin
from . import serializers
@ -88,6 +91,28 @@ class ImageAttachmentViewSet(WritableSerializerMixin, ModelViewSet):
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):
"""
List all UserActions to provide a log of recent activity.

View File

@ -1,11 +1,10 @@
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 extras.reports import Report
from extras.models import ReportResult
from extras.reports import get_reports
class Command(BaseCommand):
@ -18,43 +17,34 @@ class Command(BaseCommand):
def handle(self, *args, **options):
# Gather all reports to be run
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))
reports = get_reports()
# Run reports
for name, report_cls in reports:
self.stdout.write("[{:%H:%M:%S}] Running {}...".format(timezone.now(), name))
for module_name, report in reports:
for report_name, report_cls in report:
report_name_full = '{}.{}'.format(module_name, report_name)
if module_name in options['reports'] or report_name_full in options['reports']:
# Run the report
self.stdout.write(
"[{:%H:%M:%S}] Running {}.{}...".format(timezone.now(), module_name, report_name)
)
report = report_cls()
results = report.run()
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()))
# 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.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import ValidationError
from django.db import models
from django.db.models import Q
@ -388,6 +389,26 @@ class ImageAttachment(models.Model):
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
#

View File

@ -1,8 +1,41 @@
from collections import OrderedDict
import importlib
import inspect
import pkgutil
from django.utils import timezone
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):
@ -29,6 +62,7 @@ class Report(object):
}
}
"""
description = None
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+)/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 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 .forms import ImageAttachmentForm
from .models import ImageAttachment
from .models import ImageAttachment, ReportResult
from .reports import get_reports
#
# Image attachments
#
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_imageattachment'
model = ImageAttachment
@ -30,3 +38,23 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
def get_return_url(self, request, imageattachment):
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 %}