mirror of https://github.com/ansible/ansible.git
parent
f86df7c88b
commit
7edfeb3665
@ -0,0 +1,240 @@
|
||||
# (c) 2016 Matt Clay <matt@mystile.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from collections import OrderedDict
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
try:
|
||||
from junit_xml import TestSuite, TestCase
|
||||
HAS_JUNIT_XML = True
|
||||
except ImportError:
|
||||
HAS_JUNIT_XML = False
|
||||
|
||||
|
||||
class CallbackModule(CallbackBase):
|
||||
"""
|
||||
This callback writes playbook output to a JUnit formatted XML file.
|
||||
|
||||
Tasks show up in the report as follows:
|
||||
'ok': pass
|
||||
'failed' with 'EXPECTED FAILURE' in the task name: pass
|
||||
'failed' due to an exception: error
|
||||
'failed' for other reasons: failure
|
||||
'skipped': skipped
|
||||
|
||||
This plugin makes use of the following environment variables:
|
||||
JUNIT_OUTPUT_DIR (optional): Directory to write XML files to.
|
||||
Default: ~/.ansible.log
|
||||
|
||||
Requires:
|
||||
junit_xml
|
||||
|
||||
"""
|
||||
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'aggregate'
|
||||
CALLBACK_NAME = 'junit'
|
||||
CALLBACK_NEEDS_WHITELIST = True
|
||||
|
||||
def __init__(self):
|
||||
super(CallbackModule, self).__init__()
|
||||
|
||||
self._output_dir = os.getenv('JUNIT_OUTPUT_DIR', os.path.expanduser('~/.ansible.log'))
|
||||
self._playbook_path = None
|
||||
self._playbook_name = None
|
||||
self._play_name = None
|
||||
self._task_data = OrderedDict()
|
||||
|
||||
self.disabled = False
|
||||
|
||||
if not HAS_JUNIT_XML:
|
||||
self.disabled = True
|
||||
self._display.warning('The `junit_xml` python module is not installed. '
|
||||
'Disabling the `junit` callback plugin.')
|
||||
|
||||
if not os.path.exists(self._output_dir):
|
||||
os.mkdir(self._output_dir)
|
||||
|
||||
def _start_task(self, task):
|
||||
""" record the start of a task for one or more hosts """
|
||||
|
||||
uuid = task._uuid
|
||||
|
||||
if uuid in self._task_data:
|
||||
return
|
||||
|
||||
play = self._play_name
|
||||
name = task.get_name().strip()
|
||||
path = task.get_path()
|
||||
|
||||
if not task.no_log:
|
||||
args = ', '.join(('%s=%s' % a for a in task.args.items()))
|
||||
if args:
|
||||
name += ' ' + args
|
||||
|
||||
self._task_data[uuid] = TaskData(uuid, name, path, play)
|
||||
|
||||
def _finish_task(self, status, result):
|
||||
""" record the results of a task for a single host """
|
||||
|
||||
task_uuid = result._task._uuid
|
||||
|
||||
if hasattr(result, '_host'):
|
||||
host_uuid = result._host._uuid
|
||||
host_name = result._host.name
|
||||
else:
|
||||
host_uuid = 'include'
|
||||
host_name = 'include'
|
||||
|
||||
task_data = self._task_data[task_uuid]
|
||||
|
||||
if status == 'failed' and 'EXPECTED FAILURE' in task_data.name:
|
||||
status = 'ok'
|
||||
|
||||
task_data.add_host(HostData(host_uuid, host_name, status, result))
|
||||
|
||||
def _build_test_case(self, task_data, host_data):
|
||||
""" build a TestCase from the given TaskData and HostData """
|
||||
|
||||
name = '[%s] %s: %s' % (host_data.name, task_data.play, task_data.name)
|
||||
duration = host_data.finish - task_data.start
|
||||
|
||||
if host_data.status == 'included':
|
||||
return TestCase(name, task_data.path, duration, host_data.result)
|
||||
|
||||
res = host_data.result._result
|
||||
rc = res.get('rc', 0)
|
||||
dump = self._dump_results(res, indent=0)
|
||||
|
||||
if host_data.status == 'ok':
|
||||
return TestCase(name, task_data.path, duration, dump)
|
||||
|
||||
test_case = TestCase(name, task_data.path, duration)
|
||||
|
||||
if host_data.status == 'failed':
|
||||
if 'exception' in res:
|
||||
message = res['exception'].strip().split('\n')[-1]
|
||||
output = res['exception']
|
||||
test_case.add_error_info(message, output)
|
||||
elif 'msg' in res:
|
||||
message = res['msg']
|
||||
test_case.add_failure_info(message, dump)
|
||||
else:
|
||||
test_case.add_failure_info('rc=%s' % rc, dump)
|
||||
elif host_data.status == 'skipped':
|
||||
if 'skip_reason' in res:
|
||||
message = res['skip_reason']
|
||||
else:
|
||||
message = 'skipped'
|
||||
test_case.add_skipped_info(message)
|
||||
|
||||
return test_case
|
||||
|
||||
def _generate_report(self):
|
||||
""" generate a TestSuite report from the collected TaskData and HostData """
|
||||
|
||||
test_cases = []
|
||||
|
||||
for task_uuid, task_data in self._task_data.items():
|
||||
for host_uuid, host_data in task_data.host_data.items():
|
||||
test_cases.append(self._build_test_case(task_data, host_data))
|
||||
|
||||
test_suite = TestSuite(self._playbook_name, test_cases)
|
||||
report = TestSuite.to_xml_string([test_suite])
|
||||
|
||||
output_file = os.path.join(self._output_dir, '%s-%s.xml' % (self._playbook_name, time.time()))
|
||||
|
||||
with open(output_file, 'wb') as xml:
|
||||
xml.write(to_bytes(report, errors='strict'))
|
||||
|
||||
def v2_playbook_on_start(self, playbook):
|
||||
self._playbook_path = playbook._file_name
|
||||
self._playbook_name = os.path.splitext(os.path.basename(self._playbook_path))[0]
|
||||
|
||||
def v2_playbook_on_play_start(self, play):
|
||||
self._play_name = play.get_name()
|
||||
|
||||
def v2_runner_on_no_hosts(self, task):
|
||||
self._start_task(task)
|
||||
|
||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||
self._start_task(task)
|
||||
|
||||
def v2_playbook_on_cleanup_task_start(self, task):
|
||||
self._start_task(task)
|
||||
|
||||
def v2_playbook_on_handler_task_start(self, task):
|
||||
self._start_task(task)
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
if ignore_errors:
|
||||
self._finish_task('ok', result)
|
||||
else:
|
||||
self._finish_task('failed', result)
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
self._finish_task('ok', result)
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
self._finish_task('skipped', result)
|
||||
|
||||
def v2_playbook_on_include(self, included_file):
|
||||
self._finish_task('included', included_file)
|
||||
|
||||
def v2_playbook_on_stats(self, stats):
|
||||
self._generate_report()
|
||||
|
||||
|
||||
class TaskData:
|
||||
"""
|
||||
Data about an individual task.
|
||||
"""
|
||||
|
||||
def __init__(self, uuid, name, path, play):
|
||||
self.uuid = uuid
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.play = play
|
||||
self.start = None
|
||||
self.host_data = OrderedDict()
|
||||
self.start = time.time()
|
||||
|
||||
def add_host(self, host):
|
||||
if host.uuid in self.host_data:
|
||||
raise Exception('%s: %s: %s: duplicate host callback: %s' % (self.path, self.play, self.name, host.name))
|
||||
|
||||
self.host_data[host.uuid] = host
|
||||
|
||||
|
||||
class HostData:
|
||||
"""
|
||||
Data about an individual host.
|
||||
"""
|
||||
|
||||
def __init__(self, uuid, name, status, result):
|
||||
self.uuid = uuid
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.result = result
|
||||
self.finish = time.time()
|
Loading…
Reference in New Issue