PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB` sh-3ll

HOME


sh-3ll 1.0
DIR:/lib/python3.6/site-packages/sos/collector/transports/
Upload File :
Current File : //lib/python3.6/site-packages/sos/collector/transports/__init__.py
# Copyright Red Hat 2021, Jake Hunsaker <jhunsake@redhat.com>

# This file is part of the sos project: https://github.com/sosreport/sos
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# version 2 of the GNU General Public License.
#
# See the LICENSE file in the source distribution for further information.

import inspect
import logging
import re
from shlex import quote

import pexpect

from sos.collector.exceptions import (ConnectionException,
                                      CommandTimeoutException)
from sos.utilities import bold


class RemoteTransport():
    """The base class used for defining supported remote transports to connect
    to remote nodes in conjunction with `sos collect`.

    This abstraction is used to manage the backend connections to nodes so that
    SoSNode() objects can be leveraged generically to connect to nodes, inspect
    those nodes, and run commands on them.
    """

    name = 'undefined'
    default_user = None

    def __init__(self, address, commons):
        self.address = address
        self.opts = commons['cmdlineopts']
        self.tmpdir = commons['tmpdir']
        self.need_sudo = commons['need_sudo']
        self._hostname = None
        self.soslog = logging.getLogger('sos')
        self.ui_log = logging.getLogger('sos_ui')

    def _sanitize_log_msg(self, msg):
        """Attempts to obfuscate sensitive information in log messages such as
        passwords"""
        reg = r'(?P<var>(pass|key|secret|PASS|KEY|SECRET).*?=)(?P<value>.*?\s)'
        return re.sub(reg, r'\g<var>****** ', msg)

    def log_info(self, msg):
        """Used to print and log info messages"""
        caller = inspect.stack()[1][3]
        lmsg = f'[{self.hostname}:{caller}] {msg}'
        self.soslog.info(lmsg)

    def log_error(self, msg):
        """Used to print and log error messages"""
        caller = inspect.stack()[1][3]
        lmsg = f'[{self.hostname}:{caller}] {msg}'
        self.soslog.error(lmsg)

    def log_debug(self, msg):
        """Used to print and log debug messages"""
        msg = self._sanitize_log_msg(msg)
        caller = inspect.stack()[1][3]
        msg = f'[{self.hostname}:{caller}] {msg}'
        self.soslog.debug(msg)

    @property
    def hostname(self):
        if self._hostname and 'localhost' not in self._hostname:
            return self._hostname
        return self.address

    @property
    def connected(self):
        """Is the transport __currently__ connected to the node, or otherwise
        capable of seamlessly running a command or similar on the node?
        """
        return False

    @property
    def remote_exec(self):
        """This is the command string needed to leverage the remote transport
        when executing commands. For example, for an SSH transport this would
        be the `ssh <options>` string prepended to any command so that the
        command is executed by the ssh binary.

        This is also referenced by the `remote_exec` parameter for policies
        when loading a policy for a remote node
        """
        return None

    @classmethod
    def display_help(cls, section):
        if cls is RemoteTransport:
            return cls.display_self_help(section)
        section.set_title(f"{cls.name.title().replace('_', ' ')} "
                          "Transport Detailed Help")
        if cls.__doc__ and cls.__doc__ is not RemoteTransport.__doc__:
            section.add_text(cls.__doc__)
        else:
            section.add_text(
                'Detailed information not available for this transport'
            )
        return None

    @classmethod
    def display_self_help(cls, section):
        section.set_title('SoS Remote Transport Help')
        section.add_text(
            "\nTransports define how SoS connects to nodes and executes "
            f"commands on them for the purposes of an {bold('sos collect')} "
            "run. Generally, this means transports define how commands are "
            "wrapped locally so that they are executed on the remote node(s) "
            "instead."
        )

        section.add_text(
            "Transports are generally selected by the cluster profile loaded "
            "for a given execution, however users may explicitly set one "
            f"using '{bold('--transport=$transport_name')}'. Note that not all"
            " transports will function for all cluster/node types."
        )

        section.add_text(
            'By default, OpenSSH Control Persist is attempted. Additional '
            'information for each supported transport is available in the '
            'following help sections:\n'
        )

        from sos.collector.sosnode import TRANSPORTS
        for transport in TRANSPORTS:
            _sec = bold(f"collect.transports.{transport}")
            _desc = f"The '{transport.lower()}' transport"
            section.add_text(
                f"{' ':>8}{_sec:<45}{_desc:<30}",
                newline=False
            )

    def connect(self, password):
        """Perform the connection steps in order to ensure that we are able to
        connect to the node for all future operations. Note that this should
        not provide an interactive shell at this time.
        """
        if self._connect(password):
            if not self._hostname:
                self._get_hostname()
            return True
        return False

    def _connect(self, password):
        """Actually perform the connection requirements. Should be overridden
        by specific transports that subclass RemoteTransport
        """
        raise NotImplementedError(
            f"Transport {self.name} does not define connect")

    def reconnect(self, password):
        """Attempts to reconnect to the node using the standard connect()
        but does not do so indefinitely. This imposes a strict number of retry
        attempts before failing out
        """
        attempts = 1
        last_err = 'unknown'
        while attempts < 5:
            self.log_debug(f"Attempting reconnect (#{attempts}) to node")
            try:
                if self.connect(password):
                    return True
            except Exception as err:
                self.log_debug(f"Attempt #{attempts} exception: {err}")
                last_err = err
            attempts += 1
        self.log_error("Unable to reconnect to node after 5 attempts, "
                       "aborting.")
        raise ConnectionException(f"last exception from transport: {last_err}")

    def disconnect(self):
        """Perform whatever steps are necessary, if any, to terminate any
        connection to the node
        """
        try:
            if self._disconnect():
                self.log_debug("Successfully disconnected from node")
            else:
                self.log_error("Unable to successfully disconnect, see log for"
                               " more details")
        except Exception as err:
            self.log_error(f"Failed to disconnect: {err}")

    def _disconnect(self):
        raise NotImplementedError(
            f"Transport {self.name} does not define disconnect")

    @property
    def _need_shell(self):
        """
        Transports may override this to control when/if commands executed over
        the transport needs to utilize a shell on the remote host.
        """
        return False

    def run_command(self, cmd, timeout=180, need_root=False, env=None,
                    use_shell='auto'):
        """Run a command on the node, returning its output and exit code.
        This should return the exit code of the command being executed, not the
        exit code of whatever mechanism the transport uses to execute that
        command

        :param cmd:         The command to run
        :type cmd:          ``str``

        :param timeout:     The maximum time in seconds to allow the cmd to run
        :type timeout:      ``int```

        :param need_root:   Does ``cmd`` require root privileges?
        :type need_root:   ``bool``

        :param env:         Specify env vars to be passed to the ``cmd``
        :type env:          ``dict``

        :param use_shell:     Does ``cmd`` require execution within a shell?
        :type use_shell:      ``bool`` or ``auto`` for transport-determined

        :returns:           Output of ``cmd`` and the exit code
        :rtype:             ``dict`` with keys ``output`` and ``status``
        """
        self.log_debug(f'Running command {cmd}')
        if (use_shell is True or
                (self._need_shell if use_shell == 'auto' else False)):
            cmd = f"/bin/bash -c {quote(cmd)}"
            self.log_debug(f"Shell requested, command is now {cmd}")
        # currently we only use/support the use of pexpect for handling the
        # execution of these commands, as opposed to directly invoking
        # subprocess.Popen() in conjunction with tools like sshpass.
        # If that changes in the future, we'll add decision making logic here
        # to route to the appropriate handler, but for now we just go straight
        # to using pexpect
        return self._run_command_with_pexpect(cmd, timeout, need_root, env)

    def _format_cmd_for_exec(self, cmd):
        """Format the command in the way needed for the remote transport to
        successfully execute it as one would when manually executing it

        :param cmd:     The command being executed, as formatted by SoSNode
        :type cmd:      ``str``


        :returns:       The command further formatted as needed by this
                        transport
        :rtype:         ``str``
        """
        cmd = f"{self.remote_exec} {quote(cmd)}"
        cmd = cmd.lstrip()
        return cmd

    def _run_command_with_pexpect(self, cmd, timeout, need_root, env):
        """Execute the command using pexpect, which allows us to more easily
        handle prompts and timeouts compared to directly leveraging the
        subprocess.Popen() method.

        :param cmd:     The command to execute. This will be automatically
                        formatted to use the transport.
        :type cmd:      ``str``

        :param timeout: The maximum time in seconds to run ``cmd``
        :type timeout:  ``int``

        :param need_root:   Does ``cmd`` need to run as root or with sudo?
        :type need_root:    ``bool``

        :param env:     Any env vars that ``cmd`` should be run with
        :type env:      ``dict``
        """
        cmd = self._format_cmd_for_exec(cmd)

        # if for any reason env is empty, set it to None as otherwise
        # pexpect interprets this to mean "run this command with no env vars of
        # any kind"
        if not env:
            env = None

        try:
            result = pexpect.spawn(cmd, encoding='utf-8', env=env)
        except pexpect.exceptions.ExceptionPexpect as err:
            self.log_debug(err.value)
            return {'status': 127, 'output': ''}

        _expects = [pexpect.EOF, pexpect.TIMEOUT]
        if need_root and self.opts.ssh_user != 'root':
            _expects.extend([
                '\\[sudo\\] password for .*:',
                'Password:'
            ])

        index = result.expect(_expects, timeout=timeout)

        if index in [2, 3]:
            self._send_pexpect_password(index, result)
            index = result.expect(_expects, timeout=timeout)

        if index == 0:
            out = result.before
            result.close()
            return {'status': result.exitstatus, 'output': out}
        if index == 1:
            raise CommandTimeoutException(cmd)
        # if we somehow manage to flow to this point, use this bogus exit code
        # as a signal to debugging efforts that whatever went sideways did so
        # as part of the above block
        self.log_debug(f"Unexpected index {index} from pexpect: {result}")
        return {'status': 999, 'output': ''}

    def _send_pexpect_password(self, index, result):
        """Handle password prompts for sudo and su usage for non-root SSH users

        :param index:       The index pexpect.spawn returned to match against
                            either a sudo or su prompt
        :type index:        ``int``

        :param result:      The spawn running the command
        :type result:       ``pexpect.spawn``
        """
        if index == 2:
            if not self.opts.sudo_pw and not self.opts.nopasswd_sudo:
                msg = ("Unable to run command: sudo password "
                       "required but not provided")
                self.log_error(msg)
                raise Exception(msg)
            result.sendline(self.opts.sudo_pw)
        elif index == 3:
            if not self.opts.root_password:
                msg = "Unable to run command as root: no root password given"
                self.log_error(msg)
                raise Exception(msg)
            result.sendline(self.opts.root_password)

    def _get_hostname(self):
        """Determine the hostname of the node and set that for future reference
        and logging

        :returns:   The hostname of the system, per the `hostname` command
        :rtype:     ``str``
        """
        _out = self.run_command('hostname')
        if _out['status'] == 0:
            self._hostname = _out['output'].strip()

        if not self._hostname:
            self._hostname = self.address
        self.log_info(f"Hostname set to {self._hostname}")
        return self._hostname

    def copy_file_to_remote(self, fname, dest):
        """Copy a local file, fname, to dest on the remote node

        :param fname:   The name of the file to copy
        :type fname:    ``str``

        :param dest:    Where to save the file to remotely
        :type dest:     ``str``

        :returns:   True if file was successfully copied to remote, or False
        :rtype:     ``bool``
        """
        attempts = 0
        try:
            while attempts < 3:
                attempts += 1
                ret = self._copy_file_to_remote(fname, dest)
                if ret:
                    return True
                self.log_info(f"File copy attempt {attempts} failed")
            self.log_info("File copy failed after 3 attempts")
            return False
        except Exception as err:
            self.log_error("Exception encountered during config copy attempt "
                           f"{attempts} for {fname}: {err}")
            raise err

    def _copy_file_to_remote(self, fname, dest):
        raise NotImplementedError(
            f"Transport {self.name} does not support file copying")

    def retrieve_file(self, fname, dest):
        """Copy a remote file, fname, to dest on the local node

        :param fname:   The name of the file to retrieve
        :type fname:    ``str``

        :param dest:    Where to save the file to locally
        :type dest:     ``str``

        :returns:   True if file was successfully copied from remote, or False
        :rtype:     ``bool``
        """
        attempts = 0
        try:
            while attempts < 5:
                attempts += 1
                ret = self._retrieve_file(fname, dest)
                if ret:
                    return True
                self.log_info(f"File retrieval attempt {attempts} failed")
            self.log_info("File retrieval failed after 5 attempts")
            return False
        except Exception as err:
            self.log_error("Exception encountered during retrieval attempt "
                           f"{attempts} for {fname}: {err}")
            raise err

    def _retrieve_file(self, fname, dest):
        raise NotImplementedError(
            f"Transport {self.name} does not support file copying")

    def read_file(self, fname):
        """Read the given file fname and return its contents

        :param fname:   The name of the file to read
        :type fname:    ``str``

        :returns:   The content of the file
        :rtype:     ``str``
        """
        self.log_debug(f"Reading file {fname}")
        return self._read_file(fname)

    def _read_file(self, fname):
        res = self.run_command(f"cat {fname}", timeout=10)
        if res['status'] == 0:
            return res['output']
        if 'No such file' in res['output']:
            self.log_debug(f"File {fname} does not exist on node")
        else:
            self.log_error(f"Error reading {fname}: "
                           f"{res['output'].split(':')[1:]}")
        return ''

# vim: set et ts=4 sw=4 :