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/imh-python/lib/python3.9/site-packages/libcloud/compute
Viewing File: /opt/imh-python/lib/python3.9/site-packages/libcloud/compute/ssh.py
# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Wraps multiple ways to communicate over SSH. """ from typing import Type from typing import Optional from typing import Tuple from typing import List from typing import Union from typing import cast have_paramiko = False try: import paramiko have_paramiko = True except ImportError: pass # Depending on your version of Paramiko, it may cause a deprecation # warning on Python 2.6. # Ref: https://bugs.launchpad.net/paramiko/+bug/392973 import os import time import subprocess import logging import warnings from os.path import split as psplit from os.path import join as pjoin from libcloud.utils.logging import ExtraLogFormatter from libcloud.utils.py3 import StringIO from libcloud.utils.py3 import b __all__ = [ 'BaseSSHClient', 'ParamikoSSHClient', 'ShellOutSSHClient', 'SSHCommandTimeoutError' ] SUPPORTED_KEY_TYPES_URL = 'https://libcloud.readthedocs.io/en/latest/compute/deployment.html#supported-private-ssh-key-types' # NOQA class SSHCommandTimeoutError(Exception): """ Exception which is raised when an SSH command times out. """ def __init__(self, cmd, timeout, stdout=None, stderr=None): # type: (str, float, Optional[str], Optional[str]) -> None self.cmd = cmd self.timeout = timeout self.stdout = stdout self.stderr = stderr self.message = 'Command didn\'t finish in %s seconds' % (timeout) super(SSHCommandTimeoutError, self).__init__(self.message) def __repr__(self): return ('<SSHCommandTimeoutError: cmd="%s",timeout=%s)>' % (self.cmd, self.timeout)) def __str__(self): return self.__repr__() class BaseSSHClient(object): """ Base class representing a connection over SSH/SCP to a remote node. """ def __init__(self, hostname, # type: str port=22, # type: int username='root', # type: str password=None, # type: Optional[str] key=None, # type: Optional[str] key_files=None, # type: Optional[Union[str, List[str]]] timeout=None # type: Optional[float] ): """ :type hostname: ``str`` :keyword hostname: Hostname or IP address to connect to. :type port: ``int`` :keyword port: TCP port to communicate on, defaults to 22. :type username: ``str`` :keyword username: Username to use, defaults to root. :type password: ``str`` :keyword password: Password to authenticate with or a password used to unlock a private key if a password protected key is used. :param key: Deprecated in favor of ``key_files`` argument. :type key_files: ``str`` or ``list`` :keyword key_files: A list of paths to the private key files to use. """ if key is not None: message = ('You are using deprecated "key" argument which has ' 'been replaced with "key_files" argument') warnings.warn(message, DeprecationWarning) # key_files has precedent key_files = key if not key_files else key_files self.hostname = hostname self.port = port self.username = username self.password = password self.key_files = key_files self.timeout = timeout def connect(self): # type: () -> bool """ Connect to the remote node over SSH. :return: True if the connection has been successfully established, False otherwise. :rtype: ``bool`` """ raise NotImplementedError( 'connect not implemented for this ssh client') def put(self, path, contents=None, chmod=None, mode='w'): # type: (str, Optional[Union[str, bytes]], Optional[int], str) -> str """ Upload a file to the remote node. :type path: ``str`` :keyword path: File path on the remote node. :type contents: ``str`` :keyword contents: File Contents. :type chmod: ``int`` :keyword chmod: chmod file to this after creation. :type mode: ``str`` :keyword mode: Mode in which the file is opened. :return: Full path to the location where a file has been saved. :rtype: ``str`` """ raise NotImplementedError( 'put not implemented for this ssh client') def delete(self, path): # type: (str) -> bool """ Delete/Unlink a file on the remote node. :type path: ``str`` :keyword path: File path on the remote node. :return: True if the file has been successfully deleted, False otherwise. :rtype: ``bool`` """ raise NotImplementedError( 'delete not implemented for this ssh client') def run(self, cmd, timeout=None): # type: (str, Optional[float]) -> Tuple[str, str, int] """ Run a command on a remote node. :type cmd: ``str`` :keyword cmd: Command to run. :return ``list`` of [stdout, stderr, exit_status] """ raise NotImplementedError( 'run not implemented for this ssh client') def close(self): # type: () -> bool """ Shutdown connection to the remote node. :return: True if the connection has been successfully closed, False otherwise. :rtype: ``bool`` """ raise NotImplementedError( 'close not implemented for this ssh client') def _get_and_setup_logger(self): # type: () -> logging.Logger logger = logging.getLogger('libcloud.compute.ssh') path = os.getenv('LIBCLOUD_DEBUG') if path: handler = logging.FileHandler(path) handler.setFormatter(ExtraLogFormatter()) logger.addHandler(handler) logger.setLevel(logging.DEBUG) return logger class ParamikoSSHClient(BaseSSHClient): """ A SSH Client powered by Paramiko. """ # Maximum number of bytes to read at once from a socket CHUNK_SIZE = 4096 # How long to sleep while waiting for command to finish (to prevent busy # waiting) SLEEP_DELAY = 0.2 def __init__(self, hostname, # type: str port=22, # type: int username='root', # type: str password=None, # type: Optional[str] key=None, # type: Optional[str] key_files=None, # type: Optional[Union[str, List[str]]] key_material=None, # type: Optional[str] timeout=None # type: Optional[float] ): """ Authentication is always attempted in the following order: - The key passed in (if key is provided) - Any key we can find through an SSH agent (only if no password and key is provided) - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (only if no password and key is provided) - Plain username/password auth, if a password was given (if password is provided) """ if key_files and key_material: raise ValueError(('key_files and key_material arguments are ' 'mutually exclusive')) super(ParamikoSSHClient, self).__init__(hostname=hostname, port=port, username=username, password=password, key=key, key_files=key_files, timeout=timeout) self.key_material = key_material self.client = paramiko.SSHClient() self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.logger = self._get_and_setup_logger() def connect(self): conninfo = {'hostname': self.hostname, 'port': self.port, 'username': self.username, 'allow_agent': False, 'look_for_keys': False} if self.password: conninfo['password'] = self.password if self.key_files: conninfo['key_filename'] = self.key_files if self.key_material: conninfo['pkey'] = self._get_pkey_object( key=self.key_material, password=self.password) if not self.password and not (self.key_files or self.key_material): conninfo['allow_agent'] = True conninfo['look_for_keys'] = True if self.timeout: conninfo['timeout'] = self.timeout # This is a workaround for paramiko only supporting key files in # format staring with "BEGIN RSA PRIVATE KEY". # If key_files are provided and a key looks like a PEM formatted key # we try to convert it into a format supported by paramiko if (self.key_files and not isinstance(self.key_files, (list, tuple)) and os.path.isfile(self.key_files)): with open(self.key_files, 'r') as fp: key_material = fp.read() try: pkey = self._get_pkey_object(key=key_material, password=self.password) except paramiko.ssh_exception.PasswordRequiredException as e: raise e except Exception: pass else: # It appears key is valid, but it was passed in in an invalid # format. Try to use the converted key directly del conninfo['key_filename'] conninfo['pkey'] = pkey extra = {'_hostname': self.hostname, '_port': self.port, '_username': self.username, '_timeout': self.timeout} if self.password: extra['_auth_method'] = 'password' else: extra['_auth_method'] = 'key_file' if self.key_files: extra['_key_file'] = self.key_files self.logger.debug('Connecting to server', extra=extra) self.client.connect(**conninfo) return True def put(self, path, contents=None, chmod=None, mode='w'): extra = {'_path': path, '_mode': mode, '_chmod': chmod} self.logger.debug('Uploading file', extra=extra) sftp = self.client.open_sftp() # less than ideal, but we need to mkdir stuff otherwise file() fails head, tail = psplit(path) if path[0] == "/": sftp.chdir("/") else: # Relative path - start from a home directory (~) sftp.chdir('.') for part in head.split("/"): if part != "": try: sftp.mkdir(part) except IOError: # so, there doesn't seem to be a way to # catch EEXIST consistently *sigh* pass sftp.chdir(part) cwd = sftp.getcwd() ak = sftp.file(tail, mode=mode) ak.write(contents) if chmod is not None: ak.chmod(chmod) ak.close() sftp.close() if path[0] == '/': file_path = path else: file_path = pjoin(cwd, path) return file_path def delete(self, path): extra = {'_path': path} self.logger.debug('Deleting file', extra=extra) sftp = self.client.open_sftp() sftp.unlink(path) sftp.close() return True def run(self, cmd, timeout=None): # type: (str, Optional[float]) -> Tuple[str, str, int] """ Note: This function is based on paramiko's exec_command() method. :param timeout: How long to wait (in seconds) for the command to finish (optional). :type timeout: ``float`` """ extra1 = {'_cmd': cmd} self.logger.debug('Executing command', extra=extra1) # Use the system default buffer size bufsize = -1 transport = self.client.get_transport() chan = transport.open_session() start_time = time.time() chan.exec_command(cmd) stdout = StringIO() stderr = StringIO() # Create a stdin file and immediately close it to prevent any # interactive script from hanging the process. stdin = chan.makefile('wb', bufsize) stdin.close() # Receive all the output # Note #1: This is used instead of chan.makefile approach to prevent # buffering issues and hanging if the executed command produces a lot # of output. # # Note #2: If you are going to remove "ready" checks inside the loop # you are going to have a bad time. Trying to consume from a channel # which is not ready will block for indefinitely. exit_status_ready = chan.exit_status_ready() if exit_status_ready: # It's possible that some data is already available when exit # status is ready stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) while not exit_status_ready: current_time = time.time() elapsed_time = (current_time - start_time) if timeout and (elapsed_time > timeout): # TODO: Is this the right way to clean up? chan.close() stdout_str = stdout.getvalue() # type: str stderr_str = stderr.getvalue() # type: str raise SSHCommandTimeoutError(cmd=cmd, timeout=timeout, stdout=stdout_str, stderr=stderr_str) stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) # We need to check the exist status here, because the command could # print some output and exit during this sleep below. exit_status_ready = chan.exit_status_ready() if exit_status_ready: break # Short sleep to prevent busy waiting time.sleep(self.SLEEP_DELAY) # Receive the exit status code of the command we ran. status = chan.recv_exit_status() # type: int stdout_str = stdout.getvalue() stderr_str = stderr.getvalue() extra2 = {'_status': status, '_stdout': stdout_str, '_stderr': stderr_str} self.logger.debug('Command finished', extra=extra2) result = (stdout_str, stderr_str, status) # type: Tuple[str, str, int] return result def close(self): self.logger.debug('Closing server connection') self.client.close() return True def _consume_stdout(self, chan): """ Try to consume stdout data from chan if it's receive ready. """ stdout = self._consume_data_from_channel( chan=chan, recv_method=chan.recv, recv_ready_method=chan.recv_ready) return stdout def _consume_stderr(self, chan): """ Try to consume stderr data from chan if it's receive ready. """ stderr = self._consume_data_from_channel( chan=chan, recv_method=chan.recv_stderr, recv_ready_method=chan.recv_stderr_ready) return stderr def _consume_data_from_channel(self, chan, recv_method, recv_ready_method): """ Try to consume data from the provided channel. Keep in mind that data is only consumed if the channel is receive ready. """ result = StringIO() result_bytes = bytearray() if recv_ready_method(): data = recv_method(self.CHUNK_SIZE) result_bytes += b(data) while data: ready = recv_ready_method() if not ready: break data = recv_method(self.CHUNK_SIZE) result_bytes += b(data) # We only decode data at the end because a single chunk could contain # a part of multi byte UTF-8 character (whole multi bytes character # could be split over two chunks) result.write(result_bytes.decode('utf-8', errors='ignore')) return result def _get_pkey_object(self, key, password=None): """ Try to detect private key type and return paramiko.PKey object. # NOTE: Paramiko only supports key in PKCS#1 PEM format. """ key_types = [ (paramiko.RSAKey, 'RSA'), (paramiko.DSSKey, 'DSA'), (paramiko.ECDSAKey, 'EC') ] paramiko_version = getattr(paramiko, '__version__', '0.0.0') paramiko_version = tuple([int(c) for c in paramiko_version.split('.')]) if paramiko_version >= (2, 2, 0): # Ed25519 is only supported in paramiko >= 2.2.0 key_types.append((paramiko.ed25519key.Ed25519Key, 'Ed25519')) for cls, key_type in key_types: # Work around for paramiko not recognizing keys which start with # "----BEGIN PRIVATE KEY-----" # Since key is already in PEM format, we just try changing the # header and footer key_split = key.strip().splitlines() if (key_split[0] == '-----BEGIN PRIVATE KEY-----' and key_split[-1] == '-----END PRIVATE KEY-----'): key_split[0] = '-----BEGIN %s PRIVATE KEY-----' % (key_type) key_split[-1] = '-----END %s PRIVATE KEY-----' % (key_type) key_value = '\n'.join(key_split) else: # Already a valid key, us it as is key_value = key try: key = cls.from_private_key(StringIO(key_value), password) except paramiko.ssh_exception.PasswordRequiredException as e: raise e except (paramiko.ssh_exception.SSHException, AssertionError) as e: if 'private key file checkints do not match' in str(e).lower(): msg = ('Invalid password provided for encrypted key. ' 'Original error: %s' % (str(e))) # Indicates invalid password for password protected keys raise paramiko.ssh_exception.SSHException(msg) # Invalid key, try other key type pass else: return key msg = ('Invalid or unsupported key type (only RSA, DSS, ECDSA and' ' Ed25519 keys' ' in PEM format are supported). For more information on ' ' supported key file types, see %s' % (SUPPORTED_KEY_TYPES_URL)) raise paramiko.ssh_exception.SSHException(msg) class ShellOutSSHClient(BaseSSHClient): """ This client shells out to "ssh" binary to run commands on the remote server. Note: This client should not be used in production. """ def __init__(self, hostname, # type: str port=22, # type: int username='root', # type: str password=None, # type: Optional[str] key=None, # type: Optional[str] key_files=None, # type: Optional[str] timeout=None # type: Optional[float] ): super(ShellOutSSHClient, self).__init__(hostname=hostname, port=port, username=username, password=password, key=key, key_files=key_files, timeout=timeout) if self.password: raise ValueError('ShellOutSSHClient only supports key auth') child = subprocess.Popen(['ssh'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) child.communicate() if child.returncode == 127: raise ValueError('ssh client is not available') self.logger = self._get_and_setup_logger() def connect(self): """ This client doesn't support persistent connections establish a new connection every time "run" method is called. """ return True def run(self, cmd, timeout=None): return self._run_remote_shell_command([cmd]) def put(self, path, contents=None, chmod=None, mode='w'): if mode == 'w': redirect = '>' elif mode == 'a': redirect = '>>' else: raise ValueError('Invalid mode: ' + mode) cmd = ['echo "%s" %s %s' % (contents, redirect, path)] self._run_remote_shell_command(cmd) return path def delete(self, path): cmd = ['rm', '-rf', path] self._run_remote_shell_command(cmd) return True def close(self): return True def _get_base_ssh_command(self): # type: () -> List[str] cmd = ['ssh'] if self.key_files: self.key_files = cast(str, self.key_files) cmd += ['-i', self.key_files] if self.timeout: cmd += ['-oConnectTimeout=%s' % (self.timeout)] cmd += ['%s@%s' % (self.username, self.hostname)] return cmd def _run_remote_shell_command(self, cmd): # type: (List[str]) -> Tuple[str, str, int] """ Run a command on a remote server. :param cmd: Command to run. :type cmd: ``list`` of ``str`` :return: Command stdout, stderr and status code. :rtype: ``tuple`` """ base_cmd = self._get_base_ssh_command() full_cmd = base_cmd + [' '.join(cmd)] self.logger.debug('Executing command: "%s"' % (' '.join(full_cmd))) child = subprocess.Popen(full_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = child.communicate() stdout_str = cast(str, stdout) stderr_str = cast(str, stdout) return (stdout_str, stderr_str, child.returncode) class MockSSHClient(BaseSSHClient): pass SSHClient = ParamikoSSHClient # type: Type[BaseSSHClient] if not have_paramiko: SSHClient = MockSSHClient # type: ignore