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/cloud/clouds
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/cloud/clouds/joyent.py
""" Joyent Cloud Module =================== The Joyent Cloud module is used to interact with the Joyent cloud system. Set up the cloud configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/joyent.conf``: .. code-block:: yaml my-joyent-config: driver: joyent # The Joyent login user user: fred # The Joyent user's password password: saltybacon # The location of the ssh private key that can log into the new VM private_key: /root/mykey.pem # The name of the private key keyname: mykey When creating your profiles for the joyent cloud, add the location attribute to the profile, this will automatically get picked up when performing tasks associated with that vm. An example profile might look like: .. code-block:: yaml joyent_512: provider: my-joyent-config size: g4-highcpu-512M image: centos-6 location: us-east-1 This driver can also be used with the Joyent SmartDataCenter project. More details can be found at: .. _`SmartDataCenter`: https://github.com/joyent/sdc Using SDC requires that an api_host_suffix is set. The default value for this is `.api.joyentcloud.com`. All characters, including the leading `.`, should be included: .. code-block:: yaml api_host_suffix: .api.myhostname.com :depends: PyCrypto """ import base64 import datetime import http.client import inspect import logging import os import pprint import salt.config as config import salt.utils.cloud import salt.utils.files import salt.utils.http import salt.utils.json import salt.utils.yaml from salt.exceptions import ( SaltCloudExecutionFailure, SaltCloudExecutionTimeout, SaltCloudNotFound, SaltCloudSystemExit, ) try: from M2Crypto import EVP HAS_REQUIRED_CRYPTO = True HAS_M2 = True except ImportError: HAS_M2 = False try: from Cryptodome.Hash import SHA256 from Cryptodome.Signature import PKCS1_v1_5 HAS_REQUIRED_CRYPTO = True except ImportError: try: from Crypto.Hash import SHA256 # nosec from Crypto.Signature import PKCS1_v1_5 # nosec HAS_REQUIRED_CRYPTO = True except ImportError: HAS_REQUIRED_CRYPTO = False # Get logging started log = logging.getLogger(__name__) __virtualname__ = "joyent" JOYENT_API_HOST_SUFFIX = ".api.joyentcloud.com" JOYENT_API_VERSION = "~7.2" JOYENT_LOCATIONS = { "us-east-1": "North Virginia, USA", "us-west-1": "Bay Area, California, USA", "us-sw-1": "Las Vegas, Nevada, USA", "eu-ams-1": "Amsterdam, Netherlands", } DEFAULT_LOCATION = "us-east-1" # joyent no longer reports on all data centers, so setting this value to true # causes the list_nodes function to get information on machines from all # data centers POLL_ALL_LOCATIONS = True VALID_RESPONSE_CODES = [ http.client.OK, http.client.ACCEPTED, http.client.CREATED, http.client.NO_CONTENT, ] # Only load in this module if the Joyent configurations are in place def __virtual__(): """ Check for Joyent configs """ if HAS_REQUIRED_CRYPTO is False: return False, "Either PyCrypto or Cryptodome needs to be installed." if get_configured_provider() is False: return False return __virtualname__ def _get_active_provider_name(): try: return __active_provider_name__.value() except AttributeError: return __active_provider_name__ def get_configured_provider(): """ Return the first configured instance. """ return config.is_provider_configured( __opts__, _get_active_provider_name() or __virtualname__, ("user", "password") ) def get_image(vm_): """ Return the image object to use """ images = avail_images() vm_image = config.get_cloud_config_value("image", vm_, __opts__) if vm_image and str(vm_image) in images: images[vm_image]["name"] = images[vm_image]["id"] return images[vm_image] raise SaltCloudNotFound(f"The specified image, '{vm_image}', could not be found.") def get_size(vm_): """ Return the VM's size object """ sizes = avail_sizes() vm_size = config.get_cloud_config_value("size", vm_, __opts__) if not vm_size: raise SaltCloudNotFound("No size specified for this VM.") if vm_size and str(vm_size) in sizes: return sizes[vm_size] raise SaltCloudNotFound(f"The specified size, '{vm_size}', could not be found.") def query_instance(vm_=None, call=None): """ Query an instance upon creation from the Joyent API """ if isinstance(vm_, str) and call == "action": vm_ = {"name": vm_, "provider": "joyent"} if call == "function": # Technically this function may be called other ways too, but it # definitely cannot be called with --function. raise SaltCloudSystemExit( "The query_instance action must be called with -a or --action." ) __utils__["cloud.fire_event"]( "event", "querying instance", "salt/cloud/{}/querying".format(vm_["name"]), sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) def _query_ip_address(): data = show_instance(vm_["name"], call="action") if not data: log.error("There was an error while querying Joyent. Empty response") # Trigger a failure in the wait for IP function return False if isinstance(data, dict) and "error" in data: log.warning("There was an error in the query %s", data.get("error")) # Trigger a failure in the wait for IP function return False log.debug("Returned query data: %s", data) if "primaryIp" in data[1]: # Wait for SSH to be fully configured on the remote side if data[1]["state"] == "running": return data[1]["primaryIp"] return None try: data = salt.utils.cloud.wait_for_ip( _query_ip_address, timeout=config.get_cloud_config_value( "wait_for_ip_timeout", vm_, __opts__, default=10 * 60 ), interval=config.get_cloud_config_value( "wait_for_ip_interval", vm_, __opts__, default=10 ), interval_multiplier=config.get_cloud_config_value( "wait_for_ip_interval_multiplier", vm_, __opts__, default=1 ), ) except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc: try: # destroy(vm_['name']) pass except SaltCloudSystemExit: pass finally: raise SaltCloudSystemExit(str(exc)) return data def create(vm_): """ Create a single VM from a data dict CLI Example: .. code-block:: bash salt-cloud -p profile_name vm_name """ try: # Check for required profile parameters before sending any API calls. if ( vm_["profile"] and config.is_profile_configured( __opts__, _get_active_provider_name() or "joyent", vm_["profile"], vm_=vm_, ) is False ): return False except AttributeError: pass key_filename = config.get_cloud_config_value( "private_key", vm_, __opts__, search_global=False, default=None ) __utils__["cloud.fire_event"]( "event", "starting create", "salt/cloud/{}/creating".format(vm_["name"]), args=__utils__["cloud.filter_event"]( "creating", vm_, ["name", "profile", "provider", "driver"] ), sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) log.info( "Creating Cloud VM %s in %s", vm_["name"], vm_.get("location", DEFAULT_LOCATION) ) # added . for fqdn hostnames salt.utils.cloud.check_name(vm_["name"], "a-zA-Z0-9-.") kwargs = { "name": vm_["name"], "image": get_image(vm_), "size": get_size(vm_), "location": vm_.get("location", DEFAULT_LOCATION), } # Let's not assign a default here; only assign a network value if # one is explicitly configured if "networks" in vm_: kwargs["networks"] = vm_.get("networks") __utils__["cloud.fire_event"]( "event", "requesting instance", "salt/cloud/{}/requesting".format(vm_["name"]), args={ "kwargs": __utils__["cloud.filter_event"]( "requesting", kwargs, list(kwargs) ), }, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) data = create_node(**kwargs) if data == {}: log.error("Error creating %s on JOYENT", vm_["name"]) return False query_instance(vm_) data = show_instance(vm_["name"], call="action") vm_["key_filename"] = key_filename vm_["ssh_host"] = data[1]["primaryIp"] __utils__["cloud.bootstrap"](vm_, __opts__) __utils__["cloud.fire_event"]( "event", "created instance", "salt/cloud/{}/created".format(vm_["name"]), args=__utils__["cloud.filter_event"]( "created", vm_, ["name", "profile", "provider", "driver"] ), sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) return data[1] def create_node(**kwargs): """ convenience function to make the rest api call for node creation. """ name = kwargs["name"] size = kwargs["size"] image = kwargs["image"] location = kwargs["location"] networks = kwargs.get("networks") tag = kwargs.get("tag") locality = kwargs.get("locality") metadata = kwargs.get("metadata") firewall_enabled = kwargs.get("firewall_enabled") create_data = { "name": name, "package": size["name"], "image": image["name"], } if networks is not None: create_data["networks"] = networks if locality is not None: create_data["locality"] = locality if metadata is not None: for key, value in metadata.items(): create_data[f"metadata.{key}"] = value if tag is not None: for key, value in tag.items(): create_data[f"tag.{key}"] = value if firewall_enabled is not None: create_data["firewall_enabled"] = firewall_enabled data = salt.utils.json.dumps(create_data) ret = query(command="my/machines", data=data, method="POST", location=location) if ret[0] in VALID_RESPONSE_CODES: return ret[1] else: log.error("Failed to create node %s: %s", name, ret[1]) return {} def destroy(name, call=None): """ destroy a machine by name :param name: name given to the machine :param call: call value in this case is 'action' :return: array of booleans , true if successfully stopped and true if successfully removed CLI Example: .. code-block:: bash salt-cloud -d vm_name """ if call == "function": raise SaltCloudSystemExit( "The destroy action must be called with -d, --destroy, -a or --action." ) __utils__["cloud.fire_event"]( "event", "destroying instance", f"salt/cloud/{name}/destroying", args={"name": name}, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) node = get_node(name) ret = query( command="my/machines/{}".format(node["id"]), location=node["location"], method="DELETE", ) __utils__["cloud.fire_event"]( "event", "destroyed instance", f"salt/cloud/{name}/destroyed", args={"name": name}, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) if __opts__.get("update_cachedir", False) is True: __utils__["cloud.delete_minion_cachedir"]( name, _get_active_provider_name().split(":")[0], __opts__ ) return ret[0] in VALID_RESPONSE_CODES def reboot(name, call=None): """ reboot a machine by name :param name: name given to the machine :param call: call value in this case is 'action' :return: true if successful CLI Example: .. code-block:: bash salt-cloud -a reboot vm_name """ node = get_node(name) ret = take_action( name=name, call=call, method="POST", command="my/machines/{}".format(node["id"]), location=node["location"], data={"action": "reboot"}, ) return ret[0] in VALID_RESPONSE_CODES def stop(name, call=None): """ stop a machine by name :param name: name given to the machine :param call: call value in this case is 'action' :return: true if successful CLI Example: .. code-block:: bash salt-cloud -a stop vm_name """ node = get_node(name) ret = take_action( name=name, call=call, method="POST", command="my/machines/{}".format(node["id"]), location=node["location"], data={"action": "stop"}, ) return ret[0] in VALID_RESPONSE_CODES def start(name, call=None): """ start a machine by name :param name: name given to the machine :param call: call value in this case is 'action' :return: true if successful CLI Example: .. code-block:: bash salt-cloud -a start vm_name """ node = get_node(name) ret = take_action( name=name, call=call, method="POST", command="my/machines/{}".format(node["id"]), location=node["location"], data={"action": "start"}, ) return ret[0] in VALID_RESPONSE_CODES def take_action( name=None, call=None, command=None, data=None, method="GET", location=DEFAULT_LOCATION, ): """ take action call used by start,stop, reboot :param name: name given to the machine :param call: call value in this case is 'action' :command: api path :data: any data to be passed to the api, must be in json format :method: GET,POST,or DELETE :location: data center to execute the command on :return: true if successful """ caller = inspect.stack()[1][3] if call != "action": raise SaltCloudSystemExit("This action must be called with -a or --action.") if data: data = salt.utils.json.dumps(data) ret = [] try: ret = query(command=command, data=data, method=method, location=location) log.info("Success %s for node %s", caller, name) except Exception as exc: # pylint: disable=broad-except if "InvalidState" in str(exc): ret = [200, {}] else: log.error( "Failed to invoke %s node %s: %s", caller, name, exc, # Show the traceback if the debug logging level is enabled exc_info_on_loglevel=logging.DEBUG, ) ret = [100, {}] return ret def ssh_interface(vm_): """ Return the ssh_interface type to connect to. Either 'public_ips' (default) or 'private_ips'. """ return config.get_cloud_config_value( "ssh_interface", vm_, __opts__, default="public_ips", search_global=False ) def get_location(vm_=None): """ Return the joyent data center to use, in this order: - CLI parameter - VM parameter - Cloud profile setting """ return __opts__.get( "location", config.get_cloud_config_value( "location", vm_ or get_configured_provider(), __opts__, default=DEFAULT_LOCATION, search_global=False, ), ) def avail_locations(call=None): """ List all available locations """ if call == "action": raise SaltCloudSystemExit( "The avail_locations function must be called with " "-f or --function, or with the --list-locations option" ) ret = {} for key in JOYENT_LOCATIONS: ret[key] = {"name": key, "region": JOYENT_LOCATIONS[key]} # this can be enabled when the bug in the joyent get data centers call is # corrected, currently only the European dc (new api) returns the correct # values # ret = {} # rcode, datacenters = query( # command='my/datacenters', location=DEFAULT_LOCATION, method='GET' # ) # if rcode in VALID_RESPONSE_CODES and isinstance(datacenters, dict): # for key in datacenters: # ret[key] = { # 'name': key, # 'url': datacenters[key] # } return ret def has_method(obj, method_name): """ Find if the provided object has a specific method """ if method_name in dir(obj): return True log.error("Method '%s' not yet supported!", method_name) return False def key_list(items=None): """ convert list to dictionary using the key as the identifier :param items: array to iterate over :return: dictionary """ if items is None: items = [] ret = {} if items and isinstance(items, list): for item in items: if "name" in item: # added for consistency with old code if "id" not in item: item["id"] = item["name"] ret[item["name"]] = item return ret def get_node(name): """ gets the node from the full node list by name :param name: name of the vm :return: node object """ nodes = list_nodes() if name in nodes: return nodes[name] return None def show_instance(name, call=None): """ get details about a machine :param name: name given to the machine :param call: call value in this case is 'action' :return: machine information CLI Example: .. code-block:: bash salt-cloud -a show_instance vm_name """ node = get_node(name) ret = query( command="my/machines/{}".format(node["id"]), location=node["location"], method="GET", ) return ret def _old_libcloud_node_state(id_): """ Libcloud supported node states """ states_int = { 0: "RUNNING", 1: "REBOOTING", 2: "TERMINATED", 3: "PENDING", 4: "UNKNOWN", 5: "STOPPED", 6: "SUSPENDED", 7: "ERROR", 8: "PAUSED", } states_str = { "running": "RUNNING", "rebooting": "REBOOTING", "starting": "STARTING", "terminated": "TERMINATED", "pending": "PENDING", "unknown": "UNKNOWN", "stopping": "STOPPING", "stopped": "STOPPED", "suspended": "SUSPENDED", "error": "ERROR", "paused": "PAUSED", "reconfiguring": "RECONFIGURING", } return states_str[id_] if isinstance(id_, str) else states_int[id_] def joyent_node_state(id_): """ Convert joyent returned state to state common to other data center return values for consistency :param id_: joyent state value :return: state value """ states = { "running": 0, "stopped": 2, "stopping": 2, "provisioning": 3, "deleted": 2, "unknown": 4, } if id_ not in states: id_ = "unknown" return _old_libcloud_node_state(states[id_]) def reformat_node(item=None, full=False): """ Reformat the returned data from joyent, determine public/private IPs and strip out fields if necessary to provide either full or brief content. :param item: node dictionary :param full: full or brief output :return: dict """ desired_keys = [ "id", "name", "state", "public_ips", "private_ips", "size", "image", "location", ] item["private_ips"] = [] item["public_ips"] = [] if "ips" in item: for ip in item["ips"]: if salt.utils.cloud.is_public_ip(ip): item["public_ips"].append(ip) else: item["private_ips"].append(ip) # add any undefined desired keys for key in desired_keys: if key not in item: item[key] = None # remove all the extra key value pairs to provide a brief listing to_del = [] if not full: for key in item.keys(): # iterate over a copy of the keys if key not in desired_keys: to_del.append(key) for key in to_del: del item[key] if "state" in item: item["state"] = joyent_node_state(item["state"]) return item def list_nodes(full=False, call=None): """ list of nodes, keeping only a brief listing CLI Example: .. code-block:: bash salt-cloud -Q """ if call == "action": raise SaltCloudSystemExit( "The list_nodes function must be called with -f or --function." ) ret = {} if POLL_ALL_LOCATIONS: for location in JOYENT_LOCATIONS: result = query(command="my/machines", location=location, method="GET") if result[0] in VALID_RESPONSE_CODES: nodes = result[1] for node in nodes: if "name" in node: node["location"] = location ret[node["name"]] = reformat_node(item=node, full=full) else: log.error("Invalid response when listing Joyent nodes: %s", result[1]) else: location = get_location() result = query(command="my/machines", location=location, method="GET") nodes = result[1] for node in nodes: if "name" in node: node["location"] = location ret[node["name"]] = reformat_node(item=node, full=full) return ret def list_nodes_full(call=None): """ list of nodes, maintaining all content provided from joyent listings CLI Example: .. code-block:: bash salt-cloud -F """ if call == "action": raise SaltCloudSystemExit( "The list_nodes_full function must be called with -f or --function." ) return list_nodes(full=True) def list_nodes_select(call=None): """ Return a list of the VMs that are on the provider, with select fields """ return salt.utils.cloud.list_nodes_select( list_nodes_full("function"), __opts__["query.selection"], call, ) def _get_proto(): """ Checks configuration to see whether the user has SSL turned on. Default is: .. code-block:: yaml use_ssl: True """ use_ssl = config.get_cloud_config_value( "use_ssl", get_configured_provider(), __opts__, search_global=False, default=True, ) if use_ssl is True: return "https" return "http" def avail_images(call=None): """ Get list of available images CLI Example: .. code-block:: bash salt-cloud --list-images Can use a custom URL for images. Default is: .. code-block:: yaml image_url: images.joyent.com/images """ if call == "action": raise SaltCloudSystemExit( "The avail_images function must be called with " "-f or --function, or with the --list-images option" ) user = config.get_cloud_config_value( "user", get_configured_provider(), __opts__, search_global=False ) img_url = config.get_cloud_config_value( "image_url", get_configured_provider(), __opts__, search_global=False, default=f"{DEFAULT_LOCATION}{JOYENT_API_HOST_SUFFIX}/{user}/images", ) if not img_url.startswith("http://") and not img_url.startswith("https://"): img_url = f"{_get_proto()}://{img_url}" rcode, data = query(command="my/images", method="GET") log.debug(data) ret = {} for image in data: ret[image["name"]] = image return ret def avail_sizes(call=None): """ get list of available packages CLI Example: .. code-block:: bash salt-cloud --list-sizes """ if call == "action": raise SaltCloudSystemExit( "The avail_sizes function must be called with " "-f or --function, or with the --list-sizes option" ) rcode, items = query(command="my/packages") if rcode not in VALID_RESPONSE_CODES: return {} return key_list(items=items) def list_keys(kwargs=None, call=None): """ List the keys available """ if call != "function": log.error("The list_keys function must be called with -f or --function.") return False if not kwargs: kwargs = {} ret = {} rcode, data = query(command="my/keys", method="GET") for pair in data: ret[pair["name"]] = pair["key"] return {"keys": ret} def show_key(kwargs=None, call=None): """ List the keys available """ if call != "function": log.error("The list_keys function must be called with -f or --function.") return False if not kwargs: kwargs = {} if "keyname" not in kwargs: log.error("A keyname is required.") return False rcode, data = query( command="my/keys/{}".format(kwargs["keyname"]), method="GET", ) return {"keys": {data["name"]: data["key"]}} def import_key(kwargs=None, call=None): """ List the keys available CLI Example: .. code-block:: bash salt-cloud -f import_key joyent keyname=mykey keyfile=/tmp/mykey.pub """ if call != "function": log.error("The import_key function must be called with -f or --function.") return False if not kwargs: kwargs = {} if "keyname" not in kwargs: log.error("A keyname is required.") return False if "keyfile" not in kwargs: log.error("The location of the SSH keyfile is required.") return False if not os.path.isfile(kwargs["keyfile"]): log.error("The specified keyfile (%s) does not exist.", kwargs["keyfile"]) return False with salt.utils.files.fopen(kwargs["keyfile"], "r") as fp_: kwargs["key"] = salt.utils.stringutils.to_unicode(fp_.read()) send_data = {"name": kwargs["keyname"], "key": kwargs["key"]} kwargs["data"] = salt.utils.json.dumps(send_data) rcode, data = query( command="my/keys", method="POST", data=kwargs["data"], ) log.debug(pprint.pformat(data)) return {"keys": {data["name"]: data["key"]}} def delete_key(kwargs=None, call=None): """ List the keys available CLI Example: .. code-block:: bash salt-cloud -f delete_key joyent keyname=mykey """ if call != "function": log.error("The delete_keys function must be called with -f or --function.") return False if not kwargs: kwargs = {} if "keyname" not in kwargs: log.error("A keyname is required.") return False rcode, data = query( command="my/keys/{}".format(kwargs["keyname"]), method="DELETE", ) return data def get_location_path( location=DEFAULT_LOCATION, api_host_suffix=JOYENT_API_HOST_SUFFIX ): """ create url from location variable :param location: joyent data center location :return: url """ return f"{_get_proto()}://{location}{api_host_suffix}" def query(action=None, command=None, args=None, method="GET", location=None, data=None): """ Make a web call to Joyent """ user = config.get_cloud_config_value( "user", get_configured_provider(), __opts__, search_global=False ) if not user: log.error( "username is required for Joyent API requests. Please set one in your" " provider configuration" ) password = config.get_cloud_config_value( "password", get_configured_provider(), __opts__, search_global=False ) verify_ssl = config.get_cloud_config_value( "verify_ssl", get_configured_provider(), __opts__, search_global=False, default=True, ) ssh_keyfile = config.get_cloud_config_value( "private_key", get_configured_provider(), __opts__, search_global=False, default=True, ) if not ssh_keyfile: log.error( "ssh_keyfile is required for Joyent API requests. Please set one in your" " provider configuration" ) ssh_keyname = config.get_cloud_config_value( "keyname", get_configured_provider(), __opts__, search_global=False, default=True, ) if not ssh_keyname: log.error( "ssh_keyname is required for Joyent API requests. Please set one in your" " provider configuration" ) if not location: location = get_location() api_host_suffix = config.get_cloud_config_value( "api_host_suffix", get_configured_provider(), __opts__, search_global=False, default=JOYENT_API_HOST_SUFFIX, ) path = get_location_path(location=location, api_host_suffix=api_host_suffix) if action: path += action if command: path += f"/{command}" log.debug("User: '%s' on PATH: %s", user, path) if (not user) or (not ssh_keyfile) or (not ssh_keyname) or (not location): return None timenow = datetime.datetime.utcnow() timestamp = timenow.strftime("%a, %d %b %Y %H:%M:%S %Z").strip() rsa_key = salt.crypt.get_rsa_key(ssh_keyfile, None) if HAS_M2: md = EVP.MessageDigest("sha256") md.update(timestamp.encode(__salt_system_encoding__)) digest = md.final() signed = rsa_key.sign(digest, algo="sha256") else: rsa_ = PKCS1_v1_5.new(rsa_key) # pylint: disable=used-before-assignment hash_ = SHA256.new() # pylint: disable=used-before-assignment hash_.update(timestamp.encode(__salt_system_encoding__)) signed = rsa_.sign(hash_) signed = base64.b64encode(signed) user_arr = user.split("/") if len(user_arr) == 1: keyid = f"/{user_arr[0]}/keys/{ssh_keyname}" elif len(user_arr) == 2: keyid = f"/{user_arr[0]}/users/{user_arr[1]}/keys/{ssh_keyname}" else: log.error("Malformed user string") headers = { "Content-Type": "application/json", "Accept": "application/json", "X-Api-Version": JOYENT_API_VERSION, "Date": timestamp, "Authorization": 'Signature keyId="{}",algorithm="rsa-sha256" {}'.format( keyid, signed.decode(__salt_system_encoding__) ), } if not isinstance(args, dict): args = {} # post form data if not data: data = salt.utils.json.dumps({}) return_content = None result = salt.utils.http.query( path, method, params=args, header_dict=headers, data=data, decode=False, text=True, status=True, headers=True, verify_ssl=verify_ssl, opts=__opts__, ) log.debug("Joyent Response Status Code: %s", result["status"]) if "headers" not in result: return [result["status"], result["error"]] if "Content-Length" in result["headers"]: content = result["text"] return_content = salt.utils.yaml.safe_load(content) return [result["status"], return_content]