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
This commit is contained in:
Dag Wieers 2017-09-21 04:54:26 +02:00 committed by GitHub
parent 0e22afef52
commit 57b85198fe

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2012, Dag Wieers <dag@wieers.com> # Copyright: (c) 2012, Dag Wieers <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
callback: mail callback: mail
type: notification type: notification
short_description: Sends failure events via email short_description: Sends failure events via email
description: description:
- This callback will report failures via email - This callback will report failures via email
version_added: "2.3" version_added: '2.0'
requirements: requirements:
- whitelisting in configuration - whitelisting in configuration
- logstash (python library) options:
options: mta:
mta: description: Mail Transfer Agent, server that accepts SMTP
description: Mail Transfer Agent, server that accepts SMTP env:
env: - name: SMTPHOST
- name: SMTPHOST default: localhost
default: localhost note:
note: - "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
- "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
''' '''
import json import json
import os import os
import re
import smtplib import smtplib
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
@ -81,52 +82,85 @@ class CallbackModule(CallbackBase):
def subject_msg(self, multiline, failtype, linenr): def subject_msg(self, multiline, failtype, linenr):
return '%s: %s' % (failtype, multiline.strip('\r\n').splitlines()[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): def body_blob(self, multiline, texttype):
''' Turn some text output in a well-indented block for sending in a mail body ''' ''' 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(): for line in multiline.strip('\r\n').splitlines():
blob += '\t%s\n' % line blob += '%s\n' % line
return blob + '\n' return intro + self.indent(blob) + '\n'
def mail_result(self, result, failtype): def mail_result(self, result, failtype):
host = result._host.get_name() host = result._host.get_name()
sender = '"Ansible: %s" <root>' % host sender = '"Ansible: %s" <root>' % host
subject = '%s: %s' % (failtype, result._task.name or result._task.action)
body = '' # Add subject
body += 'Playbook: %s\n' % os.path.basename(self.playbook._file_name) 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: if result._task.name:
body += 'Task: %s\n' % result._task.name body += 'Task: %s\n' % result._task.name
body += 'Module: %s\n' % result._task.action body += 'Module: %s\n' % result._task.action
body += 'Host: %s\n' % host body += 'Host: %s\n' % host
body += '\n' body += '\n'
# Add task information (as much as possible)
body += 'The following task failed:\n\n' body += 'The following task failed:\n\n'
if 'invocation' in result._result: 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: 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: else:
body += '\t%s\n\n' % result._task.action body += self.indent('%s\n' % result._task.action)
body += '\n'
if result._result.get('stdout'): # Add item / message
subject = self.subject_msg(result._result['stdout'], failtype, -1) if self.itembody:
body += self.body_blob(result._result['stdout'], 'standard output') body += self.itembody
if result._result.get('stderr'): elif result._result.get('failed_when_result') == True:
subject = self.subject_msg(result._result['stderr'], failtype, -1) body += "due to the following condition:\n\n" + self.indent('failed_when:\n- ' + '\n- '.join(result._task.failed_when)) + '\n\n'
body += self.body_blob(result._result['stderr'], 'error output') elif result._result.get('msg'):
if result._result.get('msg'):
subject = self.subject_msg(result._result['msg'], failtype, 0)
body += self.body_blob(result._result['msg'], 'message') body += self.body_blob(result._result['msg'], 'message')
body += 'A complete dump of the error:\n\n' # Add stdout / stderr / exception / warnings / deprecations
body += '\t%s: %s' % (failtype, json.dumps(result._result)) 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) self.mail(sender=sender, subject=subject, body=body)
def v2_playbook_on_start(self, playbook): def v2_playbook_on_start(self, playbook):
self.playbook = playbook self.playbook = playbook
self.itembody = ''
def v2_runner_on_failed(self, result, ignore_errors=False): def v2_runner_on_failed(self, result, ignore_errors=False):
if ignore_errors: if ignore_errors:
@ -139,3 +173,9 @@ class CallbackModule(CallbackBase):
def v2_runner_on_async_failed(self, result): def v2_runner_on_async_failed(self, result):
self.mail_result(result, 'Async failure') 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)