mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on reports
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
/netbox/netbox/ldap_config.py
|
/netbox/netbox/ldap_config.py
|
||||||
|
/netbox/reports/*
|
||||||
|
!/netbox/reports/__init__.py
|
||||||
/netbox/static
|
/netbox/static
|
||||||
.idea
|
.idea
|
||||||
/*.sh
|
/*.sh
|
||||||
|
@ -62,3 +62,17 @@ ACTION_CHOICES = (
|
|||||||
(ACTION_DELETE, 'deleted'),
|
(ACTION_DELETE, 'deleted'),
|
||||||
(ACTION_BULK_DELETE, 'bulk deleted'),
|
(ACTION_BULK_DELETE, 'bulk deleted'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Report logging levels
|
||||||
|
LOG_DEFAULT = 0
|
||||||
|
LOG_SUCCESS = 10
|
||||||
|
LOG_INFO = 20
|
||||||
|
LOG_WARNING = 30
|
||||||
|
LOG_FAILURE = 40
|
||||||
|
LOG_LEVEL_CODES = {
|
||||||
|
LOG_DEFAULT: 'default',
|
||||||
|
LOG_SUCCESS: 'success',
|
||||||
|
LOG_INFO: 'info',
|
||||||
|
LOG_WARNING: 'warning',
|
||||||
|
LOG_FAILURE: 'failure',
|
||||||
|
}
|
||||||
|
47
netbox/extras/management/commands/runreport.py
Normal file
47
netbox/extras/management/commands/runreport.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from extras.reports import Report
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Run a report to validate data in NetBox"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('reports', nargs='+', help="Report(s) to run")
|
||||||
|
# parser.add_argument('--verbose', action='store_true', default=False, help="Print all logs")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
# Gather all reports to be run
|
||||||
|
reports = []
|
||||||
|
for module_name in options['reports']:
|
||||||
|
try:
|
||||||
|
report_module = importlib.import_module('reports.report_{}'.format(module_name))
|
||||||
|
except ImportError:
|
||||||
|
self.stdout.write(
|
||||||
|
"Report '{}' not found. Ensure that the report has been saved as 'report_{}.py' in the reports "
|
||||||
|
"directory.".format(module_name, module_name)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
for name, cls in inspect.getmembers(report_module, inspect.isclass):
|
||||||
|
if cls in Report.__subclasses__():
|
||||||
|
reports.append((name, cls))
|
||||||
|
|
||||||
|
# Run reports
|
||||||
|
for name, report in reports:
|
||||||
|
self.stdout.write("[{:%H:%M:%S}] Running report {}...".format(timezone.now(), name))
|
||||||
|
report = report()
|
||||||
|
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 report.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()))
|
100
netbox/extras/reports.py
Normal file
100
netbox/extras/reports.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
|
||||||
|
|
||||||
|
|
||||||
|
class Report(object):
|
||||||
|
"""
|
||||||
|
NetBox users can extend this object to write custom reports to be used for validating data within NetBox. Each
|
||||||
|
report must have one or more test methods named `test_*`.
|
||||||
|
|
||||||
|
The `results` attribute of a completed report will take the following form:
|
||||||
|
|
||||||
|
{
|
||||||
|
'test_bar': {
|
||||||
|
'failures': 42,
|
||||||
|
'log': [
|
||||||
|
(<datetime>, <level>, <object>, <message>),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'test_foo': {
|
||||||
|
'failures': 0,
|
||||||
|
'log': [
|
||||||
|
(<datetime>, <level>, <object>, <message>),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
results = OrderedDict()
|
||||||
|
active_test = None
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
# Compile test methods and initialize results skeleton
|
||||||
|
test_methods = []
|
||||||
|
for method in dir(self):
|
||||||
|
if method.startswith('test_') and callable(getattr(self, method)):
|
||||||
|
test_methods.append(method)
|
||||||
|
self.results[method] = OrderedDict([
|
||||||
|
('success', 0),
|
||||||
|
('info', 0),
|
||||||
|
('warning', 0),
|
||||||
|
('failed', 0),
|
||||||
|
('log', []),
|
||||||
|
])
|
||||||
|
if not test_methods:
|
||||||
|
raise Exception("A report must contain at least one test method.")
|
||||||
|
self.test_methods = test_methods
|
||||||
|
|
||||||
|
def _log(self, obj, message, level=LOG_DEFAULT):
|
||||||
|
"""
|
||||||
|
Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below.
|
||||||
|
"""
|
||||||
|
if level not in LOG_LEVEL_CODES:
|
||||||
|
raise Exception("Unknown logging level: {}".format(level))
|
||||||
|
logline = [timezone.now(), level, obj, message]
|
||||||
|
self.results[self.active_test]['log'].append(logline)
|
||||||
|
|
||||||
|
def log_success(self, obj, message=None):
|
||||||
|
"""
|
||||||
|
Record a successful test against an object. Logging a message is optional.
|
||||||
|
"""
|
||||||
|
if message:
|
||||||
|
self._log(obj, message, level=LOG_SUCCESS)
|
||||||
|
self.results[self.active_test]['success'] += 1
|
||||||
|
|
||||||
|
def log_info(self, obj, message):
|
||||||
|
"""
|
||||||
|
Log an informational message.
|
||||||
|
"""
|
||||||
|
self._log(obj, message, level=LOG_INFO)
|
||||||
|
self.results[self.active_test]['info'] += 1
|
||||||
|
|
||||||
|
def log_warning(self, obj, message):
|
||||||
|
"""
|
||||||
|
Log a warning.
|
||||||
|
"""
|
||||||
|
self._log(obj, message, level=LOG_WARNING)
|
||||||
|
self.results[self.active_test]['warning'] += 1
|
||||||
|
|
||||||
|
def log_failure(self, obj, message):
|
||||||
|
"""
|
||||||
|
Log a failure. Calling this method will automatically mark the report as failed.
|
||||||
|
"""
|
||||||
|
self._log(obj, message, level=LOG_FAILURE)
|
||||||
|
self.results[self.active_test]['failed'] += 1
|
||||||
|
self.failed = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Run the report. Each test method will be executed in order.
|
||||||
|
"""
|
||||||
|
for method_name in self.test_methods:
|
||||||
|
self.active_test = method_name
|
||||||
|
test_method = getattr(self, method_name)
|
||||||
|
test_method()
|
0
netbox/reports/__init__.py
Normal file
0
netbox/reports/__init__.py
Normal file
Reference in New Issue
Block a user