reports.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from collections import OrderedDict
  2. import importlib
  3. import inspect
  4. import pkgutil
  5. from django.utils import timezone
  6. from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_LEVEL_CODES, LOG_SUCCESS, LOG_WARNING
  7. from .models import ReportResult
  8. import reports as custom_reports
  9. def is_report(obj):
  10. """
  11. Returns True if the given object is a Report.
  12. """
  13. if obj in Report.__subclasses__():
  14. return True
  15. return False
  16. def get_report(module_name, report_name):
  17. """
  18. Return a specific report from within a module.
  19. """
  20. module = importlib.import_module('reports.{}'.format(module_name))
  21. report = getattr(module, report_name, None)
  22. if report is None:
  23. return None
  24. return report()
  25. def get_reports():
  26. """
  27. Compile a list of all reports available across all modules in the reports path. Returns a list of tuples:
  28. [
  29. (module_name, (report, report, report, ...)),
  30. (module_name, (report, report, report, ...)),
  31. ...
  32. ]
  33. """
  34. module_list = []
  35. # Iterate through all modules within the reports path. These are the user-defined files in which reports are
  36. # defined.
  37. for importer, module_name, is_pkg in pkgutil.walk_packages(custom_reports.__path__):
  38. module = importlib.import_module('reports.{}'.format(module_name))
  39. report_list = [cls() for _, cls in inspect.getmembers(module, is_report)]
  40. module_list.append((module_name, report_list))
  41. return module_list
  42. class Report(object):
  43. """
  44. NetBox users can extend this object to write custom reports to be used for validating data within NetBox. Each
  45. report must have one or more test methods named `test_*`.
  46. The `_results` attribute of a completed report will take the following form:
  47. {
  48. 'test_bar': {
  49. 'failures': 42,
  50. 'log': [
  51. (<datetime>, <level>, <object>, <message>),
  52. ...
  53. ]
  54. },
  55. 'test_foo': {
  56. 'failures': 0,
  57. 'log': [
  58. (<datetime>, <level>, <object>, <message>),
  59. ...
  60. ]
  61. }
  62. }
  63. """
  64. description = None
  65. def __init__(self):
  66. self._results = OrderedDict()
  67. self.active_test = None
  68. self.failed = False
  69. # Compile test methods and initialize results skeleton
  70. test_methods = []
  71. for method in dir(self):
  72. if method.startswith('test_') and callable(getattr(self, method)):
  73. test_methods.append(method)
  74. self._results[method] = OrderedDict([
  75. ('success', 0),
  76. ('info', 0),
  77. ('warning', 0),
  78. ('failure', 0),
  79. ('log', []),
  80. ])
  81. if not test_methods:
  82. raise Exception("A report must contain at least one test method.")
  83. self.test_methods = test_methods
  84. @property
  85. def module(self):
  86. return self.__module__.rsplit('.', 1)[1]
  87. @property
  88. def name(self):
  89. return self.__class__.__name__
  90. @property
  91. def full_name(self):
  92. return '.'.join([self.module, self.name])
  93. def _log(self, obj, message, level=LOG_DEFAULT):
  94. """
  95. Log a message from a test method. Do not call this method directly; use one of the log_* wrappers below.
  96. """
  97. if level not in LOG_LEVEL_CODES:
  98. raise Exception("Unknown logging level: {}".format(level))
  99. self._results[self.active_test]['log'].append((
  100. timezone.now().isoformat(),
  101. LOG_LEVEL_CODES.get(level),
  102. str(obj) if obj else None,
  103. obj.get_absolute_url() if getattr(obj, 'get_absolute_url', None) else None,
  104. message,
  105. ))
  106. def log(self, message):
  107. """
  108. Log a message which is not associated with a particular object.
  109. """
  110. self._log(None, message, level=LOG_DEFAULT)
  111. def log_success(self, obj, message=None):
  112. """
  113. Record a successful test against an object. Logging a message is optional.
  114. """
  115. if message:
  116. self._log(obj, message, level=LOG_SUCCESS)
  117. self._results[self.active_test]['success'] += 1
  118. def log_info(self, obj, message):
  119. """
  120. Log an informational message.
  121. """
  122. self._log(obj, message, level=LOG_INFO)
  123. self._results[self.active_test]['info'] += 1
  124. def log_warning(self, obj, message):
  125. """
  126. Log a warning.
  127. """
  128. self._log(obj, message, level=LOG_WARNING)
  129. self._results[self.active_test]['warning'] += 1
  130. def log_failure(self, obj, message):
  131. """
  132. Log a failure. Calling this method will automatically mark the report as failed.
  133. """
  134. self._log(obj, message, level=LOG_FAILURE)
  135. self._results[self.active_test]['failure'] += 1
  136. self.failed = True
  137. def run(self):
  138. """
  139. Run the report and return its results. Each test method will be executed in order.
  140. """
  141. for method_name in self.test_methods:
  142. self.active_test = method_name
  143. test_method = getattr(self, method_name)
  144. test_method()
  145. # Delete any previous ReportResult and create a new one to record the result.
  146. ReportResult.objects.filter(report=self.full_name).delete()
  147. result = ReportResult(report=self.full_name, failed=self.failed, data=self._results)
  148. result.save()
  149. self.result = result