From 57b85198fe635281065771afdbf3ede268a40624 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 21 Sep 2017 04:54:26 +0200 Subject: [PATCH] Add support for loop-tasks and improved output This PR includes: - Support for loop-tasks with proper subject/error content - Improved output (and proper indentation) - Complex data structures are now pretty printed - Better selection of mail subject --- lib/ansible/plugins/callback/mail.py | 114 ++++++++++++++++++--------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/lib/ansible/plugins/callback/mail.py b/lib/ansible/plugins/callback/mail.py index 4101120c77d..a0cab5742dc 100644 --- a/lib/ansible/plugins/callback/mail.py +++ b/lib/ansible/plugins/callback/mail.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + # Copyright: (c) 2012, Dag Wieers # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -6,27 +7,27 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' - callback: mail - type: notification - short_description: Sends failure events via email - description: - - This callback will report failures via email - version_added: "2.3" - requirements: - - whitelisting in configuration - - logstash (python library) - options: - mta: - description: Mail Transfer Agent, server that accepts SMTP - env: - - name: SMTPHOST - default: localhost - note: - - "TODO: expand configuration options now that plugins can leverage Ansible's configuration" +callback: mail +type: notification +short_description: Sends failure events via email +description: +- This callback will report failures via email +version_added: '2.0' +requirements: +- whitelisting in configuration +options: + mta: + description: Mail Transfer Agent, server that accepts SMTP + env: + - name: SMTPHOST + default: localhost +note: +- "TODO: expand configuration options now that plugins can leverage Ansible's configuration" ''' import json import os +import re import smtplib from ansible.module_utils.six import string_types @@ -81,52 +82,85 @@ class CallbackModule(CallbackBase): def subject_msg(self, multiline, failtype, linenr): return '%s: %s' % (failtype, multiline.strip('\r\n').splitlines()[linenr]) + def indent(self, multiline, indent=8): + return re.sub('^', ' '*indent, multiline, flags=re.MULTILINE) + def body_blob(self, multiline, texttype): ''' Turn some text output in a well-indented block for sending in a mail body ''' - blob = 'with the following %s:\n\n' % texttype + intro = 'with the following %s:\n\n' % texttype + blob = '' for line in multiline.strip('\r\n').splitlines(): - blob += '\t%s\n' % line - return blob + '\n' + blob += '%s\n' % line + return intro + self.indent(blob) + '\n' def mail_result(self, result, failtype): host = result._host.get_name() - sender = '"Ansible: %s" ' % host - subject = '%s: %s' % (failtype, result._task.name or result._task.action) - body = '' - body += 'Playbook: %s\n' % os.path.basename(self.playbook._file_name) + # Add subject + if self.itembody: + subject = self.itemsubject + elif result._result.get('failed_when_result') == True: + subject = "Failed due to 'failed_when' condition" + elif result._result.get('exception'): + subject = self.subject_msg(result._result['exception'], failtype, -1) + elif result._result.get('msg'): + subject = self.subject_msg(result._result['msg'], failtype, 0) + elif result._result.get('stderr'): + subject = self.subject_msg(result._result['stderr'], failtype, -1) + elif result._result.get('stdout'): + subject = self.subject_msg(result._result['stdout'], failtype, -1) + else: + subject = '%s: %s' % (failtype, result._task.name or result._task.action) + + # Make playbook name visible (e.g. in Outlook/Gmail condensed view) + body = 'Playbook: %s\n' % os.path.basename(self.playbook._file_name) if result._task.name: body += 'Task: %s\n' % result._task.name body += 'Module: %s\n' % result._task.action body += 'Host: %s\n' % host body += '\n' + # Add task information (as much as possible) body += 'The following task failed:\n\n' if 'invocation' in result._result: - body += '\t%s: %s\n\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'])) + body += self.indent('%s: %s\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'], indent=4))) elif result._task.name: - body += '\t%s (%s)\n\n' % (result._task.name, result._task.action) + body += self.indent('%s (%s)\n' % (result._task.name, result._task.action)) else: - body += '\t%s\n\n' % result._task.action + body += self.indent('%s\n' % result._task.action) + body += '\n' - if result._result.get('stdout'): - subject = self.subject_msg(result._result['stdout'], failtype, -1) - body += self.body_blob(result._result['stdout'], 'standard output') - if result._result.get('stderr'): - subject = self.subject_msg(result._result['stderr'], failtype, -1) - body += self.body_blob(result._result['stderr'], 'error output') - if result._result.get('msg'): - subject = self.subject_msg(result._result['msg'], failtype, 0) + # Add item / message + if self.itembody: + body += self.itembody + elif result._result.get('failed_when_result') == True: + body += "due to the following condition:\n\n" + self.indent('failed_when:\n- ' + '\n- '.join(result._task.failed_when)) + '\n\n' + elif result._result.get('msg'): body += self.body_blob(result._result['msg'], 'message') - body += 'A complete dump of the error:\n\n' - body += '\t%s: %s' % (failtype, json.dumps(result._result)) + # Add stdout / stderr / exception / warnings / deprecations + if result._result.get('exception'): + body += self.body_blob(result._result['exception'], 'exception') + if result._result.get('stdout'): + body += self.body_blob(result._result['stdout'], 'standard output') + if result._result.get('stderr'): + body += self.body_blob(result._result['stderr'], 'error output') + if result._result.get('warnings'): + for i in range(len(result._result.get('warnings'))): + body += self.body_blob(result._result['warnings'][i], 'exception %d' % i+1) + if result._result.get('deprecations'): + for i in range(len(result._result.get('deprecations'))): + body += self.body_blob(result._result['deprecations'][i], 'exception %d' % i+1) + + body += 'and a complete dump of the error:\n\n' + body += self.indent('%s: %s' % (failtype, json.dumps(result._result, indent=4))) self.mail(sender=sender, subject=subject, body=body) def v2_playbook_on_start(self, playbook): self.playbook = playbook + self.itembody = '' def v2_runner_on_failed(self, result, ignore_errors=False): if ignore_errors: @@ -139,3 +173,9 @@ class CallbackModule(CallbackBase): def v2_runner_on_async_failed(self, result): self.mail_result(result, 'Async failure') + + def v2_runner_item_on_failed(self, result): + # Pass item information to task failure + self.itemsubject = result._result['msg'] + self.itembody += self.body_blob(json.dumps(result._result, indent=4), "failed item dump '%(item)s'" % result._result) +# self.itembody += self.body_blob(json.dumps(dir(result), indent=4), "failed full dump '%(item)s'" % result._result)