Beispiel #1
0
def _format_host(host, data, indent_level=1):
    """
    Main highstate formatter. can be called recursively if a nested highstate
    contains other highstates (ie in an orchestration)
    """
    host = salt.utils.data.decode(host)

    colors = salt.utils.color.get_colors(__opts__.get("color"),
                                         __opts__.get("color_theme"))
    tabular = __opts__.get("state_tabular", False)
    rcounts = {}
    rdurations = []
    hcolor = colors["GREEN"]
    hstrs = []
    nchanges = 0
    strip_colors = __opts__.get("strip_colors", True)

    if isinstance(data, int):
        nchanges = 1
        hstrs.append("{0}    {1}{2[ENDC]}".format(hcolor, data, colors))
        hcolor = colors["CYAN"]  # Print the minion name in cyan
    elif isinstance(data, str):
        # Data in this format is from saltmod.function,
        # so it is always a 'change'
        nchanges = 1
        for data in data.splitlines():
            hstrs.append("{0}    {1}{2[ENDC]}".format(hcolor, data, colors))
        hcolor = colors["CYAN"]  # Print the minion name in cyan
    elif isinstance(data, list):
        # Errors have been detected, list them in RED!
        hcolor = colors["LIGHT_RED"]
        hstrs.append("    {0}Data failed to compile:{1[ENDC]}".format(
            hcolor, colors))
        for err in data:
            if strip_colors:
                err = salt.output.strip_esc_sequence(
                    salt.utils.data.decode(err))
            hstrs.append("{0}----------\n    {1}{2[ENDC]}".format(
                hcolor, err, colors))
    elif isinstance(data, dict):
        # Verify that the needed data is present
        data_tmp = {}
        for tname, info in data.items():
            if (isinstance(info, dict) and tname != "changes" and info
                    and "__run_num__" not in info):
                err = ("The State execution failed to record the order "
                       "in which all states were executed. The state "
                       "return missing data is:")
                hstrs.insert(0, pprint.pformat(info))
                hstrs.insert(0, err)
            if isinstance(info, dict) and "result" in info:
                data_tmp[tname] = info
        data = data_tmp
        # Everything rendered as it should display the output
        for tname in sorted(data, key=lambda k: data[k].get("__run_num__", 0)):
            ret = data[tname]
            # Increment result counts
            rcounts.setdefault(ret["result"], 0)

            # unpack state compression counts
            compressed_count = 1
            if (__opts__.get("state_compress_ids", False)
                    and "_|-state_compressed_" in tname):
                _, _id, _, _ = tname.split("_|-")
                count_match = re.search(r"\((\d+)\)$", _id)
                if count_match:
                    compressed_count = int(count_match.group(1))

            rcounts[ret["result"]] += compressed_count
            rduration = ret.get("duration", 0)
            try:
                rdurations.append(float(rduration))
            except ValueError:
                rduration, _, _ = rduration.partition(" ms")
                try:
                    rdurations.append(float(rduration))
                except ValueError:
                    log.error("Cannot parse a float from duration %s",
                              ret.get("duration", 0))

            tcolor = colors["GREEN"]
            if ret.get("name") in [
                    "state.orch", "state.orchestrate", "state.sls"
            ]:
                nested = output(ret["changes"], indent_level=indent_level + 1)
                ctext = re.sub("^",
                               " " * 14 * indent_level,
                               "\n" + nested,
                               flags=re.MULTILINE)
                schanged = True
                nchanges += 1
            else:
                schanged, ctext = _format_changes(ret["changes"])
                # if compressed, the changes are keyed by name
                if schanged and compressed_count > 1:
                    nchanges += len(ret["changes"].get("compressed changes",
                                                       {})) or 1
                else:
                    nchanges += 1 if schanged else 0

            # Skip this state if it was successful & diff output was requested
            if (__opts__.get("state_output_diff", False) and ret["result"]
                    and not schanged):
                continue

            # Skip this state if state_verbose is False, the result is True and
            # there were no changes made
            if (not __opts__.get("state_verbose", False) and ret["result"]
                    and not schanged):
                continue

            if schanged:
                tcolor = colors["CYAN"]
            if ret["result"] is False:
                hcolor = colors["RED"]
                tcolor = colors["RED"]
            if ret["result"] is None:
                hcolor = colors["LIGHT_YELLOW"]
                tcolor = colors["LIGHT_YELLOW"]

            state_output = __opts__.get("state_output", "full").lower()
            comps = tname.split("_|-")

            if state_output.endswith("_id"):
                # Swap in the ID for the name. Refs #35137
                comps[2] = comps[1]

            if state_output.startswith("filter"):
                # By default, full data is shown for all types. However, return
                # data may be excluded by setting state_output_exclude to a
                # comma-separated list of True, False or None, or including the
                # same list with the exclude option on the command line. For
                # now, this option must include a comma. For example:
                #     exclude=True,
                # The same functionality is also available for making return
                # data terse, instead of excluding it.
                cliargs = __opts__.get("arg", [])
                clikwargs = {}
                for item in cliargs:
                    if isinstance(item, dict) and "__kwarg__" in item:
                        clikwargs = item.copy()

                exclude = clikwargs.get(
                    "exclude", __opts__.get("state_output_exclude", []))
                if isinstance(exclude, str):
                    exclude = str(exclude).split(",")

                terse = clikwargs.get("terse",
                                      __opts__.get("state_output_terse", []))
                if isinstance(terse, str):
                    terse = str(terse).split(",")

                if str(ret["result"]) in terse:
                    msg = _format_terse(tcolor, comps, ret, colors, tabular)
                    hstrs.append(msg)
                    continue
                if str(ret["result"]) in exclude:
                    continue

            elif any((
                    state_output.startswith("terse"),
                    state_output.startswith("mixed")
                    and ret["result"] is not False,  # only non-error'd
                    state_output.startswith("changes") and ret["result"]
                    and not schanged,  # non-error'd non-changed
            )):
                # Print this chunk in a terse way and continue in the loop
                msg = _format_terse(tcolor, comps, ret, colors, tabular)
                hstrs.append(msg)
                continue

            state_lines = [
                "{tcolor}----------{colors[ENDC]}",
                "    {tcolor}      ID: {comps[1]}{colors[ENDC]}",
                "    {tcolor}Function: {comps[0]}.{comps[3]}{colors[ENDC]}",
                "    {tcolor}  Result: {ret[result]!s}{colors[ENDC]}",
                "    {tcolor} Comment: {comment}{colors[ENDC]}",
            ]
            if __opts__.get("state_output_profile") and "start_time" in ret:
                state_lines.extend([
                    "    {tcolor} Started: {ret[start_time]!s}{colors[ENDC]}",
                    "    {tcolor}Duration: {ret[duration]!s}{colors[ENDC]}",
                ])
            # This isn't the prettiest way of doing this, but it's readable.
            if comps[1] != comps[2]:
                state_lines.insert(
                    3, "    {tcolor}    Name: {comps[2]}{colors[ENDC]}")
            # be sure that ret['comment'] is utf-8 friendly
            try:
                if not isinstance(ret["comment"], str):
                    ret["comment"] = str(ret["comment"])
            except UnicodeDecodeError:
                # If we got here, we're on Python 2 and ret['comment'] somehow
                # contained a str type with unicode content.
                ret["comment"] = salt.utils.stringutils.to_unicode(
                    ret["comment"])
            try:
                comment = salt.utils.data.decode(ret["comment"])
                comment = comment.strip().replace("\n", "\n" + " " * 14)
            except AttributeError:  # Assume comment is a list
                try:
                    comment = ret["comment"].join(" ").replace(
                        "\n", "\n" + " " * 13)
                except AttributeError:
                    # Comment isn't a list either, just convert to string
                    comment = str(ret["comment"])
                    comment = comment.strip().replace("\n", "\n" + " " * 14)
            # If there is a data attribute, append it to the comment
            if "data" in ret:
                if isinstance(ret["data"], list):
                    for item in ret["data"]:
                        comment = "{} {}".format(comment, item)
                elif isinstance(ret["data"], dict):
                    for key, value in ret["data"].items():
                        comment = "{}\n\t\t{}: {}".format(comment, key, value)
                else:
                    comment = "{} {}".format(comment, ret["data"])
            for detail in ["start_time", "duration"]:
                ret.setdefault(detail, "")
            if ret["duration"] != "":
                ret["duration"] = "{} ms".format(ret["duration"])
            svars = {
                "tcolor": tcolor,
                "comps": comps,
                "ret": ret,
                "comment": salt.utils.data.decode(comment),
                # This nukes any trailing \n and indents the others.
                "colors": colors,
            }
            hstrs.extend([sline.format(**svars) for sline in state_lines])
            changes = "     Changes:   " + ctext
            hstrs.append("{0}{1}{2[ENDC]}".format(tcolor, changes, colors))

            if "warnings" in ret:
                rcounts.setdefault("warnings", 0)
                rcounts["warnings"] += 1
                wrapper = textwrap.TextWrapper(width=80,
                                               initial_indent=" " * 14,
                                               subsequent_indent=" " * 14)
                hstrs.append(
                    "   {colors[LIGHT_RED]} Warnings: {0}{colors[ENDC]}".
                    format(wrapper.fill("\n".join(ret["warnings"])).lstrip(),
                           colors=colors))

        # Append result counts to end of output
        colorfmt = "{0}{1}{2[ENDC]}"
        rlabel = {
            True: "Succeeded",
            False: "Failed",
            None: "Not Run",
            "warnings": "Warnings",
        }
        count_max_len = max([len(str(x)) for x in rcounts.values()] or [0])
        label_max_len = max([len(x) for x in rlabel.values()] or [0])
        line_max_len = label_max_len + count_max_len + 2  # +2 for ': '
        hstrs.append(
            colorfmt.format(
                colors["CYAN"],
                "\nSummary for {}\n{}".format(host, "-" * line_max_len),
                colors,
            ))

        def _counts(label, count):
            return "{0}: {1:>{2}}".format(label, count,
                                          line_max_len - (len(label) + 2))

        # Successful states
        changestats = []
        if None in rcounts and rcounts.get(None, 0) > 0:
            # test=True states
            changestats.append(
                colorfmt.format(
                    colors["LIGHT_YELLOW"],
                    "unchanged={}".format(rcounts.get(None, 0)),
                    colors,
                ))
        if nchanges > 0:
            changestats.append(
                colorfmt.format(colors["GREEN"], "changed={}".format(nchanges),
                                colors))
        if changestats:
            changestats = " ({})".format(", ".join(changestats))
        else:
            changestats = ""
        hstrs.append(
            colorfmt.format(
                colors["GREEN"],
                _counts(rlabel[True],
                        rcounts.get(True, 0) + rcounts.get(None, 0)),
                colors,
            ) + changestats)

        # Failed states
        num_failed = rcounts.get(False, 0)
        hstrs.append(
            colorfmt.format(
                colors["RED"] if num_failed else colors["CYAN"],
                _counts(rlabel[False], num_failed),
                colors,
            ))

        if __opts__.get("state_output_pct", False):
            # Add success percentages to the summary output
            try:
                success_pct = round(
                    ((rcounts.get(True, 0) + rcounts.get(None, 0)) /
                     (sum(rcounts.values()) - rcounts.get("warnings", 0))) *
                    100,
                    2,
                )

                hstrs.append(
                    colorfmt.format(
                        colors["GREEN"],
                        _counts("Success %", success_pct),
                        colors,
                    ))
            except ZeroDivisionError:
                pass

            # Add failure percentages to the summary output
            try:
                failed_pct = round(
                    (num_failed /
                     (sum(rcounts.values()) - rcounts.get("warnings", 0))) *
                    100,
                    2,
                )

                hstrs.append(
                    colorfmt.format(
                        colors["RED"] if num_failed else colors["CYAN"],
                        _counts("Failure %", failed_pct),
                        colors,
                    ))
            except ZeroDivisionError:
                pass

        num_warnings = rcounts.get("warnings", 0)
        if num_warnings:
            hstrs.append(
                colorfmt.format(
                    colors["LIGHT_RED"],
                    _counts(rlabel["warnings"], num_warnings),
                    colors,
                ))
        totals = "{0}\nTotal states run: {1:>{2}}".format(
            "-" * line_max_len,
            sum(rcounts.values()) - rcounts.get("warnings", 0),
            line_max_len - 7,
        )
        hstrs.append(colorfmt.format(colors["CYAN"], totals, colors))

        if __opts__.get("state_output_profile"):
            sum_duration = sum(rdurations)
            duration_unit = "ms"
            # convert to seconds if duration is 1000ms or more
            if sum_duration > 999:
                sum_duration /= 1000
                duration_unit = "s"
            total_duration = "Total run time: {} {}".format(
                "{:.3f}".format(sum_duration).rjust(line_max_len - 5),
                duration_unit)
            hstrs.append(
                colorfmt.format(colors["CYAN"], total_duration, colors))

    if strip_colors:
        host = salt.output.strip_esc_sequence(host)
    hstrs.insert(0, "{0}{1}:{2[ENDC]}".format(hcolor, host, colors))
    return "\n".join(hstrs), nchanges > 0
