PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/saltstack/salt/lib/python3.10/site-packages/salt/output
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/output/highstate.py
""" Outputter for displaying results of state runs ============================================== The return data from the Highstate command is a standard data structure which is parsed by the highstate outputter to deliver a clean and readable set of information about the HighState run on minions. Two configurations can be set to modify the highstate outputter. These values can be set in the master config to change the output of the ``salt`` command or set in the minion config to change the output of the ``salt-call`` command. state_verbose: By default `state_verbose` is set to `True`, setting this to `False` will instruct the highstate outputter to omit displaying anything in green, this means that nothing with a result of True and no changes will not be printed state_output: The highstate outputter has six output modes, ``full``, ``terse``, ``mixed``, ``changes`` and ``filter`` * The default is set to ``full``, which will display many lines of detailed information for each executed chunk. * If ``terse`` is used, then the output is greatly simplified and shown in only one line. * If ``mixed`` is used, then terse output will be used unless a state failed, in which case full output will be used. * If ``changes`` is used, then terse output will be used if there was no error and no changes, otherwise full output will be used. * If ``filter`` is used, then either or both of two different filters can be used: ``exclude`` or ``terse``. * for ``exclude``, state.highstate expects a list of states to be excluded (or ``None``) followed by ``True`` for terse output or ``False`` for regular output. Because of parsing nuances, if only one of these is used, it must still contain a comma. For instance: `exclude=True,`. * for ``terse``, state.highstate expects simply ``True`` or ``False``. These can be set as such from the command line, or in the Salt config as `state_output_exclude` or `state_output_terse`, respectively. The output modes have one modifier: ``full_id``, ``terse_id``, ``mixed_id``, ``changes_id`` and ``filter_id`` If ``_id`` is used, then the corresponding form will be used, but the value for ``name`` will be drawn from the state ID. This is useful for cases where the name value might be very long and hard to read. state_tabular: If `state_output` uses the terse output, set this to `True` for an aligned output format. If you wish to use a custom format, this can be set to a string. state_output_pct: Set `state_output_pct` to `True` in order to add "Success %" and "Failure %" to the "Summary" section at the end of the highstate output. state_compress_ids: Set `state_compress_ids` to `True` to aggregate information about states which have multiple "names" under the same state ID in the highstate output. This is useful in combination with the `terse_id` value set in the `state_output` option when states are using the `names` state parameter. Example usage: If ``state_output: filter`` is set in the configuration file: .. code-block:: bash salt '*' state.highstate exclude=None,True means to exclude no states from the highstate and turn on terse output. .. code-block:: bash salt twd state.highstate exclude=problemstate1,problemstate2,False means to exclude states ``problemstate1`` and ``problemstate2`` from the highstate, and use regular output. Example output for the above highstate call when ``top.sls`` defines only one other state to apply to minion ``twd``: .. code-block:: text twd: Summary for twd ------------ Succeeded: 1 (changed=1) Failed: 0 ------------ Total states run: 1 Example output with no special settings in configuration files: .. code-block:: text myminion: ---------- ID: test.ping Function: module.run Result: True Comment: Module function test.ping executed Changes: ---------- ret: True Summary for myminion ------------ Succeeded: 1 Failed: 0 ------------ Total: 0 """ import collections import logging import pprint import re import textwrap import salt.output import salt.utils.color import salt.utils.data import salt.utils.stringutils log = logging.getLogger(__name__) def _compress_ids(data): """ Function to take incoming raw state data and roll IDs with multiple names into a single state block for reporting purposes. This functionality is most useful for any "_id" state_output options, such as ``terse_id``. The following example state has one ID and four names. .. code-block:: yaml mix-matched results: cmd.run: - names: - "true" - "false" - "/bin/true" - "/bin/false" With ``state_output: terse_id`` set, this can create many lines of output which are not unique enough to be worth the screen real estate they occupy. .. code-block:: text 19:10:10.969049 [ 8.546 ms] cmd.run Changed Name: mix-matched results 19:10:10.977998 [ 8.606 ms] cmd.run Failed Name: mix-matched results 19:10:10.987116 [ 7.618 ms] cmd.run Changed Name: mix-matched results 19:10:10.995172 [ 9.344 ms] cmd.run Failed Name: mix-matched results Enabling ``state_compress_ids: True`` consolidates the state data by ID and result (e.g. success or failure). The earliest start time is chosen for display, duration is aggregated, and the total number of names if shown in parentheses to the right of the ID. .. code-block:: text 19:10:46.283323 [ 16.236 ms] cmd.run Changed Name: mix-matched results (2) 19:10:46.292181 [ 16.255 ms] cmd.run Failed Name: mix-matched results (2) A better real world use case would be passing dozens of files and directories to the ``names`` parameter of the ``file.absent`` state. The amount of lines consolidated in that case would be substantial. """ if not isinstance(data, dict): return data compressed = {} # any failures to compress result in passing the original data # to the highstate outputter without modification try: for host, hostdata in data.items(): compressed[host] = {} # count the number of unique IDs. use sls name and result in the key # so differences can be shown separately in the output id_count = collections.Counter( [ "_".join( map( str, [ tname.split("_|-")[0], info["__id__"], info["__sls__"], info["result"], ], ) ) for tname, info in hostdata.items() ] ) for tname, info in hostdata.items(): comps = tname.split("_|-") _id = "_".join( map( str, [comps[0], info["__id__"], info["__sls__"], info["result"]] ) ) # state does not need to be compressed if id_count[_id] == 1: compressed[host][tname] = info continue # replace name to create a single key by sls and result comps[2] = "_".join( map( str, [ "state_compressed", info["__sls__"], info["__id__"], info["result"], ], ) ) comps[1] = "{} ({})".format(info["__id__"], id_count[_id]) tname = "_|-".join(comps) # store the first entry as-is if tname not in compressed[host]: compressed[host][tname] = info continue # subsequent entries for compression will use the lowest # __run_num__ value, the sum of the duration, and the earliest # start time found compressed[host][tname]["__run_num__"] = min( info["__run_num__"], compressed[host][tname]["__run_num__"] ) compressed[host][tname]["duration"] = round( sum([info["duration"], compressed[host][tname]["duration"]]), 3 ) compressed[host][tname]["start_time"] = sorted( [info["start_time"], compressed[host][tname]["start_time"]] )[0] # changes are turned into a dict of changes keyed by name if compressed[host][tname].get("changes") and info.get("changes"): if not compressed[host][tname]["changes"].get("compressed changes"): compressed[host][tname]["changes"] = { "compressed changes": { compressed[host][tname]["name"]: compressed[host][ tname ]["changes"] } } compressed[host][tname]["changes"]["compressed changes"].update( {info["name"]: info["changes"]} ) elif info.get("changes"): compressed[host][tname]["changes"] = { "compressed changes": {info["name"]: info["changes"]} } except Exception: # pylint: disable=broad-except log.warning("Unable to compress state output by ID! Returning output normally.") return data return compressed def output(data, **kwargs): # pylint: disable=unused-argument """ The HighState Outputter is only meant to be used with the state.highstate function, or a function that returns highstate return data. """ # If additional information is passed through via the "data" dictionary to # the highstate outputter, such as "outputter" or "retcode", discard it. # We only want the state data that was passed through, if it is wrapped up # in the "data" key, as the orchestrate runner does. See Issue #31330, # pull request #27838, and pull request #27175 for more information. # account for envelope data if being passed lookup_jid ret if isinstance(data, dict) and "return" in data: data = data["return"] if isinstance(data, dict) and "data" in data: data = data["data"] # account for envelope data if being passed lookup_jid ret if isinstance(data, dict) and len(data.keys()) == 1: _data = next(iter(data.values())) if isinstance(_data, dict): if "jid" in _data and "fun" in _data: data = _data.get("return", {}).get("data", data) # output() is recursive, if we aren't passed a dict just return it if isinstance(data, int) or isinstance(data, str): return data if data is None: return "None" # Discard retcode in dictionary as present in orchestrate data local_masters = [key for key in data.keys() if key.endswith("_master")] orchestrator_output = "retcode" in data.keys() and len(local_masters) == 1 if orchestrator_output: del data["retcode"] # pre-process data if state_compress_ids is set if __opts__.get("state_compress_ids", False): data = _compress_ids(data) indent_level = kwargs.get("indent_level", 1) ret = [ _format_host(host, hostdata, indent_level=indent_level)[0] for host, hostdata in data.items() ] if ret: return "\n".join(ret) log.error( "Data passed to highstate outputter is not a valid highstate return: %s", data ) # We should not reach here, but if we do return empty string return "" 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 = [] pdurations = [] 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 if "__parallel__" in ret: pduration = ret.get("duration", 0) try: pdurations.append(float(pduration)) except ValueError: pduration, _, _ = pduration.partition(" ms") try: pdurations.append(float(pduration)) except ValueError: log.error( "Cannot parse a float from duration %s", ret.get("duration", 0), ) else: 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 = f"{comment} {item}" elif isinstance(ret["data"], dict): for key, value in ret["data"].items(): comment = f"{comment}\n\t\t{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"], f"unchanged={rcounts.get(None, 0)}", colors, ) ) if nchanges > 0: changestats.append( colorfmt.format(colors["GREEN"], f"changed={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) if pdurations: max_pduration = max(pdurations) sum_duration = sum_duration + max_pduration 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( f"{sum_duration:.3f}".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 _nested_changes(changes): """ Print the changes data using the nested outputter """ ret = "\n" ret += salt.output.out_format(changes, "nested", __opts__, nested_indent=14) return ret def _format_changes(changes, orchestration=False): """ Format the changes dict based on what the data is """ if not changes: return False, "" if orchestration: return True, _nested_changes(changes) if not isinstance(changes, dict): return True, f"Invalid Changes data: {changes}" ret = changes.get("ret") if ret is not None and changes.get("out") == "highstate": ctext = "" changed = False for host, hostdata in ret.items(): s, c = _format_host(host, hostdata) ctext += "\n" + "\n".join((" " * 14 + l) for l in s.splitlines()) changed = changed or c else: changed = True ctext = _nested_changes(changes) return changed, ctext def _format_terse(tcolor, comps, ret, colors, tabular): """ Terse formatting of a message. """ result = "Clean" if ret["changes"]: result = "Changed" if ret["result"] is False: result = "Failed" elif ret["result"] is None: result = "Differs" if tabular is True: fmt_string = "" if "warnings" in ret: fmt_string += "{c[LIGHT_RED]}Warnings:\n{w}{c[ENDC]}\n".format( c=colors, w="\n".join(ret["warnings"]) ) fmt_string += "{0}" if __opts__.get("state_output_profile") and "start_time" in ret: fmt_string += "{6[start_time]!s} [{6[duration]!s:>7} ms] " fmt_string += "{2:>10}.{3:<10} {4:7} Name: {1}{5}" elif isinstance(tabular, str): fmt_string = tabular else: fmt_string = "" if "warnings" in ret: fmt_string += "{c[LIGHT_RED]}Warnings:\n{w}{c[ENDC]}".format( c=colors, w="\n".join(ret["warnings"]) ) fmt_string += " {0} Name: {1} - Function: {2}.{3} - Result: {4}" if __opts__.get("state_output_profile") and "start_time" in ret: fmt_string += " - Started: {6[start_time]!s} - Duration: {6[duration]!s} ms" fmt_string += "{5}" msg = fmt_string.format( tcolor, comps[2], comps[0], comps[-1], result, colors["ENDC"], ret ) return msg