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:
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
)
|
||||
|
33
netbox/extras/migrations/0008_reports.py
Normal file
33
netbox/extras/migrations/0008_reports.py
Normal 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'),),
|
||||
},
|
||||
),
|
||||
]
|
@ -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
|
||||
#
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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'),
|
||||
|
||||
]
|
||||
|
@ -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,
|
||||
})
|
||||
|
18
netbox/templates/extras/report_list.html
Normal file
18
netbox/templates/extras/report_list.html
Normal 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 %}
|
Reference in New Issue
Block a user