"Fossies" - the Fresh Open Source Software Archive

Member "salt-3006.1/salt/output/highstate.py" (5 May 2023, 31402 Bytes) of package /linux/misc/salt-3006.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "highstate.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3005.1-4_vs_3006.0rc1.

    1 """
    2 Outputter for displaying results of state runs
    3 ==============================================
    4 
    5 The return data from the Highstate command is a standard data structure
    6 which is parsed by the highstate outputter to deliver a clean and readable
    7 set of information about the HighState run on minions.
    8 
    9 Two configurations can be set to modify the highstate outputter. These values
   10 can be set in the master config to change the output of the ``salt`` command or
   11 set in the minion config to change the output of the ``salt-call`` command.
   12 
   13 state_verbose:
   14     By default `state_verbose` is set to `True`, setting this to `False` will
   15     instruct the highstate outputter to omit displaying anything in green, this
   16     means that nothing with a result of True and no changes will not be printed
   17 
   18 state_output:
   19     The highstate outputter has six output modes,
   20     ``full``, ``terse``, ``mixed``, ``changes`` and ``filter``
   21 
   22     * The default is set to ``full``, which will display many lines of detailed
   23       information for each executed chunk.
   24 
   25     * If ``terse`` is used, then the output is greatly simplified and shown in
   26       only one line.
   27 
   28     * If ``mixed`` is used, then terse output will be used unless a state
   29       failed, in which case full output will be used.
   30 
   31     * If ``changes`` is used, then terse output will be used if there was no
   32       error and no changes, otherwise full output will be used.
   33 
   34     * If ``filter`` is used, then either or both of two different filters can be
   35       used: ``exclude`` or ``terse``.
   36 
   37         * for ``exclude``, state.highstate expects a list of states to be excluded (or ``None``)
   38           followed by ``True`` for terse output or ``False`` for regular output.
   39           Because of parsing nuances, if only one of these is used, it must still
   40           contain a comma. For instance: `exclude=True,`.
   41 
   42         * for ``terse``, state.highstate expects simply ``True`` or ``False``.
   43 
   44       These can be set as such from the command line, or in the Salt config as
   45       `state_output_exclude` or `state_output_terse`, respectively.
   46 
   47     The output modes have one modifier:
   48 
   49     ``full_id``, ``terse_id``, ``mixed_id``, ``changes_id`` and ``filter_id``
   50     If ``_id`` is used, then the corresponding form will be used, but the value for ``name``
   51     will be drawn from the state ID. This is useful for cases where the name
   52     value might be very long and hard to read.
   53 
   54 state_tabular:
   55     If `state_output` uses the terse output, set this to `True` for an aligned
   56     output format.  If you wish to use a custom format, this can be set to a
   57     string.
   58 
   59 state_output_pct:
   60     Set `state_output_pct` to `True` in order to add "Success %" and "Failure %"
   61     to the "Summary" section at the end of the highstate output.
   62 
   63 state_compress_ids:
   64     Set `state_compress_ids` to `True` to aggregate information about states
   65     which have multiple "names" under the same state ID in the highstate output.
   66     This is useful in combination with the `terse_id` value set in the
   67     `state_output` option when states are using the `names` state parameter.
   68 
   69 Example usage:
   70 
   71 If ``state_output: filter`` is set in the configuration file:
   72 
   73 .. code-block:: bash
   74 
   75     salt '*' state.highstate exclude=None,True
   76 
   77 
   78 means to exclude no states from the highstate and turn on terse output.
   79 
   80 .. code-block:: bash
   81 
   82     salt twd state.highstate exclude=problemstate1,problemstate2,False
   83 
   84 
   85 means to exclude states ``problemstate1`` and ``problemstate2``
   86 from the highstate, and use regular output.
   87 
   88 Example output for the above highstate call when ``top.sls`` defines only
   89 one other state to apply to minion ``twd``:
   90 
   91 .. code-block:: text
   92 
   93     twd:
   94 
   95     Summary for twd
   96     ------------
   97     Succeeded: 1 (changed=1)
   98     Failed:    0
   99     ------------
  100     Total states run:     1
  101 
  102 
  103 Example output with no special settings in configuration files:
  104 
  105 .. code-block:: text
  106 
  107     myminion:
  108     ----------
  109               ID: test.ping
  110         Function: module.run
  111           Result: True
  112          Comment: Module function test.ping executed
  113          Changes:
  114                   ----------
  115                   ret:
  116                       True
  117 
  118     Summary for myminion
  119     ------------
  120     Succeeded: 1
  121     Failed:    0
  122     ------------
  123     Total:     0
  124 """
  125 
  126 
  127 import collections
  128 import logging
  129 import pprint
  130 import re
  131 import textwrap
  132 
  133 import salt.output
  134 import salt.utils.color
  135 import salt.utils.data
  136 import salt.utils.stringutils
  137 
  138 log = logging.getLogger(__name__)
  139 
  140 
  141 def _compress_ids(data):
  142     """
  143     Function to take incoming raw state data and roll IDs with multiple names
  144     into a single state block for reporting purposes. This functionality is most
  145     useful for any "_id" state_output options, such as ``terse_id``.
  146 
  147     The following example state has one ID and four names.
  148 
  149     .. code-block:: yaml
  150 
  151     mix-matched results:
  152       cmd.run:
  153         - names:
  154           - "true"
  155           - "false"
  156           - "/bin/true"
  157           - "/bin/false"
  158 
  159     With ``state_output: terse_id`` set, this can create many lines of output
  160     which are not unique enough to be worth the screen real estate they occupy.
  161 
  162     .. code-block:: text
  163 
  164         19:10:10.969049 [  8.546 ms]        cmd.run        Changed   Name: mix-matched results
  165         19:10:10.977998 [  8.606 ms]        cmd.run        Failed    Name: mix-matched results
  166         19:10:10.987116 [  7.618 ms]        cmd.run        Changed   Name: mix-matched results
  167         19:10:10.995172 [  9.344 ms]        cmd.run        Failed    Name: mix-matched results
  168 
  169     Enabling ``state_compress_ids: True`` consolidates the state data by ID and
  170     result (e.g. success or failure). The earliest start time is chosen for
  171     display, duration is aggregated, and the total number of names if shown in
  172     parentheses to the right of the ID.
  173 
  174     .. code-block:: text
  175 
  176         19:10:46.283323 [ 16.236 ms]        cmd.run        Changed   Name: mix-matched results (2)
  177         19:10:46.292181 [ 16.255 ms]        cmd.run        Failed    Name: mix-matched results (2)
  178 
  179     A better real world use case would be passing dozens of files and
  180     directories to the ``names`` parameter of the ``file.absent`` state. The
  181     amount of lines consolidated in that case would be substantial.
  182     """
  183     if not isinstance(data, dict):
  184         return data
  185 
  186     compressed = {}
  187 
  188     # any failures to compress result in passing the original data
  189     # to the highstate outputter without modification
  190     try:
  191         for host, hostdata in data.items():
  192             compressed[host] = {}
  193             # count the number of unique IDs. use sls name and result in the key
  194             # so differences can be shown separately in the output
  195             id_count = collections.Counter(
  196                 [
  197                     "_".join(
  198                         map(
  199                             str,
  200                             [
  201                                 tname.split("_|-")[0],
  202                                 info["__id__"],
  203                                 info["__sls__"],
  204                                 info["result"],
  205                             ],
  206                         )
  207                     )
  208                     for tname, info in hostdata.items()
  209                 ]
  210             )
  211             for tname, info in hostdata.items():
  212                 comps = tname.split("_|-")
  213                 _id = "_".join(
  214                     map(
  215                         str, [comps[0], info["__id__"], info["__sls__"], info["result"]]
  216                     )
  217                 )
  218                 # state does not need to be compressed
  219                 if id_count[_id] == 1:
  220                     compressed[host][tname] = info
  221                     continue
  222 
  223                 # replace name to create a single key by sls and result
  224                 comps[2] = "_".join(
  225                     map(
  226                         str,
  227                         [
  228                             "state_compressed",
  229                             info["__sls__"],
  230                             info["__id__"],
  231                             info["result"],
  232                         ],
  233                     )
  234                 )
  235                 comps[1] = "{} ({})".format(info["__id__"], id_count[_id])
  236                 tname = "_|-".join(comps)
  237 
  238                 # store the first entry as-is
  239                 if tname not in compressed[host]:
  240                     compressed[host][tname] = info
  241                     continue
  242 
  243                 # subsequent entries for compression will use the lowest
  244                 # __run_num__ value, the sum of the duration, and the earliest
  245                 # start time found
  246                 compressed[host][tname]["__run_num__"] = min(
  247                     info["__run_num__"], compressed[host][tname]["__run_num__"]
  248                 )
  249                 compressed[host][tname]["duration"] = round(
  250                     sum([info["duration"], compressed[host][tname]["duration"]]), 3
  251                 )
  252                 compressed[host][tname]["start_time"] = sorted(
  253                     [info["start_time"], compressed[host][tname]["start_time"]]
  254                 )[0]
  255 
  256                 # changes are turned into a dict of changes keyed by name
  257                 if compressed[host][tname].get("changes") and info.get("changes"):
  258                     if not compressed[host][tname]["changes"].get("compressed changes"):
  259                         compressed[host][tname]["changes"] = {
  260                             "compressed changes": {
  261                                 compressed[host][tname]["name"]: compressed[host][
  262                                     tname
  263                                 ]["changes"]
  264                             }
  265                         }
  266                     compressed[host][tname]["changes"]["compressed changes"].update(
  267                         {info["name"]: info["changes"]}
  268                     )
  269                 elif info.get("changes"):
  270                     compressed[host][tname]["changes"] = {
  271                         "compressed changes": {info["name"]: info["changes"]}
  272                     }
  273     except Exception:  # pylint: disable=broad-except
  274         log.warning("Unable to compress state output by ID! Returning output normally.")
  275         return data
  276 
  277     return compressed
  278 
  279 
  280 def output(data, **kwargs):  # pylint: disable=unused-argument
  281     """
  282     The HighState Outputter is only meant to be used with the state.highstate
  283     function, or a function that returns highstate return data.
  284     """
  285     # If additional information is passed through via the "data" dictionary to
  286     # the highstate outputter, such as "outputter" or "retcode", discard it.
  287     # We only want the state data that was passed through, if it is wrapped up
  288     # in the "data" key, as the orchestrate runner does. See Issue #31330,
  289     # pull request #27838, and pull request #27175 for more information.
  290     # account for envelope data if being passed lookup_jid ret
  291     if isinstance(data, dict) and "return" in data:
  292         data = data["return"]
  293 
  294     if isinstance(data, dict) and "data" in data:
  295         data = data["data"]
  296 
  297     # account for envelope data if being passed lookup_jid ret
  298     if isinstance(data, dict) and len(data.keys()) == 1:
  299         _data = next(iter(data.values()))
  300 
  301         if isinstance(_data, dict):
  302             if "jid" in _data and "fun" in _data:
  303                 data = _data.get("return", {}).get("data", data)
  304 
  305     # output() is recursive, if we aren't passed a dict just return it
  306     if isinstance(data, int) or isinstance(data, str):
  307         return data
  308 
  309     if data is None:
  310         return "None"
  311 
  312     # Discard retcode in dictionary as present in orchestrate data
  313     local_masters = [key for key in data.keys() if key.endswith("_master")]
  314     orchestrator_output = "retcode" in data.keys() and len(local_masters) == 1
  315 
  316     if orchestrator_output:
  317         del data["retcode"]
  318 
  319     # pre-process data if state_compress_ids is set
  320     if __opts__.get("state_compress_ids", False):
  321         data = _compress_ids(data)
  322 
  323     indent_level = kwargs.get("indent_level", 1)
  324     ret = [
  325         _format_host(host, hostdata, indent_level=indent_level)[0]
  326         for host, hostdata in data.items()
  327     ]
  328     if ret:
  329         return "\n".join(ret)
  330     log.error(
  331         "Data passed to highstate outputter is not a valid highstate return: %s", data
  332     )
  333     # We should not reach here, but if we do return empty string
  334     return ""
  335 
  336 
  337 def _format_host(host, data, indent_level=1):
  338     """
  339     Main highstate formatter. can be called recursively if a nested highstate
  340     contains other highstates (ie in an orchestration)
  341     """
  342     host = salt.utils.data.decode(host)
  343 
  344     colors = salt.utils.color.get_colors(
  345         __opts__.get("color"), __opts__.get("color_theme")
  346     )
  347     tabular = __opts__.get("state_tabular", False)
  348     rcounts = {}
  349     rdurations = []
  350     pdurations = []
  351     hcolor = colors["GREEN"]
  352     hstrs = []
  353     nchanges = 0
  354     strip_colors = __opts__.get("strip_colors", True)
  355 
  356     if isinstance(data, int):
  357         nchanges = 1
  358         hstrs.append("{0}    {1}{2[ENDC]}".format(hcolor, data, colors))
  359         hcolor = colors["CYAN"]  # Print the minion name in cyan
  360     elif isinstance(data, str):
  361         # Data in this format is from saltmod.function,
  362         # so it is always a 'change'
  363         nchanges = 1
  364         for data in data.splitlines():
  365             hstrs.append("{0}    {1}{2[ENDC]}".format(hcolor, data, colors))
  366         hcolor = colors["CYAN"]  # Print the minion name in cyan
  367     elif isinstance(data, list):
  368         # Errors have been detected, list them in RED!
  369         hcolor = colors["LIGHT_RED"]
  370         hstrs.append("    {0}Data failed to compile:{1[ENDC]}".format(hcolor, colors))
  371         for err in data:
  372             if strip_colors:
  373                 err = salt.output.strip_esc_sequence(salt.utils.data.decode(err))
  374             hstrs.append("{0}----------\n    {1}{2[ENDC]}".format(hcolor, err, colors))
  375     elif isinstance(data, dict):
  376         # Verify that the needed data is present
  377         data_tmp = {}
  378         for tname, info in data.items():
  379             if (
  380                 isinstance(info, dict)
  381                 and tname != "changes"
  382                 and info
  383                 and "__run_num__" not in info
  384             ):
  385                 err = (
  386                     "The State execution failed to record the order "
  387                     "in which all states were executed. The state "
  388                     "return missing data is:"
  389                 )
  390                 hstrs.insert(0, pprint.pformat(info))
  391                 hstrs.insert(0, err)
  392             if isinstance(info, dict) and "result" in info:
  393                 data_tmp[tname] = info
  394         data = data_tmp
  395         # Everything rendered as it should display the output
  396         for tname in sorted(data, key=lambda k: data[k].get("__run_num__", 0)):
  397             ret = data[tname]
  398             # Increment result counts
  399             rcounts.setdefault(ret["result"], 0)
  400 
  401             # unpack state compression counts
  402             compressed_count = 1
  403             if (
  404                 __opts__.get("state_compress_ids", False)
  405                 and "_|-state_compressed_" in tname
  406             ):
  407                 _, _id, _, _ = tname.split("_|-")
  408                 count_match = re.search(r"\((\d+)\)$", _id)
  409                 if count_match:
  410                     compressed_count = int(count_match.group(1))
  411 
  412             rcounts[ret["result"]] += compressed_count
  413             if "__parallel__" in ret:
  414                 pduration = ret.get("duration", 0)
  415                 try:
  416                     pdurations.append(float(pduration))
  417                 except ValueError:
  418                     pduration, _, _ = pduration.partition(" ms")
  419                     try:
  420                         pdurations.append(float(pduration))
  421                     except ValueError:
  422                         log.error(
  423                             "Cannot parse a float from duration %s",
  424                             ret.get("duration", 0),
  425                         )
  426             else:
  427                 rduration = ret.get("duration", 0)
  428                 try:
  429                     rdurations.append(float(rduration))
  430                 except ValueError:
  431                     rduration, _, _ = rduration.partition(" ms")
  432                     try:
  433                         rdurations.append(float(rduration))
  434                     except ValueError:
  435                         log.error(
  436                             "Cannot parse a float from duration %s",
  437                             ret.get("duration", 0),
  438                         )
  439 
  440             tcolor = colors["GREEN"]
  441             if ret.get("name") in ["state.orch", "state.orchestrate", "state.sls"]:
  442                 nested = output(ret["changes"], indent_level=indent_level + 1)
  443                 ctext = re.sub(
  444                     "^", " " * 14 * indent_level, "\n" + nested, flags=re.MULTILINE
  445                 )
  446                 schanged = True
  447                 nchanges += 1
  448             else:
  449                 schanged, ctext = _format_changes(ret["changes"])
  450                 # if compressed, the changes are keyed by name
  451                 if schanged and compressed_count > 1:
  452                     nchanges += len(ret["changes"].get("compressed changes", {})) or 1
  453                 else:
  454                     nchanges += 1 if schanged else 0
  455 
  456             # Skip this state if it was successful & diff output was requested
  457             if (
  458                 __opts__.get("state_output_diff", False)
  459                 and ret["result"]
  460                 and not schanged
  461             ):
  462                 continue
  463 
  464             # Skip this state if state_verbose is False, the result is True and
  465             # there were no changes made
  466             if (
  467                 not __opts__.get("state_verbose", False)
  468                 and ret["result"]
  469                 and not schanged
  470             ):
  471                 continue
  472 
  473             if schanged:
  474                 tcolor = colors["CYAN"]
  475             if ret["result"] is False:
  476                 hcolor = colors["RED"]
  477                 tcolor = colors["RED"]
  478             if ret["result"] is None:
  479                 hcolor = colors["LIGHT_YELLOW"]
  480                 tcolor = colors["LIGHT_YELLOW"]
  481 
  482             state_output = __opts__.get("state_output", "full").lower()
  483             comps = tname.split("_|-")
  484 
  485             if state_output.endswith("_id"):
  486                 # Swap in the ID for the name. Refs #35137
  487                 comps[2] = comps[1]
  488 
  489             if state_output.startswith("filter"):
  490                 # By default, full data is shown for all types. However, return
  491                 # data may be excluded by setting state_output_exclude to a
  492                 # comma-separated list of True, False or None, or including the
  493                 # same list with the exclude option on the command line. For
  494                 # now, this option must include a comma. For example:
  495                 #     exclude=True,
  496                 # The same functionality is also available for making return
  497                 # data terse, instead of excluding it.
  498                 cliargs = __opts__.get("arg", [])
  499                 clikwargs = {}
  500                 for item in cliargs:
  501                     if isinstance(item, dict) and "__kwarg__" in item:
  502                         clikwargs = item.copy()
  503 
  504                 exclude = clikwargs.get(
  505                     "exclude", __opts__.get("state_output_exclude", [])
  506                 )
  507                 if isinstance(exclude, str):
  508                     exclude = str(exclude).split(",")
  509 
  510                 terse = clikwargs.get("terse", __opts__.get("state_output_terse", []))
  511                 if isinstance(terse, str):
  512                     terse = str(terse).split(",")
  513 
  514                 if str(ret["result"]) in terse:
  515                     msg = _format_terse(tcolor, comps, ret, colors, tabular)
  516                     hstrs.append(msg)
  517                     continue
  518                 if str(ret["result"]) in exclude:
  519                     continue
  520 
  521             elif any(
  522                 (
  523                     state_output.startswith("terse"),
  524                     state_output.startswith("mixed")
  525                     and ret["result"] is not False,  # only non-error'd
  526                     state_output.startswith("changes")
  527                     and ret["result"]
  528                     and not schanged,  # non-error'd non-changed
  529                 )
  530             ):
  531                 # Print this chunk in a terse way and continue in the loop
  532                 msg = _format_terse(tcolor, comps, ret, colors, tabular)
  533                 hstrs.append(msg)
  534                 continue
  535 
  536             state_lines = [
  537                 "{tcolor}----------{colors[ENDC]}",
  538                 "    {tcolor}      ID: {comps[1]}{colors[ENDC]}",
  539                 "    {tcolor}Function: {comps[0]}.{comps[3]}{colors[ENDC]}",
  540                 "    {tcolor}  Result: {ret[result]!s}{colors[ENDC]}",
  541                 "    {tcolor} Comment: {comment}{colors[ENDC]}",
  542             ]
  543             if __opts__.get("state_output_profile") and "start_time" in ret:
  544                 state_lines.extend(
  545                     [
  546                         "    {tcolor} Started: {ret[start_time]!s}{colors[ENDC]}",
  547                         "    {tcolor}Duration: {ret[duration]!s}{colors[ENDC]}",
  548                     ]
  549                 )
  550             # This isn't the prettiest way of doing this, but it's readable.
  551             if comps[1] != comps[2]:
  552                 state_lines.insert(3, "    {tcolor}    Name: {comps[2]}{colors[ENDC]}")
  553             # be sure that ret['comment'] is utf-8 friendly
  554             try:
  555                 if not isinstance(ret["comment"], str):
  556                     ret["comment"] = str(ret["comment"])
  557             except UnicodeDecodeError:
  558                 # If we got here, we're on Python 2 and ret['comment'] somehow
  559                 # contained a str type with unicode content.
  560                 ret["comment"] = salt.utils.stringutils.to_unicode(ret["comment"])
  561             try:
  562                 comment = salt.utils.data.decode(ret["comment"])
  563                 comment = comment.strip().replace("\n", "\n" + " " * 14)
  564             except AttributeError:  # Assume comment is a list
  565                 try:
  566                     comment = ret["comment"].join(" ").replace("\n", "\n" + " " * 13)
  567                 except AttributeError:
  568                     # Comment isn't a list either, just convert to string
  569                     comment = str(ret["comment"])
  570                     comment = comment.strip().replace("\n", "\n" + " " * 14)
  571             # If there is a data attribute, append it to the comment
  572             if "data" in ret:
  573                 if isinstance(ret["data"], list):
  574                     for item in ret["data"]:
  575                         comment = "{} {}".format(comment, item)
  576                 elif isinstance(ret["data"], dict):
  577                     for key, value in ret["data"].items():
  578                         comment = "{}\n\t\t{}: {}".format(comment, key, value)
  579                 else:
  580                     comment = "{} {}".format(comment, ret["data"])
  581             for detail in ["start_time", "duration"]:
  582                 ret.setdefault(detail, "")
  583             if ret["duration"] != "":
  584                 ret["duration"] = "{} ms".format(ret["duration"])
  585             svars = {
  586                 "tcolor": tcolor,
  587                 "comps": comps,
  588                 "ret": ret,
  589                 "comment": salt.utils.data.decode(comment),
  590                 # This nukes any trailing \n and indents the others.
  591                 "colors": colors,
  592             }
  593             hstrs.extend([sline.format(**svars) for sline in state_lines])
  594             changes = "     Changes:   " + ctext
  595             hstrs.append("{0}{1}{2[ENDC]}".format(tcolor, changes, colors))
  596 
  597             if "warnings" in ret:
  598                 rcounts.setdefault("warnings", 0)
  599                 rcounts["warnings"] += 1
  600                 wrapper = textwrap.TextWrapper(
  601                     width=80, initial_indent=" " * 14, subsequent_indent=" " * 14
  602                 )
  603                 hstrs.append(
  604                     "   {colors[LIGHT_RED]} Warnings: {0}{colors[ENDC]}".format(
  605                         wrapper.fill("\n".join(ret["warnings"])).lstrip(), colors=colors
  606                     )
  607                 )
  608 
  609         # Append result counts to end of output
  610         colorfmt = "{0}{1}{2[ENDC]}"
  611         rlabel = {
  612             True: "Succeeded",
  613             False: "Failed",
  614             None: "Not Run",
  615             "warnings": "Warnings",
  616         }
  617         count_max_len = max([len(str(x)) for x in rcounts.values()] or [0])
  618         label_max_len = max([len(x) for x in rlabel.values()] or [0])
  619         line_max_len = label_max_len + count_max_len + 2  # +2 for ': '
  620         hstrs.append(
  621             colorfmt.format(
  622                 colors["CYAN"],
  623                 "\nSummary for {}\n{}".format(host, "-" * line_max_len),
  624                 colors,
  625             )
  626         )
  627 
  628         def _counts(label, count):
  629             return "{0}: {1:>{2}}".format(label, count, line_max_len - (len(label) + 2))
  630 
  631         # Successful states
  632         changestats = []
  633         if None in rcounts and rcounts.get(None, 0) > 0:
  634             # test=True states
  635             changestats.append(
  636                 colorfmt.format(
  637                     colors["LIGHT_YELLOW"],
  638                     "unchanged={}".format(rcounts.get(None, 0)),
  639                     colors,
  640                 )
  641             )
  642         if nchanges > 0:
  643             changestats.append(
  644                 colorfmt.format(colors["GREEN"], "changed={}".format(nchanges), colors)
  645             )
  646         if changestats:
  647             changestats = " ({})".format(", ".join(changestats))
  648         else:
  649             changestats = ""
  650         hstrs.append(
  651             colorfmt.format(
  652                 colors["GREEN"],
  653                 _counts(rlabel[True], rcounts.get(True, 0) + rcounts.get(None, 0)),
  654                 colors,
  655             )
  656             + changestats
  657         )
  658 
  659         # Failed states
  660         num_failed = rcounts.get(False, 0)
  661         hstrs.append(
  662             colorfmt.format(
  663                 colors["RED"] if num_failed else colors["CYAN"],
  664                 _counts(rlabel[False], num_failed),
  665                 colors,
  666             )
  667         )
  668 
  669         if __opts__.get("state_output_pct", False):
  670             # Add success percentages to the summary output
  671             try:
  672                 success_pct = round(
  673                     (
  674                         (rcounts.get(True, 0) + rcounts.get(None, 0))
  675                         / (sum(rcounts.values()) - rcounts.get("warnings", 0))
  676                     )
  677                     * 100,
  678                     2,
  679                 )
  680 
  681                 hstrs.append(
  682                     colorfmt.format(
  683                         colors["GREEN"],
  684                         _counts("Success %", success_pct),
  685                         colors,
  686                     )
  687                 )
  688             except ZeroDivisionError:
  689                 pass
  690 
  691             # Add failure percentages to the summary output
  692             try:
  693                 failed_pct = round(
  694                     (num_failed / (sum(rcounts.values()) - rcounts.get("warnings", 0)))
  695                     * 100,
  696                     2,
  697                 )
  698 
  699                 hstrs.append(
  700                     colorfmt.format(
  701                         colors["RED"] if num_failed else colors["CYAN"],
  702                         _counts("Failure %", failed_pct),
  703                         colors,
  704                     )
  705                 )
  706             except ZeroDivisionError:
  707                 pass
  708 
  709         num_warnings = rcounts.get("warnings", 0)
  710         if num_warnings:
  711             hstrs.append(
  712                 colorfmt.format(
  713                     colors["LIGHT_RED"],
  714                     _counts(rlabel["warnings"], num_warnings),
  715                     colors,
  716                 )
  717             )
  718         totals = "{0}\nTotal states run: {1:>{2}}".format(
  719             "-" * line_max_len,
  720             sum(rcounts.values()) - rcounts.get("warnings", 0),
  721             line_max_len - 7,
  722         )
  723         hstrs.append(colorfmt.format(colors["CYAN"], totals, colors))
  724 
  725         if __opts__.get("state_output_profile"):
  726             sum_duration = sum(rdurations)
  727             if pdurations:
  728                 max_pduration = max(pdurations)
  729                 sum_duration = sum_duration + max_pduration
  730             duration_unit = "ms"
  731             # convert to seconds if duration is 1000ms or more
  732             if sum_duration > 999:
  733                 sum_duration /= 1000
  734                 duration_unit = "s"
  735             total_duration = "Total run time: {} {}".format(
  736                 "{:.3f}".format(sum_duration).rjust(line_max_len - 5), duration_unit
  737             )
  738             hstrs.append(colorfmt.format(colors["CYAN"], total_duration, colors))
  739 
  740     if strip_colors:
  741         host = salt.output.strip_esc_sequence(host)
  742     hstrs.insert(0, "{0}{1}:{2[ENDC]}".format(hcolor, host, colors))
  743     return "\n".join(hstrs), nchanges > 0
  744 
  745 
  746 def _nested_changes(changes):
  747     """
  748     Print the changes data using the nested outputter
  749     """
  750     ret = "\n"
  751     ret += salt.output.out_format(changes, "nested", __opts__, nested_indent=14)
  752     return ret
  753 
  754 
  755 def _format_changes(changes, orchestration=False):
  756     """
  757     Format the changes dict based on what the data is
  758     """
  759     if not changes:
  760         return False, ""
  761 
  762     if orchestration:
  763         return True, _nested_changes(changes)
  764 
  765     if not isinstance(changes, dict):
  766         return True, "Invalid Changes data: {}".format(changes)
  767 
  768     ret = changes.get("ret")
  769     if ret is not None and changes.get("out") == "highstate":
  770         ctext = ""
  771         changed = False
  772         for host, hostdata in ret.items():
  773             s, c = _format_host(host, hostdata)
  774             ctext += "\n" + "\n".join((" " * 14 + l) for l in s.splitlines())
  775             changed = changed or c
  776     else:
  777         changed = True
  778         ctext = _nested_changes(changes)
  779     return changed, ctext
  780 
  781 
  782 def _format_terse(tcolor, comps, ret, colors, tabular):
  783     """
  784     Terse formatting of a message.
  785     """
  786     result = "Clean"
  787     if ret["changes"]:
  788         result = "Changed"
  789     if ret["result"] is False:
  790         result = "Failed"
  791     elif ret["result"] is None:
  792         result = "Differs"
  793     if tabular is True:
  794         fmt_string = ""
  795         if "warnings" in ret:
  796             fmt_string += "{c[LIGHT_RED]}Warnings:\n{w}{c[ENDC]}\n".format(
  797                 c=colors, w="\n".join(ret["warnings"])
  798             )
  799         fmt_string += "{0}"
  800         if __opts__.get("state_output_profile") and "start_time" in ret:
  801             fmt_string += "{6[start_time]!s} [{6[duration]!s:>7} ms] "
  802         fmt_string += "{2:>10}.{3:<10} {4:7}   Name: {1}{5}"
  803     elif isinstance(tabular, str):
  804         fmt_string = tabular
  805     else:
  806         fmt_string = ""
  807         if "warnings" in ret:
  808             fmt_string += "{c[LIGHT_RED]}Warnings:\n{w}{c[ENDC]}".format(
  809                 c=colors, w="\n".join(ret["warnings"])
  810             )
  811         fmt_string += " {0} Name: {1} - Function: {2}.{3} - Result: {4}"
  812         if __opts__.get("state_output_profile") and "start_time" in ret:
  813             fmt_string += " - Started: {6[start_time]!s} - Duration: {6[duration]!s} ms"
  814         fmt_string += "{5}"
  815 
  816     msg = fmt_string.format(
  817         tcolor, comps[2], comps[0], comps[-1], result, colors["ENDC"], ret
  818     )
  819     return msg