"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