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/profitbricks.py
""" ProfitBricks Cloud Module ========================= The ProfitBricks SaltStack cloud module allows a ProfitBricks server to be automatically deployed and bootstraped with Salt. :depends: profitbrick >= 3.1.0 The module requires ProfitBricks credentials to be supplied along with an existing virtual datacenter UUID where the server resources will reside. The server should also be assigned a public LAN, a private LAN, or both along with SSH key pairs. ... Set up the cloud configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/profitbricks.conf``: .. code-block:: yaml my-profitbricks-config: driver: profitbricks # The ProfitBricks login username username: user@example.com # The ProfitBricks login password password: secretpassword # The ProfitBricks virtual datacenter UUID datacenter_id: <UUID> # SSH private key filename ssh_private_key: /path/to/private.key # SSH public key filename ssh_public_key: /path/to/public.key .. code-block:: yaml my-profitbricks-profile: provider: my-profitbricks-config # Name of a predefined server size. size: Micro Instance # Assign CPU family to server. cpu_family: INTEL_XEON # Number of CPU cores to allocate to node (overrides server size). cores: 4 # Amount of RAM in multiples of 256 MB (overrides server size). ram: 4096 # The server availability zone. availability_zone: ZONE_1 # Name or UUID of the HDD image to use. image: <UUID> # Image alias could be provided instead of image. # Example 'ubuntu:latest' #image_alias: <IMAGE_ALIAS> # Size of the node disk in GB (overrides server size). disk_size: 40 # Type of disk (HDD or SSD). disk_type: SSD # Storage availability zone to use. disk_availability_zone: ZONE_2 # Assign the server to the specified public LAN. public_lan: <ID> # Assign firewall rules to the network interface. public_firewall_rules: SSH: protocol: TCP port_range_start: 22 port_range_end: 22 # Assign the server to the specified private LAN. private_lan: <ID> # Enable NAT on the private NIC. nat: true # Assign additional volumes to the server. volumes: data-volume: disk_size: 500 disk_availability_zone: ZONE_3 log-volume: disk_size: 50 disk_type: SSD To use a private IP for connecting and bootstrapping node: .. code-block:: yaml my-profitbricks-profile: ssh_interface: private_lan Set ``deploy`` to False if Salt should not be installed on the node. .. code-block:: yaml my-profitbricks-profile: deploy: False """ import logging import os import pprint import time import salt.config as config import salt.utils.cloud import salt.utils.files import salt.utils.stringutils from salt.exceptions import ( SaltCloudConfigError, SaltCloudExecutionFailure, SaltCloudExecutionTimeout, SaltCloudNotFound, SaltCloudSystemExit, ) from salt.utils.versions import Version try: # pylint: disable=no-name-in-module import profitbricks from profitbricks.client import ( LAN, NIC, Datacenter, FirewallRule, IPBlock, LoadBalancer, PBError, PBNotFoundError, ProfitBricksService, Server, Volume, ) # pylint: enable=no-name-in-module HAS_PROFITBRICKS = True except ImportError: HAS_PROFITBRICKS = False # Get logging started log = logging.getLogger(__name__) __virtualname__ = "profitbricks" # Only load in this module if the ProfitBricks configurations are in place def __virtual__(): """ Check for ProfitBricks configurations. """ if get_configured_provider() is False: return False if get_dependencies() 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__, ("username", "password", "datacenter_id"), ) def version_compatible(version): """ Checks profitbricks version """ return Version(profitbricks.API_VERSION) >= Version(version) def get_dependencies(): """ Warn if dependencies are not met. """ return config.check_driver_dependencies( __virtualname__, {"profitbricks": HAS_PROFITBRICKS} ) def get_conn(): """ Return a conn object for the passed VM data """ return ProfitBricksService( username=config.get_cloud_config_value( "username", get_configured_provider(), __opts__, search_global=False ), password=config.get_cloud_config_value( "password", get_configured_provider(), __opts__, search_global=False ), ) def avail_locations(call=None): """ Return a dict of all available VM locations on the cloud provider with relevant data """ if call == "action": raise SaltCloudSystemExit( "The avail_images function must be called with " "-f or --function, or with the --list-locations option" ) ret = {} conn = get_conn() for item in conn.list_locations()["items"]: reg, loc = item["id"].split("/") location = {"id": item["id"]} if reg not in ret: ret[reg] = {} ret[reg][loc] = location return ret def avail_images(call=None): """ Return a list of the images that are on the provider """ if call == "action": raise SaltCloudSystemExit( "The avail_images function must be called with " "-f or --function, or with the --list-images option" ) ret = {} conn = get_conn() for item in conn.list_images()["items"]: image = {"id": item["id"]} image.update(item["properties"]) ret[image["name"]] = image return ret def list_images(call=None, kwargs=None): """ List all the images with alias by location CLI Example: .. code-block:: bash salt-cloud -f list_images my-profitbricks-config location=us/las """ if call != "function": raise SaltCloudSystemExit( "The list_images function must be called with -f or --function." ) if not version_compatible("4.0"): raise SaltCloudNotFound( "The 'image_alias' feature requires the profitbricks SDK v4.0.0 or greater." ) ret = {} conn = get_conn() if kwargs.get("location") is not None: item = conn.get_location(kwargs.get("location"), 3) ret[item["id"]] = {"image_alias": item["properties"]["imageAliases"]} return ret for item in conn.list_locations(3)["items"]: ret[item["id"]] = {"image_alias": item["properties"]["imageAliases"]} return ret def avail_sizes(call=None): """ Return a dict of all available VM sizes on the cloud provider with relevant data. Latest version can be found at: """ if call == "action": raise SaltCloudSystemExit( "The avail_sizes function must be called with " "-f or --function, or with the --list-sizes option" ) sizes = { "Micro Instance": {"id": "1", "ram": 1024, "disk": 50, "cores": 1}, "Small Instance": {"id": "2", "ram": 2048, "disk": 50, "cores": 1}, "Medium Instance": {"id": "3", "ram": 4096, "disk": 50, "cores": 2}, "Large Instance": {"id": "4", "ram": 7168, "disk": 50, "cores": 4}, "Extra Large Instance": {"id": "5", "ram": 14336, "disk": 50, "cores": 8}, "Memory Intensive Instance Medium": { "id": "6", "ram": 28672, "disk": 50, "cores": 4, }, "Memory Intensive Instance Large": { "id": "7", "ram": 57344, "disk": 50, "cores": 8, }, } return sizes def get_size(vm_): """ Return the VM's size object """ vm_size = config.get_cloud_config_value("size", vm_, __opts__) sizes = avail_sizes() if not vm_size: return sizes["Small Instance"] for size in sizes: combinations = (str(sizes[size]["id"]), str(size)) if vm_size and str(vm_size) in combinations: return sizes[size] raise SaltCloudNotFound(f"The specified size, '{vm_size}', could not be found.") def get_datacenter_id(): """ Return datacenter ID from provider configuration """ datacenter_id = config.get_cloud_config_value( "datacenter_id", get_configured_provider(), __opts__, search_global=False ) conn = get_conn() try: conn.get_datacenter(datacenter_id=datacenter_id) except PBNotFoundError: log.error("Failed to get datacenter: %s", datacenter_id) raise return datacenter_id def list_loadbalancers(call=None): """ Return a list of the loadbalancers that are on the provider """ if call == "action": raise SaltCloudSystemExit( "The avail_images function must be called with " "-f or --function, or with the --list-loadbalancers option" ) ret = {} conn = get_conn() datacenter = get_datacenter(conn) for item in conn.list_loadbalancers(datacenter["id"])["items"]: lb = {"id": item["id"]} lb.update(item["properties"]) ret[lb["name"]] = lb return ret def create_loadbalancer(call=None, kwargs=None): """ Creates a loadbalancer within the datacenter from the provider config. CLI Example: .. code-block:: bash salt-cloud -f create_loadbalancer profitbricks name=mylb """ if call != "function": raise SaltCloudSystemExit( "The create_address function must be called with -f or --function." ) if kwargs is None: kwargs = {} conn = get_conn() datacenter_id = get_datacenter_id() loadbalancer = LoadBalancer( name=kwargs.get("name"), ip=kwargs.get("ip"), dhcp=kwargs.get("dhcp") ) response = conn.create_loadbalancer(datacenter_id, loadbalancer) _wait_for_completion(conn, response, 60, "loadbalancer") return response def get_datacenter(conn): """ Return the datacenter from the config provider datacenter ID """ datacenter_id = get_datacenter_id() for item in conn.list_datacenters()["items"]: if item["id"] == datacenter_id: return item raise SaltCloudNotFound( f"The specified datacenter '{datacenter_id}' could not be found." ) def create_datacenter(call=None, kwargs=None): """ Creates a virtual datacenter based on supplied parameters. CLI Example: .. code-block:: bash salt-cloud -f create_datacenter profitbricks name=mydatacenter location=us/las description="my description" """ if call != "function": raise SaltCloudSystemExit( "The create_address function must be called with -f or --function." ) if kwargs is None: kwargs = {} if kwargs.get("name") is None: raise SaltCloudExecutionFailure('The "name" parameter is required') if kwargs.get("location") is None: raise SaltCloudExecutionFailure('The "location" parameter is required') conn = get_conn() datacenter = Datacenter( name=kwargs["name"], location=kwargs["location"], description=kwargs.get("description"), ) response = conn.create_datacenter(datacenter) _wait_for_completion(conn, response, 60, "create_datacenter") return response def get_disk_type(vm_): """ Return the type of disk to use. Either 'HDD' (default) or 'SSD'. """ return config.get_cloud_config_value( "disk_type", vm_, __opts__, default="HDD", search_global=False ) def get_wait_timeout(vm_): """ Return the wait_for_timeout for resource provisioning. """ return config.get_cloud_config_value( "wait_for_timeout", vm_, __opts__, default=15 * 60, search_global=False ) def get_image(vm_): """ Return the image object to use """ vm_image = config.get_cloud_config_value("image", vm_, __opts__).encode( "ascii", "salt-cloud-force-ascii" ) images = avail_images() for key in images: if vm_image and vm_image in (images[key]["id"], images[key]["name"]): return images[key] raise SaltCloudNotFound(f"The specified image, '{vm_image}', could not be found.") def list_datacenters(conn=None, call=None): """ List all the data centers CLI Example: .. code-block:: bash salt-cloud -f list_datacenters my-profitbricks-config """ if call != "function": raise SaltCloudSystemExit( "The list_datacenters function must be called with -f or --function." ) datacenters = [] if not conn: conn = get_conn() for item in conn.list_datacenters()["items"]: datacenter = {"id": item["id"]} datacenter.update(item["properties"]) datacenters.append({item["properties"]["name"]: datacenter}) return {"Datacenters": datacenters} def list_nodes(conn=None, call=None): """ Return a list of VMs that are on the provider """ if call == "action": raise SaltCloudSystemExit( "The list_nodes function must be called with -f or --function." ) if not conn: conn = get_conn() ret = {} datacenter_id = get_datacenter_id() try: nodes = conn.list_servers(datacenter_id=datacenter_id) except PBNotFoundError: log.error("Failed to get nodes list from datacenter: %s", datacenter_id) raise for item in nodes["items"]: node = {"id": item["id"]} node.update(item["properties"]) node["state"] = node.pop("vmState") ret[node["name"]] = node return ret def list_nodes_full(conn=None, call=None): """ Return a list of the VMs that are on the provider, with all fields """ if call == "action": raise SaltCloudSystemExit( "The list_nodes_full function must be called with -f or --function." ) if not conn: conn = get_conn() # pylint: disable=E0602 ret = {} datacenter_id = get_datacenter_id() nodes = conn.list_servers(datacenter_id=datacenter_id, depth=3) for item in nodes["items"]: node = {"id": item["id"]} node.update(item["properties"]) node["state"] = node.pop("vmState") node["public_ips"] = [] node["private_ips"] = [] if item["entities"]["nics"]["items"] > 0: for nic in item["entities"]["nics"]["items"]: if nic["properties"]["ips"]: pass ip_address = nic["properties"]["ips"][0] if salt.utils.cloud.is_public_ip(ip_address): node["public_ips"].append(ip_address) else: node["private_ips"].append(ip_address) ret[node["name"]] = node __utils__["cloud.cache_node_list"]( ret, _get_active_provider_name().split(":")[0], __opts__ ) return ret def reserve_ipblock(call=None, kwargs=None): """ Reserve the IP Block """ if call == "action": raise SaltCloudSystemExit( "The reserve_ipblock function must be called with -f or --function." ) conn = get_conn() if kwargs is None: kwargs = {} ret = {} ret["ips"] = [] if kwargs.get("location") is None: raise SaltCloudExecutionFailure('The "location" parameter is required') location = kwargs.get("location") size = 1 if kwargs.get("size") is not None: size = kwargs.get("size") block = conn.reserve_ipblock(IPBlock(size=size, location=location)) for item in block["properties"]["ips"]: ret["ips"].append(item) return ret def show_instance(name, call=None): """ Show the details from the provider concerning an instance """ if call != "action": raise SaltCloudSystemExit( "The show_instance action must be called with -a or --action." ) nodes = list_nodes_full() __utils__["cloud.cache_node"](nodes[name], _get_active_provider_name(), __opts__) return nodes[name] def get_node(conn, name): """ Return a node for the named VM """ datacenter_id = get_datacenter_id() for item in conn.list_servers(datacenter_id)["items"]: if item["properties"]["name"] == name: node = {"id": item["id"]} node.update(item["properties"]) return node 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_nics(vm_): """ Create network interfaces on appropriate LANs as defined in cloud profile. """ nics = [] if "public_lan" in vm_: firewall_rules = [] # Set LAN to public if it already exists, otherwise create a new # public LAN. if "public_firewall_rules" in vm_: firewall_rules = _get_firewall_rules(vm_["public_firewall_rules"]) nic = NIC( lan=set_public_lan(int(vm_["public_lan"])), name="public", firewall_rules=firewall_rules, ) if "public_ips" in vm_: nic.ips = _get_ip_addresses(vm_["public_ips"]) nics.append(nic) if "private_lan" in vm_: firewall_rules = [] if "private_firewall_rules" in vm_: firewall_rules = _get_firewall_rules(vm_["private_firewall_rules"]) nic = NIC( lan=int(vm_["private_lan"]), name="private", firewall_rules=firewall_rules ) if "private_ips" in vm_: nic.ips = _get_ip_addresses(vm_["private_ips"]) if "nat" in vm_ and "private_ips" not in vm_: nic.nat = vm_["nat"] nics.append(nic) return nics def set_public_lan(lan_id): """ Enables public Internet access for the specified public_lan. If no public LAN is available, then a new public LAN is created. """ conn = get_conn() datacenter_id = get_datacenter_id() try: lan = conn.get_lan(datacenter_id=datacenter_id, lan_id=lan_id) if not lan["properties"]["public"]: conn.update_lan(datacenter_id=datacenter_id, lan_id=lan_id, public=True) return lan["id"] except Exception: # pylint: disable=broad-except lan = conn.create_lan(datacenter_id, LAN(public=True, name="Public LAN")) return lan["id"] def get_public_keys(vm_): """ Retrieve list of SSH public keys. """ key_filename = config.get_cloud_config_value( "ssh_public_key", vm_, __opts__, search_global=False, default=None ) if key_filename is not None: key_filename = os.path.expanduser(key_filename) if not os.path.isfile(key_filename): raise SaltCloudConfigError( f"The defined ssh_public_key '{key_filename}' does not exist" ) ssh_keys = [] with salt.utils.files.fopen(key_filename) as rfh: for key in rfh.readlines(): ssh_keys.append(salt.utils.stringutils.to_unicode(key)) return ssh_keys def get_key_filename(vm_): """ Check SSH private key file and return absolute path if exists. """ key_filename = config.get_cloud_config_value( "ssh_private_key", vm_, __opts__, search_global=False, default=None ) if key_filename is not None: key_filename = os.path.expanduser(key_filename) if not os.path.isfile(key_filename): raise SaltCloudConfigError( f"The defined ssh_private_key '{key_filename}' does not exist" ) return key_filename def signal_event(vm_, event, description): args = __utils__["cloud.filter_event"]( event, vm_, ["name", "profile", "provider", "driver"] ) __utils__["cloud.fire_event"]( "event", description, "salt/cloud/{}/creating".format(vm_["name"]), args=args, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) def create(vm_): """ Create a single VM from a data dict """ 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 "profitbricks"), vm_["profile"], ) is False ): return False except AttributeError: pass if "image_alias" in vm_ and not version_compatible("4.0"): raise SaltCloudNotFound( "The 'image_alias' parameter requires the profitbricks " "SDK v4.0.0 or greater." ) if "image" not in vm_ and "image_alias" not in vm_: log.error("The image or image_alias parameter is required.") signal_event(vm_, "creating", "starting create") data = None datacenter_id = get_datacenter_id() conn = get_conn() # Assemble list of network interfaces from the cloud profile config. nics = _get_nics(vm_) # Assemble list of volumes from the cloud profile config. volumes = [_get_system_volume(vm_)] if "volumes" in vm_: volumes.extend(_get_data_volumes(vm_)) # Assembla the composite server object. server = _get_server(vm_, volumes, nics) signal_event(vm_, "requesting", "requesting instance") try: data = conn.create_server(datacenter_id=datacenter_id, server=server) log.info( "Create server request ID: %s", data["requestId"], exc_info_on_loglevel=logging.DEBUG, ) _wait_for_completion(conn, data, get_wait_timeout(vm_), "create_server") except PBError as exc: log.error( "Error creating %s on ProfitBricks\n\n" "The following exception was thrown by the profitbricks library " "when trying to run the initial deployment: \n%s", vm_["name"], exc, exc_info_on_loglevel=logging.DEBUG, ) return False except Exception as exc: # pylint: disable=W0703 log.error( "Error creating %s \n\nError: \n%s", vm_["name"], exc, exc_info_on_loglevel=logging.DEBUG, ) return False vm_["server_id"] = data["id"] def __query_node_data(vm_, data): """ Query node data until node becomes available. """ running = False try: data = show_instance(vm_["name"], "action") if not data: return False log.debug( "Loaded node data for %s:\nname: %s\nstate: %s", vm_["name"], pprint.pformat(data["name"]), data["state"], ) except Exception as err: # pylint: disable=broad-except log.error( "Failed to get nodes list: %s", err, # Show the trackback if the debug logging level is enabled exc_info_on_loglevel=logging.DEBUG, ) # Trigger a failure in the wait for IP function return False running = data["state"] == "RUNNING" if not running: # Still not running, trigger another iteration return if ssh_interface(vm_) == "private_lan" and data["private_ips"]: vm_["ssh_host"] = data["private_ips"][0] if ssh_interface(vm_) != "private_lan" and data["public_ips"]: vm_["ssh_host"] = data["public_ips"][0] return data try: data = salt.utils.cloud.wait_for_ip( __query_node_data, update_args=(vm_, data), 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 ), ) except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc: try: # It might be already up, let's destroy it! destroy(vm_["name"]) except SaltCloudSystemExit: pass finally: raise SaltCloudSystemExit(str(exc.message)) log.debug("VM is now running") log.info("Created Cloud VM %s", vm_) log.debug("%s VM creation details:\n%s", vm_, pprint.pformat(data)) signal_event(vm_, "created", "created instance") if "ssh_host" in vm_: vm_["key_filename"] = get_key_filename(vm_) ret = __utils__["cloud.bootstrap"](vm_, __opts__) ret.update(data) return ret else: raise SaltCloudSystemExit("A valid IP address was not found.") 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"], ) datacenter_id = get_datacenter_id() conn = get_conn() node = get_node(conn, name) attached_volumes = None delete_volumes = config.get_cloud_config_value( "delete_volumes", get_configured_provider(), __opts__, search_global=False ) # Get volumes before the server is deleted attached_volumes = conn.get_attached_volumes( datacenter_id=datacenter_id, server_id=node["id"] ) conn.delete_server(datacenter_id=datacenter_id, server_id=node["id"]) # The server is deleted and now is safe to delete the volumes if delete_volumes: for vol in attached_volumes["items"]: log.debug("Deleting volume %s", vol["id"]) conn.delete_volume(datacenter_id=datacenter_id, volume_id=vol["id"]) log.debug("Deleted volume %s", vol["id"]) __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 True 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 """ datacenter_id = get_datacenter_id() conn = get_conn() node = get_node(conn, name) conn.reboot_server(datacenter_id=datacenter_id, server_id=node["id"]) return True 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 """ datacenter_id = get_datacenter_id() conn = get_conn() node = get_node(conn, name) conn.stop_server(datacenter_id=datacenter_id, server_id=node["id"]) return True 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 """ datacenter_id = get_datacenter_id() conn = get_conn() node = get_node(conn, name) conn.start_server(datacenter_id=datacenter_id, server_id=node["id"]) return True def _override_size(vm_): """ Apply any extra component overrides to VM from the cloud profile. """ vm_size = get_size(vm_) if "cores" in vm_: vm_size["cores"] = vm_["cores"] if "ram" in vm_: vm_size["ram"] = vm_["ram"] return vm_size def _get_server(vm_, volumes, nics): """ Construct server instance from cloud profile config """ # Apply component overrides to the size from the cloud profile config vm_size = _override_size(vm_) # Set the server availability zone from the cloud profile config availability_zone = config.get_cloud_config_value( "availability_zone", vm_, __opts__, default=None, search_global=False ) # Assign CPU family from the cloud profile config cpu_family = config.get_cloud_config_value( "cpu_family", vm_, __opts__, default=None, search_global=False ) # Contruct server object return Server( name=vm_["name"], ram=vm_size["ram"], availability_zone=availability_zone, cores=vm_size["cores"], cpu_family=cpu_family, create_volumes=volumes, nics=nics, ) def _get_system_volume(vm_): """ Construct VM system volume list from cloud profile config """ # Override system volume size if 'disk_size' is defined in cloud profile disk_size = get_size(vm_)["disk"] if "disk_size" in vm_: disk_size = vm_["disk_size"] # Construct the system volume volume = Volume( name="{} Storage".format(vm_["name"]), size=disk_size, disk_type=get_disk_type(vm_), ) if "image_password" in vm_: image_password = vm_["image_password"] volume.image_password = image_password # Retrieve list of SSH public keys ssh_keys = get_public_keys(vm_) volume.ssh_keys = ssh_keys if "image_alias" in vm_.keys(): volume.image_alias = vm_["image_alias"] else: volume.image = get_image(vm_)["id"] # Set volume availability zone if defined in the cloud profile if "disk_availability_zone" in vm_: volume.availability_zone = vm_["disk_availability_zone"] return volume def _get_data_volumes(vm_): """ Construct a list of optional data volumes from the cloud profile """ ret = [] volumes = vm_["volumes"] for key, value in volumes.items(): # Verify the required 'disk_size' property is present in the cloud # profile config if "disk_size" not in volumes[key].keys(): raise SaltCloudConfigError(f"The volume '{key}' is missing 'disk_size'") # Use 'HDD' if no 'disk_type' property is present in cloud profile if "disk_type" not in volumes[key].keys(): volumes[key]["disk_type"] = "HDD" # Construct volume object and assign to a list. volume = Volume( name=key, size=volumes[key]["disk_size"], disk_type=volumes[key]["disk_type"], licence_type="OTHER", ) # Set volume availability zone if defined in the cloud profile if "disk_availability_zone" in volumes[key].keys(): volume.availability_zone = volumes[key]["disk_availability_zone"] ret.append(volume) return ret def _get_ip_addresses(ip_addresses): """ Construct a list of ip address """ ret = [] for item in ip_addresses: ret.append(item) return ret def _get_firewall_rules(firewall_rules): """ Construct a list of optional firewall rules from the cloud profile. """ ret = [] for key, value in firewall_rules.items(): # Verify the required 'protocol' property is present in the cloud # profile config if "protocol" not in firewall_rules[key].keys(): raise SaltCloudConfigError( f"The firewall rule '{key}' is missing 'protocol'" ) ret.append( FirewallRule( name=key, protocol=firewall_rules[key].get("protocol", None), source_mac=firewall_rules[key].get("source_mac", None), source_ip=firewall_rules[key].get("source_ip", None), target_ip=firewall_rules[key].get("target_ip", None), port_range_start=firewall_rules[key].get("port_range_start", None), port_range_end=firewall_rules[key].get("port_range_end", None), icmp_type=firewall_rules[key].get("icmp_type", None), icmp_code=firewall_rules[key].get("icmp_code", None), ) ) return ret def _wait_for_completion(conn, promise, wait_timeout, msg): """ Poll request status until resource is provisioned. """ if not promise: return wait_timeout = time.time() + wait_timeout while wait_timeout > time.time(): time.sleep(5) operation_result = conn.get_request( request_id=promise["requestId"], status=True ) if operation_result["metadata"]["status"] == "DONE": return elif operation_result["metadata"]["status"] == "FAILED": raise Exception( "Request: {}, requestId: {} failed to complete:\n{}".format( msg, str(promise["requestId"]), operation_result["metadata"]["message"], ) ) raise Exception( 'Timed out waiting for asynchronous operation {} "{}" to complete.'.format( msg, str(promise["requestId"]) ) )