Skip to content

Commit 8f1607e

Browse files
committed
Initial work on reports
1 parent e93129f commit 8f1607e

File tree

5 files changed

+163
-0
lines changed

5 files changed

+163
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
*.pyc
22
/netbox/netbox/configuration.py
33
/netbox/netbox/ldap_config.py
4+
/netbox/reports/*
5+
!/netbox/reports/__init__.py
46
/netbox/static
57
.idea
68
/*.sh

netbox/extras/constants.py

+14
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,17 @@
6262
(ACTION_DELETE, 'deleted'),
6363
(ACTION_BULK_DELETE, 'bulk deleted'),
6464
)
65+
66+
# Report logging levels
67+
LOG_DEFAULT = 0
68+
LOG_SUCCESS = 10
69+
LOG_INFO = 20
70+
LOG_WARNING = 30
71+
LOG_FAILURE = 40
72+
LOG_LEVEL_CODES = {
73+
LOG_DEFAULT: 'default',
74+
LOG_SUCCESS: 'success',
75+
LOG_INFO: 'info',
76+
LOG_WARNING: 'warning',
77+
LOG_FAILURE: 'failure',
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import unicode_literals
2+
import importlib
3+
import inspect
4+
5+
from django.core.management.base import BaseCommand, CommandError
6+
from django.utils import timezone
7+
8+
from extras.reports import Report
9+
10+
11+
class Command(BaseCommand):
12+
help = "Run a report to validate data in NetBox"
13+
14+
def add_arguments(self, parser):
15+
parser.add_argument('reports', nargs='+', help="Report(s) to run")
16+
# parser.add_argument('--verbose', action='store_true', default=False, help="Print all logs")
17+
18+
def handle(self, *args, **options):
19+
20+
# Gather all reports to be run
21+
reports = []
22+
for module_name in options['reports']:
23+
try:
24+
report_module = importlib.import_module('reports.report_{}'.format(module_name))
25+
except ImportError:
26+
self.stdout.write(
27+
"Report '{}' not found. Ensure that the report has been saved as 'report_{}.py' in the reports "
28+
"directory.".format(module_name, module_name)
29+
)
30+
return
31+
for name, cls in inspect.getmembers(report_module, inspect.isclass):
32+
if cls in Report.__subclasses__():
33+
reports.append((name, cls))
34+
35+
# Run reports
36+
for name, report in reports:
37+
self.stdout.write("[{:%H:%M:%S}] Running report {}...".format(timezone.now(), name))
38+
report = report()
39+
report.run()
40+
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
41+
self.stdout.write("[{:%H:%M:%S}] {}: {}".format(timezone.now(), name, status))
42+
for test_name, attrs in report.results.items():
43+
self.stdout.write(" {}: {} success, {} info, {} warning, {} failed".format(
44+
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failed']
45+
))
46+
47+
self.stdout.write("[{:%H:%M:%S}] Finished".format(timezone.now()))

netbox/extras/reports.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from collections import OrderedDict
2+
3+
from django.utils import timezone
4+
5+
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
6+
7+
8+
class Report(object):
9+
"""
10+
NetBox users can extend this object to write custom reports to be used for validating data within NetBox. Each
11+
report must have one or more test methods named `test_*`.
12+
13+
The `results` attribute of a completed report will take the following form:
14+
15+
{
16+
'test_bar': {
17+
'failures': 42,
18+
'log': [
19+
(<datetime>, <level>, <object>, <message>),
20+
...
21+
]
22+
},
23+
'test_foo': {
24+
'failures': 0,
25+
'log': [
26+
(<datetime>, <level>, <object>, <message>),
27+
...
28+
]
29+
}
30+
}
31+
"""
32+
results = OrderedDict()
33+
active_test = None
34+
failed = False
35+
36+
def __init__(self):
37+
38+
# Compile test methods and initialize results skeleton
39+
test_methods = []
40+
for method in dir(self):
41+
if method.startswith('test_') and callable(getattr(self, method)):
42+
test_methods.append(method)
43+
self.results[method] = OrderedDict([
44+
('success', 0),
45+
('info', 0),
46+
('warning', 0),
47+
('failed', 0),
48+
('log', []),
49+
])
50+
if not test_methods:
51+
raise Exception("A report must contain at least one test method.")
52+
self.test_methods = test_methods
53+
54+
def _log(self, obj, message, level=LOG_DEFAULT):
55+
"""
56+
Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below.
57+
"""
58+
if level not in LOG_LEVEL_CODES:
59+
raise Exception("Unknown logging level: {}".format(level))
60+
logline = [timezone.now(), level, obj, message]
61+
self.results[self.active_test]['log'].append(logline)
62+
63+
def log_success(self, obj, message=None):
64+
"""
65+
Record a successful test against an object. Logging a message is optional.
66+
"""
67+
if message:
68+
self._log(obj, message, level=LOG_SUCCESS)
69+
self.results[self.active_test]['success'] += 1
70+
71+
def log_info(self, obj, message):
72+
"""
73+
Log an informational message.
74+
"""
75+
self._log(obj, message, level=LOG_INFO)
76+
self.results[self.active_test]['info'] += 1
77+
78+
def log_warning(self, obj, message):
79+
"""
80+
Log a warning.
81+
"""
82+
self._log(obj, message, level=LOG_WARNING)
83+
self.results[self.active_test]['warning'] += 1
84+
85+
def log_failure(self, obj, message):
86+
"""
87+
Log a failure. Calling this method will automatically mark the report as failed.
88+
"""
89+
self._log(obj, message, level=LOG_FAILURE)
90+
self.results[self.active_test]['failed'] += 1
91+
self.failed = True
92+
93+
def run(self):
94+
"""
95+
Run the report. Each test method will be executed in order.
96+
"""
97+
for method_name in self.test_methods:
98+
self.active_test = method_name
99+
test_method = getattr(self, method_name)
100+
test_method()

netbox/reports/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)