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
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