Example #1
0
File: live.py Project: yarreg/werf
 def __init__(self):
     super(CallbackModule, self).__init__()
     self._play = None
     self._live_stdout_listener = LiveStdoutListener()
Example #2
0
File: live.py Project: yarreg/werf
class CallbackModule(LiveCallbackHelpers):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'live'

    HEADER_PLACEHOLDER = '...'
    HEADER_NAME_INFO_LEN = 55
    HEADER_INFO_MIN_LEN = 5 + 3 + 5  # 5 letters from the edges and a placeholder length

    # name for this tasks can be generated from free_form (_raw_params argument)
    FREE_FORM_MODULES = ('raw', 'script', 'command', 'shell', 'meta')

    # Modules that are optimized by squashing loop items into a single call to
    # the module, mostly packaging modules with name argument
    # (apt, apk, dnf, homebrew, openbsd_pkg, pacman, pkgng, yum, zypper)
    SQUASH_LOOP_MODULES = frozenset(C.DEFAULT_SQUASH_ACTIONS)

    def __init__(self):
        super(CallbackModule, self).__init__()
        self._play = None
        self._live_stdout_listener = LiveStdoutListener()

# header format is:
# action 'task name' [significant args info]
# if task name length exceed its maximum then format is:
# action 'task name'
# if no task name:
# action [significant args info]
# task name and significant args info are squashed to fit into available space

    def _task_details(self, task, start=False):
        task_name = self._clean_str(task.name)
        info = self._get_task_info_from_args(task, start) or ''

        if info != '':
            info_space = self.HEADER_NAME_INFO_LEN - len(task_name)
            if info_space >= self.HEADER_INFO_MIN_LEN or info_space >= len(
                    info):
                info = ' [%s]' % self._squash_center(info, info_space - 3)
            else:
                info = ''

        if task_name != '':
            if len(task_name) + len(info) > self.HEADER_NAME_INFO_LEN:
                task_name = self._squash_right(
                    task_name, self.HEADER_NAME_INFO_LEN - len(info))
            task_name = " '%s'" % task_name

        return u'%s%s%s' % (task.action, task_name, info)

    # item details format is:
    # action 'task name' item 'item_name'
    # if no task_name:
    # action item 'item_name'
    # task_name and item_name are squashed if cannot fit into available space
    def _item_details(self, task, item_result):
        task_name = self._clean_str(task.name)
        if '_ansible_item_label' in item_result:
            item_name = item_result.get('_ansible_item_label', '')
        else:
            item_name = self._clean_str(item_result.get('item', ''))

        if task_name != '':
            task_space = self.HEADER_NAME_INFO_LEN - len(item_name)
            if task_space >= self.HEADER_INFO_MIN_LEN or task_space >= len(
                    task_name):
                task_name = self._squash_right(task_name, task_space - 3)
                task_name = " '%s'" % task_name
            else:
                task_name = ''

        if item_name != '':
            if len(task_name) + len(item_name) > self.HEADER_NAME_INFO_LEN:
                item_name = self._squash_right(
                    item_name, self.HEADER_NAME_INFO_LEN - len(task_name))
            item_name = " item '%s'" % (item_name)

        return u'%s%s%s' % (task.action, task_name, item_name)

    # Return content from significant arguments for well known modules
    # Also support items for the loops.
    def _get_task_info_from_args(self, task, start=False):
        info = ''
        if task.action in self.FREE_FORM_MODULES:
            info = task.args.get('_raw_params', '')
        if task.action == 'file':
            info = task.args.get('path', '')
        if task.action == 'copy':
            info = task.args.get('dest', '')
        if task.action == 'group':
            info = task.args.get('name', '')
        if task.action == 'user':
            info = task.args.get('name', '')
        if task.action == 'get_url':
            info = task.args.get('url', '')
        if task.action == 'getent':
            db = task.args.get('database', '')
            key = task.args.get('key', '')
            info = '%s %s' % (db, key)
        if task.action == 'apk':
            info = task.args.get('name', '')
        if task.action == 'apt':
            info1 = task.args.get('name', None)
            info2 = task.args.get('package', None)
            info3 = task.args.get('pkg', None)
            info = ', '.join(list(self._flatten([info1, info2, info3])))
        if task.action == 'apt_repository':
            info = task.args.get('repo', '')
        if task.action == 'apt_key':
            info = task.args.get('id', '')
        if task.action == 'unarchive':
            info = task.args.get('src', '')
        if task.action == 'locale_gen':
            info = task.args.get('name', '')
        if task.action == 'lineinfile':
            info = task.args.get('path', '')
        if task.action == 'blockinfile':
            info = task.args.get('path', '')
        if task.action == 'composer':
            info = task.args.get('command', 'install')

        if task.loop and start:
            loop_args = task.loop

            if len(loop_args) > 0:
                info = "'%s' over %s" % (info, to_text(loop_args))

        return self._clean_str(info)

    # display task result content with indentation
    # Normally each item is displayed separately. But there are squashed
    # modules, where items are squashed into list and the result is in the first 'results' item.
    def _display_msg(self, task, result, color):
        if task.action in self.SQUASH_LOOP_MODULES and 'results' in result:
            if len(result['results']) > 0:
                return self._display_msg(task, result['results'][0], color)

        # prevent dublication of stdout in case of live_stdout
        if not self._live_stdout_listener.is_live_stdout():
            stdout = result.get('stdout', None)
            if stdout:
                self.LogArgs(vt100.bold, "stdout:", vt100.reset, "\n")
                self.LogArgs(self._indent('  ', stdout), "\n")
            stderr = result.get('stderr', '')
            if stderr:
                self.LogArgs(vt100.bold, "stderr:", vt100.reset, "\n")
                self.LogArgs(
                    self._indent('  ', stringc(stderr, C.COLOR_ERROR)), "\n")

        if self._msg_is_needed(task, result):
            self.LogArgs(stringc(result['msg'], color), "\n")

        if 'rc' in result:
            exitCode = result['rc']
            exitColor = C.COLOR_OK
            if exitCode != '0' and exitCode != 0:
                exitColor = C.COLOR_ERROR
            self.LogArgs(stringc('exit code: %s' % exitCode, exitColor), "\n")

    def _msg_is_needed(self, task, result):
        if 'msg' not in result:
            return False
        # No need to display msg for loop task, because each item is displayed separately.
        # Msg is needed if there are no items.
        if 'results' in result:
            if len(result['results']) > 0:
                return False
        # TODO more variants...
        return True

    def _display_debug_msg(self, task, result):
        #if (self._display.verbosity > 0 or '_ansible_verbose_always' in result) and '_ansible_verbose_override' not in result:
        if task.args.get('msg'):
            color = C.COLOR_OK
            msg = result.get('msg', '')
        if task.args.get('var'):
            var_key = task.args.get('var')
            if isinstance(var_key, (list, dict)):
                var_key = to_text(type(var_key))
            var_obj = result.get(var_key)

            self.LogArgs(vt100.bold, "var=%s" % to_text(task.args.get('var')),
                         ", ", stringc(to_text(type(var_obj)),
                                       C.COLOR_DEBUG), vt100.reset, "\n")

            if isinstance(var_obj, (unicode, str, bytes)):
                color = C.COLOR_OK
                if 'IS NOT DEFINED' in var_obj:
                    color = C.COLOR_ERROR
                msg = var_obj
            else:
                color = C.COLOR_OK
                msg = json.dumps(var_obj, indent=4)

        self.LogArgs(stringc(msg, color), "\n")

    # TODO remove stdout here if live_stdout!
    # TODO handle results for looped tasks
    def _dump_results(self,
                      result,
                      indent=None,
                      sort_keys=True,
                      keep_invocation=False):
        if not indent and (result.get('_ansible_verbose_always')
                           or self._display.verbosity > 2):
            indent = 4

        # All result keys stating with _ansible_ are internal, so remove them from the result before we output anything.
        abridged_result = strip_internal_keys(result)

        # remove invocation unless specifically wanting it
        if not keep_invocation and self._display.verbosity < 3 and 'invocation' in result:
            del abridged_result['invocation']

        # remove diff information from screen output
        if self._display.verbosity < 3 and 'diff' in result:
            del abridged_result['diff']

        # remove exception from screen output
        if 'exception' in abridged_result:
            del abridged_result['exception']

        # remove msg, failed, changed
        #if 'msg' in abridged_result:
        #    del abridged_result['msg']
        if 'failed' in abridged_result:
            del abridged_result['failed']
        if 'changed' in abridged_result:
            del abridged_result['changed']

        if len(abridged_result) > 0:
            return json.dumps(abridged_result,
                              indent=indent,
                              ensure_ascii=False,
                              sort_keys=sort_keys)

        return ''

    def v2_playbook_on_play_start(self, play):
        self._play = play

        logboek.Init()
        try:
            cols = int(os.environ['COLUMNS'])
        except:
            cols = 140
        #cols=60
        self.HEADER_NAME_INFO_LEN = cols - 2
        logboek.SetTerminalWidth(cols)
        logboek.EnableFitMode()
        #logboek.LogProcessStart(play.name)
        self._live_stdout_listener.start()

    def v2_playbook_on_stats(self, stats):
        #pass
        self._live_stdout_listener.stop()
        #if stats.failures:
        #    logboek.LogProcessFail()
        #else:
        #    logboek.LogProcessEnd()

    def v2_playbook_on_task_start(self, task, is_conditional):
        self._display.v("TASK action=%s args=%s" %
                        (task.action, json.dumps(task.args, indent=4)))

        if self._play.strategy == 'free':
            return

        # task header line
        logboek.LogProcessStart(
            self._task_details(task, start=True).encode('utf-8'))
        # reset live_stdout flag on task start
        self._live_stdout_listener.set_live_stdout(False)

    def v2_runner_on_ok(self, result):
        self._display.v(
            "TASK action=%s OK => %s" %
            (result._task.action, json.dumps(result._result, indent=4)))
        self._clean_results(result._result, result._task.action)
        self._handle_warnings(result._result)

        try:
            task = result._task
            color = C.COLOR_OK
            if 'changed' in result._result and result._result['changed']:
                color = C.COLOR_CHANGED

            # task result info if any
            if task.action == 'debug':
                self._display_debug_msg(result._task, result._result)
            else:
                self._display_msg(result._task, result._result, color)
        except Exception as e:
            self.LogArgs(stringc(u'Exception: %s' % e, C.COLOR_ERROR), "\n")
        finally:
            # task footer line
            logboek.LogProcessEnd()

    def v2_runner_item_on_ok(self, result):
        self._display.v(
            "TASK action=%s item OK => %s" %
            (result._task.action, json.dumps(result._result, indent=4)))
        self._clean_results(result._result, result._task.action)
        self._handle_warnings(result._result)

        task = result._task
        if task.action in self.SQUASH_LOOP_MODULES:
            return
        color = C.COLOR_OK
        if 'changed' in result._result and result._result['changed']:
            color = C.COLOR_CHANGED

        # item result info if any
        if task.action == 'debug':
            self._display_debug_msg(result._task, result._result)
        else:
            self._display_msg(result._task, result._result, color)

        logboek.LogProcessStepEnd(u''.join([
            vt100.reset, vt100.bold,
            self._clean_str(self._item_details(task, result._result)),
            vt100.reset, ' ',
            stringc(u'[OK]', color)
        ]).encode('utf-8'))

        # reset live_stdout flag on item end
        self._live_stdout_listener.set_live_stdout(False)

    def v2_runner_on_failed(self, result, ignore_errors=False):
        self._display.v(
            "TASK action=%s FAILED => %s" %
            (result._task.action, json.dumps(result._result, indent=4)))
        self._handle_exception(result._result)
        self._handle_warnings(result._result)

        try:
            task = result._task
            # task result info if any
            self._display_msg(task, result._result, C.COLOR_ERROR)

        except Exception as e:
            logboek.Log(e)
        finally:
            logboek.LogProcessFail()

    def v2_runner_item_on_failed(self, result, ignore_errors=False):
        self._display.v(
            "TASK action=%s ITEM FAILED => %s" %
            (result._task.action, json.dumps(result._result, indent=4)))
        self._handle_exception(result._result)
        self._handle_warnings(result._result)

        task = result._task
        if task.action in self.SQUASH_LOOP_MODULES:
            return
        # task item result info if any
        self._display_msg(task, result._result, C.COLOR_ERROR)
        # task item status line
        logboek.LogProcessStepEnd(u''.join([
            vt100.reset,
            vt100.bold,
            self._clean_str(self._item_details(task, result._result)),
            vt100.reset,
            ' ',
            stringc(u'[FAIL]', C.COLOR_ERROR),
        ]).encode('utf-8'))
        # reset live_stdout flag on item end
        self._live_stdout_listener.set_live_stdout(False)

    def v2_runner_on_skipped(self, result):
        self.LogArgs(stringc("SKIPPED", C.COLOR_SKIP), "\n")
        logboek.LogProcessEnd()

    # Implemented for completeness. Local connection cannot be unreachable.
    def v2_runner_on_unreachable(self, result):
        self.LogArgs(stringc("UNREACHABLE!", C.COLOR_UNREACHABLE), "\n")
        logboek.LogProcessEnd()

    def v2_on_file_diff(self, result):
        if 'diff' in result._result and result._result['diff']:
            self.LogArgs(self._get_diff(result._result['diff']), "\n")

    def _handle_exception(self, result, use_stderr=False):
        if 'exception' in result:
            msg = "An exception occurred during task execution. The full traceback is:\n" + result[
                'exception']
            del result['exception']
            self.LogArgs(stringc(msg, C.COLOR_ERROR))

    def _handle_warnings(self, res):
        ''' display warnings, if enabled and any exist in the result '''
        if C.ACTION_WARNINGS:
            if 'warnings' in res and res['warnings']:
                for warning in res['warnings']:
                    self.LogArgs(
                        stringc(u'[WARNING]: %s' % warning, C.COLOR_WARN))
                del res['warnings']
            if 'deprecations' in res and res['deprecations']:
                for warning in res['deprecations']:
                    self.LogArgs(
                        stringc(self._deprecated_msg(**warning),
                                C.COLOR_DEPRECATE))
                del res['deprecations']

    def _deprecated_msg(self, msg, version=None, removed=False):
        ''' used to print out a deprecation message.'''
        if not removed and not C.DEPRECATION_WARNINGS:
            return

        if not removed:
            if version:
                new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in version %s." % (
                    msg, version)
            else:
                new_msg = "[DEPRECATION WARNING]: %s. This feature will be removed in a future release." % (
                    msg)
            new_msg = new_msg + " Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.\n\n"
        else:
            raise AnsibleError(
                "[DEPRECATED]: %s.\nPlease update your playbooks." % msg)

        return new_msg