Beispiel #2
0
def _format_host(host, data, indent_level=1):
    '''
    Main highstate formatter. can be called recursively if a nested highstate
    contains other highstates (ie in an orchestration)
    '''
    host = salt.utils.data.decode(host)

    colors = salt.utils.color.get_colors(__opts__.get('color'),
                                         __opts__.get('color_theme'))
    tabular = __opts__.get('state_tabular', False)
    rcounts = {}
    rdurations = []
    hcolor = colors['GREEN']
    hstrs = []
    nchanges = 0
    strip_colors = __opts__.get('strip_colors', True)

    if isinstance(data, int) or isinstance(data, six.string_types):
        # Data in this format is from saltmod.function,
        # so it is always a 'change'
        nchanges = 1
        hstrs.append(('{0}    {1}{2[ENDC]}'.format(hcolor, data, colors)))
        hcolor = colors['CYAN']  # Print the minion name in cyan
    if isinstance(data, list):
        # Errors have been detected, list them in RED!
        hcolor = colors['LIGHT_RED']
        hstrs.append(
            ('    {0}Data failed to compile:{1[ENDC]}'.format(hcolor, colors)))
        for err in data:
            if strip_colors:
                err = salt.output.strip_esc_sequence(
                    salt.utils.data.decode(err))
            hstrs.append(
                ('{0}----------\n    {1}{2[ENDC]}'.format(hcolor, err,
                                                          colors)))
    if isinstance(data, dict):
        # Verify that the needed data is present
        data_tmp = {}
        for tname, info in six.iteritems(data):
            if isinstance(
                    info, dict
            ) and tname is not 'changes' and info and '__run_num__' not in info:
                err = ('The State execution failed to record the order '
                       'in which all states were executed. The state '
                       'return missing data is:')
                hstrs.insert(0, pprint.pformat(info))
                hstrs.insert(0, err)
            if isinstance(info, dict) and 'result' in info:
                data_tmp[tname] = info
        data = data_tmp
        # Everything rendered as it should display the output
        for tname in sorted(data, key=lambda k: data[k].get('__run_num__', 0)):
            ret = data[tname]
            # Increment result counts
            rcounts.setdefault(ret['result'], 0)
            rcounts[ret['result']] += 1
            rduration = ret.get('duration', 0)
            try:
                rdurations.append(float(rduration))
            except ValueError:
                rduration, _, _ = rduration.partition(' ms')
                try:
                    rdurations.append(float(rduration))
                except ValueError:
                    log.error('Cannot parse a float from duration %s',
                              ret.get('duration', 0))

            tcolor = colors['GREEN']
            if ret.get('name') in [
                    'state.orch', 'state.orchestrate', 'state.sls'
            ]:
                nested = output(ret['changes']['return'],
                                indent_level=indent_level + 1)
                ctext = re.sub('^',
                               ' ' * 14 * indent_level,
                               '\n' + nested,
                               flags=re.MULTILINE)
                schanged = True
                nchanges += 1
            else:
                schanged, ctext = _format_changes(ret['changes'])
                nchanges += 1 if schanged else 0

            # Skip this state if it was successful & diff output was requested
            if __opts__.get('state_output_diff', False) and \
               ret['result'] and not schanged:
                continue

            # Skip this state if state_verbose is False, the result is True and
            # there were no changes made
            if not __opts__.get('state_verbose', False) and \
               ret['result'] and not schanged:
                continue

            if schanged:
                tcolor = colors['CYAN']
            if ret['result'] is False:
                hcolor = colors['RED']
                tcolor = colors['RED']
            if ret['result'] is None:
                hcolor = colors['LIGHT_YELLOW']
                tcolor = colors['LIGHT_YELLOW']

            state_output = __opts__.get('state_output', 'full').lower()
            comps = tname.split('_|-')

            if state_output.endswith('_id'):
                # Swap in the ID for the name. Refs #35137
                comps[2] = comps[1]

            if state_output.startswith('filter'):
                # By default, full data is shown for all types. However, return
                # data may be excluded by setting state_output_exclude to a
                # comma-separated list of True, False or None, or including the
                # same list with the exclude option on the command line. For
                # now, this option must include a comma. For example:
                #     exclude=True,
                # The same functionality is also available for making return
                # data terse, instead of excluding it.
                cliargs = __opts__.get('arg', [])
                clikwargs = {}
                for item in cliargs:
                    if isinstance(item, dict) and '__kwarg__' in item:
                        clikwargs = item.copy()

                exclude = clikwargs.get(
                    'exclude', __opts__.get('state_output_exclude', []))
                if isinstance(exclude, six.string_types):
                    exclude = six.text_type(exclude).split(',')

                terse = clikwargs.get('terse',
                                      __opts__.get('state_output_terse', []))
                if isinstance(terse, six.string_types):
                    terse = six.text_type(terse).split(',')

                if six.text_type(ret['result']) in terse:
                    msg = _format_terse(tcolor, comps, ret, colors, tabular)
                    hstrs.append(msg)
                    continue
                if six.text_type(ret['result']) in exclude:
                    continue

            elif any((
                    state_output.startswith('terse'),
                    state_output.startswith('mixed')
                    and ret['result'] is not False,  # only non-error'd
                    state_output.startswith('changes') and ret['result']
                    and not schanged  # non-error'd non-changed
            )):
                # Print this chunk in a terse way and continue in the loop
                msg = _format_terse(tcolor, comps, ret, colors, tabular)
                hstrs.append(msg)
                continue

            state_lines = [
                '{tcolor}----------{colors[ENDC]}',
                '    {tcolor}      ID: {comps[1]}{colors[ENDC]}',
                '    {tcolor}Function: {comps[0]}.{comps[3]}{colors[ENDC]}',
                '    {tcolor}  Result: {ret[result]!s}{colors[ENDC]}',
                '    {tcolor} Comment: {comment}{colors[ENDC]}',
            ]
            if __opts__.get('state_output_profile',
                            True) and 'start_time' in ret:
                state_lines.extend([
                    '    {tcolor} Started: {ret[start_time]!s}{colors[ENDC]}',
                    '    {tcolor}Duration: {ret[duration]!s}{colors[ENDC]}',
                ])
            # This isn't the prettiest way of doing this, but it's readable.
            if comps[1] != comps[2]:
                state_lines.insert(
                    3, '    {tcolor}    Name: {comps[2]}{colors[ENDC]}')
            # be sure that ret['comment'] is utf-8 friendly
            try:
                if not isinstance(ret['comment'], six.text_type):
                    ret['comment'] = six.text_type(ret['comment'])
            except UnicodeDecodeError:
                # If we got here, we're on Python 2 and ret['comment'] somehow
                # contained a str type with unicode content.
                ret['comment'] = salt.utils.stringutils.to_unicode(
                    ret['comment'])
            try:
                comment = salt.utils.data.decode(ret['comment'])
                comment = comment.strip().replace('\n', '\n' + ' ' * 14)
            except AttributeError:  # Assume comment is a list
                try:
                    comment = ret['comment'].join(' ').replace(
                        '\n', '\n' + ' ' * 13)
                except AttributeError:
                    # Comment isn't a list either, just convert to string
                    comment = six.text_type(ret['comment'])
                    comment = comment.strip().replace('\n', '\n' + ' ' * 14)
            # If there is a data attribute, append it to the comment
            if 'data' in ret:
                if isinstance(ret['data'], list):
                    for item in ret['data']:
                        comment = '{0} {1}'.format(comment, item)
                elif isinstance(ret['data'], dict):
                    for key, value in ret['data'].items():
                        comment = '{0}\n\t\t{1}: {2}'.format(
                            comment, key, value)
                else:
                    comment = '{0} {1}'.format(comment, ret['data'])
            for detail in ['start_time', 'duration']:
                ret.setdefault(detail, '')
            if ret['duration'] != '':
                ret['duration'] = '{0} ms'.format(ret['duration'])
            svars = {
                'tcolor': tcolor,
                'comps': comps,
                'ret': ret,
                'comment': salt.utils.data.decode(comment),
                # This nukes any trailing \n and indents the others.
                'colors': colors
            }
            hstrs.extend([sline.format(**svars) for sline in state_lines])
            changes = '     Changes:   ' + ctext
            hstrs.append(('{0}{1}{2[ENDC]}'.format(tcolor, changes, colors)))

            if 'warnings' in ret:
                rcounts.setdefault('warnings', 0)
                rcounts['warnings'] += 1
                wrapper = textwrap.TextWrapper(width=80,
                                               initial_indent=' ' * 14,
                                               subsequent_indent=' ' * 14)
                hstrs.append(
                    '   {colors[LIGHT_RED]} Warnings: {0}{colors[ENDC]}'.
                    format(wrapper.fill('\n'.join(ret['warnings'])).lstrip(),
                           colors=colors))

        # Append result counts to end of output
        colorfmt = '{0}{1}{2[ENDC]}'
        rlabel = {
            True: 'Succeeded',
            False: 'Failed',
            None: 'Not Run',
            'warnings': 'Warnings'
        }
        count_max_len = max(
            [len(six.text_type(x)) for x in six.itervalues(rcounts)] or [0])
        label_max_len = max([len(x) for x in six.itervalues(rlabel)] or [0])
        line_max_len = label_max_len + count_max_len + 2  # +2 for ': '
        hstrs.append(
            colorfmt.format(
                colors['CYAN'],
                '\nSummary for {0}\n{1}'.format(host,
                                                '-' * line_max_len), colors))

        def _counts(label, count):
            return '{0}: {1:>{2}}'.format(label, count,
                                          line_max_len - (len(label) + 2))

        # Successful states
        changestats = []
        if None in rcounts and rcounts.get(None, 0) > 0:
            # test=True states
            changestats.append(
                colorfmt.format(colors['LIGHT_YELLOW'],
                                'unchanged={0}'.format(rcounts.get(None, 0)),
                                colors))
        if nchanges > 0:
            changestats.append(
                colorfmt.format(colors['GREEN'],
                                'changed={0}'.format(nchanges), colors))
        if changestats:
            changestats = ' ({0})'.format(', '.join(changestats))
        else:
            changestats = ''
        hstrs.append(
            colorfmt.format(
                colors['GREEN'],
                _counts(rlabel[True],
                        rcounts.get(True, 0) + rcounts.get(None, 0)), colors) +
            changestats)

        # Failed states
        num_failed = rcounts.get(False, 0)
        hstrs.append(
            colorfmt.format(colors['RED'] if num_failed else colors['CYAN'],
                            _counts(rlabel[False], num_failed), colors))

        num_warnings = rcounts.get('warnings', 0)
        if num_warnings:
            hstrs.append(
                colorfmt.format(colors['LIGHT_RED'],
                                _counts(rlabel['warnings'], num_warnings),
                                colors))
        totals = '{0}\nTotal states run: {1:>{2}}'.format(
            '-' * line_max_len,
            sum(six.itervalues(rcounts)) - rcounts.get('warnings', 0),
            line_max_len - 7)
        hstrs.append(colorfmt.format(colors['CYAN'], totals, colors))

        if __opts__.get('state_output_profile', True):
            sum_duration = sum(rdurations)
            duration_unit = 'ms'
            # convert to seconds if duration is 1000ms or more
            if sum_duration > 999:
                sum_duration /= 1000
                duration_unit = 's'
            total_duration = 'Total run time: {0} {1}'.format(
                '{0:.3f}'.format(sum_duration).rjust(line_max_len - 5),
                duration_unit)
            hstrs.append(
                colorfmt.format(colors['CYAN'], total_duration, colors))

    if strip_colors:
        host = salt.output.strip_esc_sequence(host)
    hstrs.insert(0, ('{0}{1}:{2[ENDC]}'.format(hcolor, host, colors)))
    return '\n'.join(hstrs), nchanges > 0