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/drivers
Viewing File: /opt/imh-python/lib/python3.9/site-packages/libcloud/compute/drivers/scaleway.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. """ Scaleway Driver """ import copy try: import simplejson as json except ImportError: import json from libcloud.common.base import ConnectionUserAndKey, JsonResponse from libcloud.common.types import ProviderError from libcloud.compute.base import NodeDriver, NodeImage, Node, NodeSize from libcloud.compute.base import NodeLocation from libcloud.compute.base import StorageVolume, VolumeSnapshot, KeyPair from libcloud.compute.providers import Provider from libcloud.compute.types import NodeState, VolumeSnapshotState from libcloud.utils.iso8601 import parse_date from libcloud.utils.py3 import httplib __all__ = [ 'ScalewayResponse', 'ScalewayConnection', 'ScalewayNodeDriver' ] SCALEWAY_API_HOSTS = { 'default': 'cp-par1.scaleway.com', 'account': 'account.scaleway.com', 'par1': 'cp-par1.scaleway.com', 'ams1': 'cp-ams1.scaleway.com', } # The API doesn't give location info, so we provide it ourselves, instead. SCALEWAY_LOCATION_DATA = [ {'id': 'par1', 'name': 'Paris 1', 'country': 'FR'}, {'id': 'ams1', 'name': 'Amsterdam 1', 'country': 'NL'}, ] class ScalewayResponse(JsonResponse): valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, httplib.NO_CONTENT] def parse_error(self): return super(ScalewayResponse, self).parse_error()['message'] def success(self): return self.status in self.valid_response_codes class ScalewayConnection(ConnectionUserAndKey): """ Connection class for the Scaleway driver. """ host = SCALEWAY_API_HOSTS['default'] allow_insecure = False responseCls = ScalewayResponse def request(self, action, params=None, data=None, headers=None, method='GET', raw=False, stream=False, region=None): if region: old_host = self.host self.host = SCALEWAY_API_HOSTS[region.id if isinstance(region, NodeLocation) else region] if not self.host == old_host: self.connect() return super(ScalewayConnection, self).request(action, params, data, headers, method, raw, stream) def _request_paged(self, action, params=None, data=None, headers=None, method='GET', raw=False, stream=False, region=None): if params is None: params = {} if isinstance(params, dict): params['per_page'] = 100 elif isinstance(params, list): params.append(('per_page', 100)) # pylint: disable=no-member results = self.request(action, params, data, headers, method, raw, stream, region).object links = self.connection.getresponse().links while links and 'next' in links: next = self.request(links['next']['url'], data=data, headers=headers, method=method, raw=raw, stream=stream).object links = self.connection.getresponse().links merged = {root: child + next[root] for root, child in list(results.items())} results = merged return results def add_default_headers(self, headers): """ Add headers that are necessary for every request """ headers['X-Auth-Token'] = self.key headers['Content-Type'] = 'application/json' return headers def _to_lib_size(size): return int(size / 1000 / 1000 / 1000) def _to_api_size(size): return int(size * 1000 * 1000 * 1000) class ScalewayNodeDriver(NodeDriver): """ Scaleway Node Driver Class This is the primary driver for interacting with Scaleway. It contains all of the standard libcloud methods that Scaleway's API supports. """ type = Provider.SCALEWAY connectionCls = ScalewayConnection name = 'Scaleway' website = 'https://www.scaleway.com/' SNAPSHOT_STATE_MAP = { 'snapshotting': VolumeSnapshotState.CREATING, 'available': VolumeSnapshotState.AVAILABLE, 'error': VolumeSnapshotState.ERROR } def list_locations(self): """ List data centers available. :return: list of node location objects :rtype: ``list`` of :class:`.NodeLocation` """ return [NodeLocation(driver=self, **copy.deepcopy(location)) for location in SCALEWAY_LOCATION_DATA] def list_sizes(self, region=None): """ List available VM sizes. :param region: The region in which to list sizes (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: list of node size objects :rtype: ``list`` of :class:`.NodeSize` """ response = self.connection._request_paged('/products/servers', region=region) sizes = response['servers'] response = self.connection._request_paged( '/products/servers/availability', region=region) availability = response['servers'] return sorted([self._to_size(name, sizes[name], availability[name]) for name in sizes], key=lambda x: x.name) def _to_size(self, name, size, availability): min_disk = (_to_lib_size(size['volumes_constraint']['min_size'] or 0) if size['volumes_constraint'] else 25) max_disk = (_to_lib_size(size['volumes_constraint']['max_size'] or 0) if size['volumes_constraint'] else min_disk) extra = { 'cores': size['ncpus'], 'monthly': size['monthly_price'], 'arch': size['arch'], 'baremetal': size['baremetal'], 'availability': availability['availability'], 'max_disk': max_disk, 'internal_bandwidth': int( (size['network']['sum_internal_bandwidth'] or 0) / (1024 * 1024)), 'ipv6': size['network']['ipv6_support'], 'alt_names': size['alt_names'], } return NodeSize(id=name, name=name, ram=int((size['ram'] or 0) / (1024 * 1024)), disk=min_disk, bandwidth=int( (size['network']['sum_internet_bandwidth'] or 0) / (1024 * 1024)), price=size['hourly_price'], driver=self, extra=extra) def list_images(self, region=None): """ List available VM images. :param region: The region in which to list images (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: list of image objects :rtype: ``list`` of :class:`.NodeImage` """ response = self.connection._request_paged('/images', region=region) images = response['images'] return [self._to_image(image) for image in images] def create_image(self, node, name, region=None): """ Create a VM image from an existing node's root volume. :param node: The node from which to create the image :type node: :class:`.Node` :param name: The name to give the image :type name: ``str`` :param region: The region in which to create the image (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: the newly created image object :rtype: :class:`.NodeImage` """ data = { 'organization': self.key, 'name': name, 'arch': node.extra['arch'], 'root_volume': node.extra['volumes']['0']['id'] } response = self.connection.request('/images', data=json.dumps(data), region=region, method='POST') image = response.object['image'] return self._to_image(image) def delete_image(self, node_image, region=None): """ Delete a VM image. :param node_image: The image to delete :type node_image: :class:`.NodeImage` :param region: The region in which to find/delete the image (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: True if the image was deleted, otherwise False :rtype: ``bool`` """ return self.connection.request('/images/%s' % node_image.id, region=region, method='DELETE').success() def get_image(self, image_id, region=None): """ Retrieve a specific VM image. :param image_id: The id of the image to retrieve :type image_id: ``int`` :param region: The region in which to create the image (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: the requested image object :rtype: :class:`.NodeImage` """ response = self.connection.request('/images/%s' % image_id, region=region) image = response.object['image'] return self._to_image(image) def _to_image(self, image): extra = { 'arch': image['arch'], 'size': _to_lib_size(image.get('root_volume', {}) .get('size', 0)) or 50, 'creation_date': parse_date(image['creation_date']), 'modification_date': parse_date(image['modification_date']), 'organization': image['organization'], } return NodeImage(id=image['id'], name=image['name'], driver=self, extra=extra) def list_nodes(self, region=None): """ List all nodes. :param region: The region in which to look for nodes (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: list of node objects :rtype: ``list`` of :class:`.Node` """ response = self.connection._request_paged('/servers', region=region) servers = response['servers'] return [self._to_node(server) for server in servers] def _to_node(self, server): public_ip = server['public_ip'] private_ip = server['private_ip'] location = server['location'] or {} return Node(id=server['id'], name=server['name'], state=NodeState.fromstring(server['state']), public_ips=[public_ip['address']] if public_ip else [], private_ips=[private_ip] if private_ip else [], driver=self, extra={'volumes': server['volumes'], 'tags': server['tags'], 'arch': server['arch'], 'organization': server['organization'], 'region': location.get('zone_id', 'par1')}, created_at=parse_date(server['creation_date'])) def create_node(self, name, size, image, ex_volumes=None, ex_tags=None, region=None): """ Create a new node. :param name: The name to give the node :type name: ``str`` :param size: The size of node to create :type size: :class:`.NodeSize` :param image: The image to create the node with :type image: :class:`.NodeImage` :param ex_volumes: Additional volumes to create the node with :type ex_volumes: ``dict`` of :class:`.StorageVolume`s :param ex_tags: Tags to assign to the node :type ex_tags: ``list`` of ``str`` :param region: The region in which to create the node (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: the newly created node object :rtype: :class:`.Node` """ data = { 'name': name, 'organization': self.key, 'image': image.id, 'volumes': ex_volumes or {}, 'commercial_type': size.id, 'tags': ex_tags or [] } allocate_space = image.extra.get('size', 50) for volume in data['volumes']: allocate_space += _to_lib_size(volume['size']) while allocate_space < size.disk: if size.disk - allocate_space > 150: bump = 150 else: bump = size.disk - allocate_space vol_num = len(data['volumes']) + 1 data['volumes'][str(vol_num)] = { "name": "%s-%d" % (name, vol_num), "organization": self.key, "size": _to_api_size(bump), "volume_type": "l_ssd" } allocate_space += bump if allocate_space > size.extra.get('max_disk', size.disk): range = ("of %dGB" % size.disk if size.extra.get('max_disk', size.disk) == size.disk else "between %dGB and %dGB" % (size.extra.get('max_disk', size.disk), size.disk)) raise ProviderError( value=("%s only supports a total volume size %s; tried %dGB" % (size.id, range, allocate_space)), http_code=400, driver=self) response = self.connection.request('/servers', data=json.dumps(data), region=region, method='POST') server = response.object['server'] node = self._to_node(server) node.extra['region'] = (region.id if isinstance(region, NodeLocation) else region) or 'par1' # Scaleway doesn't start servers by default, let's do it self._action(node.id, 'poweron') return node def _action(self, server_id, action, region=None): return self.connection.request('/servers/%s/action' % server_id, region=region, data=json.dumps({'action': action}), method='POST').success() def reboot_node(self, node): """ Reboot a node. :param node: The node to be rebooted :type node: :class:`Node` :return: True if the reboot was successful, otherwise False :rtype: ``bool`` """ return self._action(node.id, 'reboot') def destroy_node(self, node): """ Destroy a node. :param node: The node to be destroyed :type node: :class:`Node` :return: True if the destroy was successful, otherwise False :rtype: ``bool`` """ return self._action(node.id, 'terminate') def list_volumes(self, region=None): """ Return a list of volumes. :param region: The region in which to look for volumes (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: A list of volume objects. :rtype: ``list`` of :class:`StorageVolume` """ response = self.connection._request_paged('/volumes', region=region) volumes = response['volumes'] return [self._to_volume(volume) for volume in volumes] def _to_volume(self, volume): extra = { 'organization': volume['organization'], 'volume_type': volume['volume_type'], 'creation_date': parse_date(volume['creation_date']), 'modification_date': parse_date(volume['modification_date']), } return StorageVolume(id=volume['id'], name=volume['name'], size=_to_lib_size(volume['size']), driver=self, extra=extra) def list_volume_snapshots(self, volume, region=None): """ List snapshots for a storage volume. @inherits :class:`NodeDriver.list_volume_snapshots` :param region: The region in which to look for snapshots (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` """ response = self.connection._request_paged('/snapshots', region=region) snapshots = filter(lambda s: s['base_volume']['id'] == volume.id, response['snapshots']) return [self._to_snapshot(snapshot) for snapshot in snapshots] def _to_snapshot(self, snapshot): state = self.SNAPSHOT_STATE_MAP.get(snapshot['state'], VolumeSnapshotState.UNKNOWN) extra = { 'organization': snapshot['organization'], 'volume_type': snapshot['volume_type'], } return VolumeSnapshot(id=snapshot['id'], driver=self, size=_to_lib_size(snapshot['size']), created=parse_date(snapshot['creation_date']), state=state, extra=extra) def create_volume(self, size, name, region=None): """ Create a new volume. :param size: Size of volume in gigabytes. :type size: ``int`` :param name: Name of the volume to be created. :type name: ``str`` :param region: The region in which to create the volume (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: The newly created volume. :rtype: :class:`StorageVolume` """ data = { 'name': name, 'organization': self.key, 'volume_type': 'l_ssd', 'size': _to_api_size(size) } response = self.connection.request('/volumes', region=region, data=json.dumps(data), method='POST') volume = response.object['volume'] return self._to_volume(volume) def create_volume_snapshot(self, volume, name, region=None): """ Create snapshot from volume. :param volume: The volume to create a snapshot from :type volume: :class`StorageVolume` :param name: The name to give the snapshot :type name: ``str`` :param region: The region in which to create the snapshot (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: The newly created snapshot. :rtype: :class:`VolumeSnapshot` """ data = { 'name': name, 'organization': self.key, 'volume_id': volume.id } response = self.connection.request('/snapshots', region=region, data=json.dumps(data), method='POST') snapshot = response.object['snapshot'] return self._to_snapshot(snapshot) def destroy_volume(self, volume, region=None): """ Destroys a storage volume. :param volume: Volume to be destroyed :type volume: :class:`StorageVolume` :param region: The region in which to look for the volume (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: True if the destroy was successful, otherwise False :rtype: ``bool`` """ return self.connection.request('/volumes/%s' % volume.id, region=region, method='DELETE').success() def destroy_volume_snapshot(self, snapshot, region=None): """ Dostroy a volume snapshot :param snapshot: volume snapshot to destroy :type snapshot: class:`VolumeSnapshot` :param region: The region in which to look for the snapshot (if None, use default region specified in __init__) :type region: :class:`.NodeLocation` :return: True if the destroy was successful, otherwise False :rtype: ``bool`` """ return self.connection.request('/snapshots/%s' % snapshot.id, region=region, method='DELETE').success() def list_key_pairs(self): """ List all the available SSH keys. :return: Available SSH keys. :rtype: ``list`` of :class:`KeyPair` """ response = self.connection.request('/users/%s' % (self._get_user_id()), region='account') keys = response.object['user']['ssh_public_keys'] return [KeyPair(name=' '.join(key['key'].split(' ')[2:]), public_key=' '.join(key['key'].split(' ')[:2]), fingerprint=key['fingerprint'], driver=self) for key in keys] def import_key_pair_from_string(self, name, key_material): """ Import a new public key from string. :param name: Key pair name. :type name: ``str`` :param key_material: Public key material. :type key_material: ``str`` :return: Imported key pair object. :rtype: :class:`KeyPair` """ new_key = KeyPair(name=name, public_key=' '.join(key_material.split(' ')[:2]), fingerprint=None, driver=self) keys = [key for key in self.list_key_pairs() if not key.name == name] keys.append(new_key) return self._save_keys(keys) def delete_key_pair(self, key_pair): """ Delete an existing key pair. :param key_pair: Key pair object. :type key_pair: :class:`KeyPair` :return: True of False based on success of Keypair deletion :rtype: ``bool`` """ keys = [key for key in self.list_key_pairs() if not key.name == key_pair.name] return self._save_keys(keys) def _get_user_id(self): response = self.connection.request('/tokens/%s' % self.secret, region='account') return response.object['token']['user_id'] def _save_keys(self, keys): data = { 'ssh_public_keys': [{'key': '%s %s' % (key.public_key, key.name)} for key in keys] } response = self.connection.request('/users/%s' % (self._get_user_id()), region='account', method='PATCH', data=json.dumps(data)) return response.success()