From 0a6a9f265fbabce0a270853b46f1fbfabb222ad2 Mon Sep 17 00:00:00 2001 From: Alex Schultz Date: Wed, 6 May 2020 15:14:30 -0600 Subject: [PATCH] Add tripleo dense formatting This callback prints out a compact format that is generally in the following format: | | | delegated host> | [ | ... ] : 36 char Task UUID but may be omitted and would be 36 spaces. : One of [TASK|CHANGED|START|OK|FATAL|WAITING|SKIPPED|INCLUDED|HANDLER]. This is a 10 character right aligned string. : free form task name text or a file name in the case of INCLUDED delgated host>: Will be one of: -> After the host is may be any number of additional data fields delimited by | and will contain a key= format. Examples being: item={'cinder': {'project': 'service'}} result={"changed": true, "path": "/tmp/ansible.briga_ee", "state": "absent"} Warnings and deprecation sdo not have a task name or UUID so they generally formatted as: | WARNING | | DEPRECATED | Change-Id: Ifc18b7204aa47617d1b3277de2d8a781f0c9ea13 --- .../ansible_plugins/callback/tripleo_dense.py | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tripleo_ansible/ansible_plugins/callback/tripleo_dense.py diff --git a/tripleo_ansible/ansible_plugins/callback/tripleo_dense.py b/tripleo_ansible/ansible_plugins/callback/tripleo_dense.py new file mode 100644 index 000000000..c3c634112 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/callback/tripleo_dense.py @@ -0,0 +1,288 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import uuid + +from ansible import constants as C +from ansible.playbook.task_include import TaskInclude +from ansible.plugins.callback.default import CallbackModule as DefaultCallback + + +class CallbackModule(DefaultCallback): + + def _output(self, msg, color): + if isinstance(msg, list): + output = ' | '.join(msg) + else: + output = msg + self._display.display(output, color=color) + + def _get_host(self, result): + delegated_vars = result._result.get('_ansible_delegated_vars', None) + if (getattr(result, '_host', False) + and getattr(result._host, 'get_name', False)): + msg = '%s' % result._host.get_name() + elif (getattr(result, '_host', False) + and getattr(result._host, 'name', False)): + msg = '%s' % result._host.name + else: + msg = 'UNKNOWN' + if delegated_vars: + msg += ' -> %s' % delegated_vars['ansible_host'] + return msg + + def _get_task_name(self, item=None): + name = '' + if item and getattr(item, 'name', False): + # item is a task + name = item.name + elif item and getattr(item, 'task_name', False): + name = item.task_name + elif item and getattr(item, '_task', False): + name = item._task.name + return name + + def _get_uuid(self, item=None): + uuid = '' + + if item and getattr(item, '_uuid', False): + # item is a task + uuid = item._uuid + elif item and getattr(item, '_task', False): + # item is a result (may not have a _task tho) + if getattr(item._task, '_uuid', False): + uuid = item._task._uuid + return '{:36}'.format(uuid) + + def _get_state(self, state): + return '{:>10}'.format(state) + + # TODO(mwhahaha): can this work for fatal/skipped/etc? + def _get_item_line(self, result, item=False): + line = [ + self._get_uuid(result) + ] + host_str = self._get_host(result=result) + + if (getattr(result, '_result', False) + and result._result.get('changed', False)): + line.append(self._get_state('CHANGED')), + line.append(self._get_task_name(result)) + line.append(host_str) + color = C.COLOR_CHANGED + else: + if not self.display_ok_hosts: + return + line.append(self._get_state('OK')) + line.append(self._get_task_name(result)) + line.append(host_str) + color = C.COLOR_OK + if item: + item_result = self._get_item_label(result._result) + # don't display if None + if item_result: + line.append('item=%s' % item_result) + return (line, color) + + def _handle_warnings(self, result): + if not C.ACTION_WARNINGS: + return + if result.get('warnings', False): + line = [ + self._get_uuid(result), + self._get_state('WARNING') + ] + color = C.COLOR_WARN + for warn in result['warnings']: + msg = line + [warn] + self._output(msg, color) + del result['warnings'] + if result.get('deprecations', False): + line = [ + self._get_uuid(result), + self._get_state('DEPRECATED') + ] + color = C.COLOR_DEPRECATE + # TODO(mwhahaha): handle deps correctly as they are a dict + for dep in result['deprecations']: + msg = line + [dep['msg']] + self._output(msg, color) + del result['deprecations'] + + def _task_line(self, task, state, color=None): + if not task.name: + return + line = [ + self._get_uuid(task), + self._get_state(state), + self._get_task_name(task) + ] + self._output(line, color) + + def v2_playbook_on_task_start(self, task, is_conditional): + self._task_line(task, 'TASK') + + def v2_playbook_on_handler_task_start(self, task): + self._task_line(task, 'HANDLER') + + def v2_playbook_on_cleanup_task_start(self, task): + self._task_line(task, 'CLEANUP') + + # TODO(mwhahaha): Push fix into default for broken version of this + # function because get_option doesn't work when k is not in _plugin_options + def v2_runner_on_start(self, host, task): + if ('show_per_host_start' in self._plugin_options + and self.get_options('show_per_host_start')): + color = C.COLOR_HIGHLIGHT + line = [ + self._get_uuid(task), + self._get_state('START'), + self._get_task_name(task=task), + host.name + ] + self._output(line, color) + + def v2_runner_item_on_ok(self, result): + if isinstance(result._task, TaskInclude): + return + (line, color) = self._get_item_line(result, item=True) + self._handle_warnings(result._result) + if result._task.loop and 'results' in result._result: + self._process_items(result) + else: + if self._run_is_verbose(result): + line.append('result=%s' % self._dump_results(result._result)) + self._display.display(' | '.join(line), color=color) + + def v2_runner_on_failed(self, result, ignore_errors=False): + self._clean_results(result._result, result._task.action) + # TODO(mwhahaha): implement this one + self._handle_exception(result._result) + self._handle_warnings(result._result) + if result._task.loop and 'results' in result._result: + self._process_items(result) + else: + if ignore_errors: + status = 'IGNORED' + color = C.COLOR_SKIP + else: + status = 'FATAL' + color = C.COLOR_ERROR + + line = [ + self._get_uuid(result), + self._get_state(status), + self._get_task_name(result), + self._get_host(result=result) + ] + item_result = self._get_item_label(result._result) + # don't display if None + if item_result: + line.append('item=%s' % item_result) + line.append('error=%s' % self._dump_results(result._result)) + self._output(line, color) + + def v2_runner_item_on_failed(self, result): + line = [ + self._get_uuid(result), + self._get_state('FATAL'), + self._get_task_name(result), + self._get_host(result=result) + ] + color = C.COLOR_ERROR + item_result = self._get_item_label(result._result) + # don't display if None + if item_result: + line.append('item=%s' % item_result) + line.append('error=%s' % self._dump_results(result._result)) + self._output(line, color) + + def v2_runner_on_ok(self, result): + if isinstance(result._task, TaskInclude): + return + (line, color) = self._get_item_line(result) + self._handle_warnings(result._result) + if result._task.loop and 'results' in result._result: + self._process_items(result) + else: + if self._run_is_verbose(result): + line.append('result=%s' % self._dump_results(result._result)) + self._output(line, color) + + def v2_runner_item_on_skipped(self, result): + if not C.DISPLAY_SKIPPED_HOSTS: + return + self._clean_results(result._result, result._task.action) + line = [ + self._get_uuid(result), + self._get_state('SKIPPED'), + self._get_task_name(result), + self._get_host(result=result) + ] + color = C.COLOR_SKIP + item_result = self._get_item_label(result._result) + # don't display if None + if item_result: + line.append('item=%s' % item_result) + if self._run_is_verbose(result): + line.append('result=%s' % self._dump_results(result._result)) + self._output(line, color) + + def v2_runner_on_skipped(self, result): + # TODO(mwhahaha): this is broken? + # if self.display_skipped_hosts: + self._clean_results(result._result, result._task.action) + if result._task.loop and 'results' in result._result: + self._process_items(result) + else: + line = [ + self._get_uuid(result), + self._get_state('SKIPPED'), + self._get_task_name(result), + self._get_host(result=result) + ] + color = C.COLOR_SKIP + item_result = self._get_item_label(result._result) + # don't display if None + if item_result: + line.append('item=%s' % item_result) + self._output(line, color) + + def v2_playbook_on_include(self, included_file): + color = C.COLOR_SKIP + # included files don't have tasks so lets generate one for the file + # for consistency. Should this be optional? + file_id = str(uuid.uuid4()) + for host in included_file._hosts: + line = [ + file_id, + self._get_state('INCLUDED'), + included_file._filename, + host.name + ] + self._output(line, color) + + def v2_runner_retry(self, result): + retry_count = result._result['retries'] - result._result['attempts'] + # NOTE(mwhahaha): action is async_status we know we're waiting vs a + # failure that is being retried. We can adjust state & color. + # We use getattr because ansible will stop using this if we try and + # access an undefined thing, so let's be careful. + if (getattr(result, '_task', False) + and (getattr(result._task, 'action', False) + in ['async_status'])): + state = 'WAITING' + else: + state = 'RETRY' + color = C.COLOR_DEBUG + host_str = self._get_host(result=result) + line = [ + self._get_uuid(result), + self._get_state(state), + self._get_task_name(result), + host_str, + '%d retries left' % retry_count + ] + if self._run_is_verbose(result, verbosity=2): + line.append("result=%s" % self._dump_results(result._result)) + self._output(line, color)