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
|
# 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)
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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())
|
||||||
|
)
|
||||||
|
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.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
|
||||||
#
|
#
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
|
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