reports.py 5.8 KB

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