zz_failure_summary.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. # vim: expandtab:tabstop=4:shiftwidth=4
  2. '''
  3. Ansible callback plugin.
  4. '''
  5. from pprint import pformat
  6. from ansible.plugins.callback import CallbackBase
  7. from ansible import constants as C
  8. from ansible.utils.color import stringc
  9. class CallbackModule(CallbackBase):
  10. '''
  11. This callback plugin stores task results and summarizes failures.
  12. The file name is prefixed with `zz_` to make this plugin be loaded last by
  13. Ansible, thus making its output the last thing that users see.
  14. '''
  15. CALLBACK_VERSION = 2.0
  16. CALLBACK_TYPE = 'aggregate'
  17. CALLBACK_NAME = 'failure_summary'
  18. CALLBACK_NEEDS_WHITELIST = False
  19. def __init__(self):
  20. super(CallbackModule, self).__init__()
  21. self.__failures = []
  22. def v2_runner_on_failed(self, result, ignore_errors=False):
  23. super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)
  24. self.__failures.append(dict(result=result, ignore_errors=ignore_errors))
  25. def v2_playbook_on_stats(self, stats):
  26. super(CallbackModule, self).v2_playbook_on_stats(stats)
  27. # TODO: update condition to consider a host var or env var to
  28. # enable/disable the summary, so that we can control the output from a
  29. # play.
  30. if self.__failures:
  31. self._print_failure_summary()
  32. def _print_failure_summary(self):
  33. '''Print a summary of failed tasks (including ignored failures).'''
  34. self._display.display(u'\nFailure summary:\n')
  35. # TODO: group failures by host or by task. If grouped by host, it is
  36. # easy to see all problems of a given host. If grouped by task, it is
  37. # easy to see what hosts needs the same fix.
  38. width = len(str(len(self.__failures)))
  39. initial_indent_format = u' {{:>{width}}}. '.format(width=width)
  40. initial_indent_len = len(initial_indent_format.format(0))
  41. subsequent_indent = u' ' * initial_indent_len
  42. subsequent_extra_indent = u' ' * (initial_indent_len + 10)
  43. for i, failure in enumerate(self.__failures, 1):
  44. entries = _format_failure(failure)
  45. self._display.display(u'\n{}{}'.format(initial_indent_format.format(i), entries[0]))
  46. for entry in entries[1:]:
  47. entry = entry.replace(u'\n', u'\n' + subsequent_extra_indent)
  48. indented = u'{}{}'.format(subsequent_indent, entry)
  49. self._display.display(indented)
  50. # Reason: disable pylint protected-access because we need to access _*
  51. # attributes of a task result to implement this method.
  52. # Status: permanently disabled unless Ansible's API changes.
  53. # pylint: disable=protected-access
  54. def _format_failure(failure):
  55. '''Return a list of pretty-formatted text entries describing a failure, including
  56. relevant information about it. Expect that the list of text entries will be joined
  57. by a newline separator when output to the user.'''
  58. result = failure['result']
  59. host = result._host.get_name()
  60. play = _get_play(result._task)
  61. if play:
  62. play = play.get_name()
  63. task = result._task.get_name()
  64. msg = result._result.get('msg', u'???')
  65. fields = (
  66. (u'Host', host),
  67. (u'Play', play),
  68. (u'Task', task),
  69. (u'Message', stringc(msg, C.COLOR_ERROR)),
  70. )
  71. if 'checks' in result._result:
  72. fields += ((u'Details', _format_failed_checks(result._result['checks'])),)
  73. row_format = '{:10}{}'
  74. return [row_format.format(header + u':', body) for header, body in fields]
  75. def _format_failed_checks(checks):
  76. '''Return pretty-formatted text describing checks that failed.'''
  77. failed_check_msgs = []
  78. for check, body in checks.items():
  79. if body.get('failed', False): # only show the failed checks
  80. msg = body.get('msg', u"Failed without returning a message")
  81. failed_check_msgs.append('check "%s":\n%s' % (check, msg))
  82. if failed_check_msgs:
  83. return stringc("\n\n".join(failed_check_msgs), C.COLOR_ERROR)
  84. else: # something failed but no checks will admit to it, so dump everything
  85. return stringc(pformat(checks), C.COLOR_ERROR)
  86. # Reason: disable pylint protected-access because we need to access _*
  87. # attributes of obj to implement this function.
  88. # This is inspired by ansible.playbook.base.Base.dump_me.
  89. # Status: permanently disabled unless Ansible's API changes.
  90. # pylint: disable=protected-access
  91. def _get_play(obj):
  92. '''Given a task or block, recursively tries to find its parent play.'''
  93. if hasattr(obj, '_play'):
  94. return obj._play
  95. if getattr(obj, '_parent'):
  96. return _get_play(obj._parent)