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/proxmox.py
""" Proxmox Cloud Module ====================== .. versionadded:: 2014.7.0 The Proxmox cloud module is used to control access to cloud providers using the Proxmox system (KVM / OpenVZ / LXC). Set up the cloud configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/proxmox.conf``: .. code-block:: yaml my-proxmox-config: # Proxmox account information user: myuser@pam or myuser@pve password: mypassword url: hypervisor.domain.tld port: 8006 driver: proxmox verify_ssl: True :maintainer: Frank Klaassen <frank@cloudright.nl> :depends: requests >= 2.2.1 :depends: IPy >= 0.81 """ import logging import pprint import re import socket import time import urllib import salt.config as config import salt.utils.cloud import salt.utils.json from salt.exceptions import ( SaltCloudExecutionFailure, SaltCloudExecutionTimeout, SaltCloudSystemExit, ) try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: from IPy import IP HAS_IPY = True except ImportError: HAS_IPY = False # Get logging started log = logging.getLogger(__name__) __virtualname__ = "proxmox" def __virtual__(): """ Check for PROXMOX 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__, ("user",) ) def get_dependencies(): """ Warn if dependencies aren't met. """ deps = {"requests": HAS_REQUESTS, "IPy": HAS_IPY} return config.check_driver_dependencies(__virtualname__, deps) url = None port = None ticket = None csrf = None verify_ssl = None api = None def _authenticate(): """ Retrieve CSRF and API tickets for the Proxmox API """ global url, port, ticket, csrf, verify_ssl url = config.get_cloud_config_value( "url", get_configured_provider(), __opts__, search_global=False ) port = config.get_cloud_config_value( "port", get_configured_provider(), __opts__, default=8006, search_global=False ) username = ( config.get_cloud_config_value( "user", get_configured_provider(), __opts__, search_global=False ), ) passwd = 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__, default=True, search_global=False, ) connect_data = {"username": username, "password": passwd} full_url = f"https://{url}:{port}/api2/json/access/ticket" response = requests.post( full_url, verify=verify_ssl, data=connect_data, timeout=120 ) response.raise_for_status() returned_data = response.json() ticket = {"PVEAuthCookie": returned_data["data"]["ticket"]} csrf = str(returned_data["data"]["CSRFPreventionToken"]) def query(conn_type, option, post_data=None): """ Execute the HTTP request to the API """ if ticket is None or csrf is None or url is None: log.debug("Not authenticated yet, doing that now..") _authenticate() full_url = f"https://{url}:{port}/api2/json/{option}" log.debug("%s: %s (%s)", conn_type, full_url, post_data) httpheaders = { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "salt-cloud-proxmox", } if conn_type == "post": httpheaders["CSRFPreventionToken"] = csrf response = requests.post( full_url, verify=verify_ssl, data=post_data, cookies=ticket, headers=httpheaders, timeout=120, ) elif conn_type == "put": httpheaders["CSRFPreventionToken"] = csrf response = requests.put( full_url, verify=verify_ssl, data=post_data, cookies=ticket, headers=httpheaders, timeout=120, ) elif conn_type == "delete": httpheaders["CSRFPreventionToken"] = csrf response = requests.delete( full_url, verify=verify_ssl, data=post_data, cookies=ticket, headers=httpheaders, timeout=120, ) elif conn_type == "get": response = requests.get( full_url, verify=verify_ssl, cookies=ticket, timeout=120 ) try: response.raise_for_status() except requests.exceptions.RequestException: # Log the details of the response. log.error("Error in %s query to %s:\n%s", conn_type, full_url, response.text) raise try: returned_data = response.json() if "data" not in returned_data: raise SaltCloudExecutionFailure return returned_data["data"] except Exception: # pylint: disable=broad-except log.error("Error in trying to process JSON") log.error(response) def _get_vm_by_name(name, allDetails=False): """ Since Proxmox works based op id's rather than names as identifiers this requires some filtering to retrieve the required information. """ vms = get_resources_vms(includeConfig=allDetails) if name in vms: return vms[name] log.info('VM with name "%s" could not be found.', name) return False def _get_vm_by_id(vmid, allDetails=False): """ Retrieve a VM based on the ID. """ for vm_name, vm_details in get_resources_vms(includeConfig=allDetails).items(): if str(vm_details["vmid"]) == str(vmid): return vm_details log.info('VM with ID "%s" could not be found.', vmid) return False def _get_next_vmid(): """ Proxmox allows the use of alternative ids instead of autoincrementing. Because of that its required to query what the first available ID is. """ return int(query("get", "cluster/nextid")) def _check_ip_available(ip_addr): """ Proxmox VMs refuse to start when the IP is already being used. This function can be used to prevent VMs being created with duplicate IP's or to generate a warning. """ for vm_name, vm_details in get_resources_vms(includeConfig=True).items(): vm_config = vm_details["config"] if ip_addr in vm_config["ip_address"] or vm_config["ip_address"] == ip_addr: log.debug('IP "%s" is already defined', ip_addr) return False log.debug("IP '%s' is available to be defined", ip_addr) return True def _parse_proxmox_upid(node, vm_=None): """ Upon requesting a task that runs for a longer period of time a UPID is given. This includes information about the job and can be used to lookup information in the log. """ ret = {} upid = node # Parse node response node = node.split(":") if node[0] == "UPID": ret["node"] = str(node[1]) ret["pid"] = str(node[2]) ret["pstart"] = str(node[3]) ret["starttime"] = str(node[4]) ret["type"] = str(node[5]) ret["vmid"] = str(node[6]) ret["user"] = str(node[7]) # include the upid again in case we'll need it again ret["upid"] = str(upid) if vm_ is not None and "technology" in vm_: ret["technology"] = str(vm_["technology"]) return ret def _lookup_proxmox_task(upid): """ Retrieve the (latest) logs and retrieve the status for a UPID. This can be used to verify whether a task has completed. """ log.debug("Getting creation status for upid: %s", upid) tasks = query("get", "cluster/tasks") if tasks: for task in tasks: if task["upid"] == upid: log.debug("Found upid task: %s", task) return task return False def get_resources_nodes(call=None, resFilter=None): """ Retrieve all hypervisors (nodes) available on this environment CLI Example: .. code-block:: bash salt-cloud -f get_resources_nodes my-proxmox-config """ log.debug("Getting resource: nodes.. (filter: %s)", resFilter) resources = query("get", "cluster/resources") ret = {} for resource in resources: if "type" in resource and resource["type"] == "node": name = resource["node"] ret[name] = resource if resFilter is not None: log.debug("Filter given: %s, returning requested resource: nodes", resFilter) return ret[resFilter] log.debug("Filter not given: %s, returning all resource: nodes", ret) return ret def get_resources_vms(call=None, resFilter=None, includeConfig=True): """ Retrieve all VMs available on this environment CLI Example: .. code-block:: bash salt-cloud -f get_resources_vms my-proxmox-config """ timeoutTime = time.time() + 60 while True: log.debug("Getting resource: vms.. (filter: %s)", resFilter) resources = query("get", "cluster/resources") ret = {} badResource = False for resource in resources: if "type" in resource and resource["type"] in ["openvz", "qemu", "lxc"]: try: name = resource["name"] except KeyError: badResource = True log.debug("No name in VM resource %s", repr(resource)) break ret[name] = resource if includeConfig: # Requested to include the detailed configuration of a VM ret[name]["config"] = get_vmconfig( ret[name]["vmid"], ret[name]["node"], ret[name]["type"] ) if time.time() > timeoutTime: raise SaltCloudExecutionTimeout("FAILED to get the proxmox resources vms") # Carry on if there wasn't a bad resource return from Proxmox if not badResource: break time.sleep(0.5) if resFilter is not None: log.debug("Filter given: %s, returning requested resource: nodes", resFilter) return ret[resFilter] log.debug("Filter not given: %s, returning all resource: nodes", ret) return ret def script(vm_): """ Return the script deployment object """ script_name = config.get_cloud_config_value("script", vm_, __opts__) if not script_name: script_name = "bootstrap-salt" return salt.utils.cloud.os_script( script_name, vm_, __opts__, salt.utils.cloud.salt_config_to_yaml( salt.utils.cloud.minion_config(__opts__, vm_) ), ) def avail_locations(call=None): """ Return a list of the hypervisors (nodes) which this Proxmox PVE machine manages CLI Example: .. code-block:: bash salt-cloud --list-locations my-proxmox-config """ if call == "action": raise SaltCloudSystemExit( "The avail_locations function must be called with " "-f or --function, or with the --list-locations option" ) # could also use the get_resources_nodes but speed is ~the same nodes = query("get", "nodes") ret = {} for node in nodes: name = node["node"] ret[name] = node return ret def avail_images(call=None, location="local"): """ Return a list of the images that are on the provider CLI Example: .. code-block:: bash salt-cloud --list-images my-proxmox-config """ if call == "action": raise SaltCloudSystemExit( "The avail_images function must be called with " "-f or --function, or with the --list-images option" ) ret = {} for host_name, host_details in avail_locations().items(): for item in query("get", f"nodes/{host_name}/storage/{location}/content"): ret[item["volid"]] = item return ret def list_nodes(call=None): """ Return a list of the VMs that are managed by the provider CLI Example: .. code-block:: bash salt-cloud -Q my-proxmox-config """ if call == "action": raise SaltCloudSystemExit( "The list_nodes function must be called with -f or --function." ) ret = {} for vm_name, vm_details in get_resources_vms(includeConfig=True).items(): log.debug("VM_Name: %s", vm_name) log.debug("vm_details: %s", vm_details) # Limit resultset on what Salt-cloud demands: ret[vm_name] = {} ret[vm_name]["id"] = str(vm_details["vmid"]) ret[vm_name]["image"] = str(vm_details["vmid"]) ret[vm_name]["size"] = str(vm_details["disk"]) ret[vm_name]["state"] = str(vm_details["status"]) # Figure out which is which to put it in the right column private_ips = [] public_ips = [] if ( "ip_address" in vm_details["config"] and vm_details["config"]["ip_address"] != "-" ): ips = vm_details["config"]["ip_address"].split(" ") for ip_ in ips: if IP(ip_).iptype() == "PRIVATE": private_ips.append(str(ip_)) else: public_ips.append(str(ip_)) ret[vm_name]["private_ips"] = private_ips ret[vm_name]["public_ips"] = public_ips return ret def list_nodes_full(call=None): """ Return a list of the VMs that are on the provider CLI Example: .. code-block:: bash salt-cloud -F my-proxmox-config """ if call == "action": raise SaltCloudSystemExit( "The list_nodes_full function must be called with -f or --function." ) return get_resources_vms(includeConfig=True) def list_nodes_select(call=None): """ Return a list of the VMs that are on the provider, with select fields CLI Example: .. code-block:: bash salt-cloud -S my-proxmox-config """ return salt.utils.cloud.list_nodes_select( list_nodes_full(), __opts__["query.selection"], call, ) def _stringlist_to_dictionary(input_string): """ Convert a stringlist (comma separated settings) to a dictionary The result of the string setting1=value1,setting2=value2 will be a python dictionary: {'setting1':'value1','setting2':'value2'} """ return dict(item.strip().split("=") for item in input_string.split(",") if item) def _dictionary_to_stringlist(input_dict): """ Convert a dictionary to a stringlist (comma separated settings) The result of the dictionary {'setting1':'value1','setting2':'value2'} will be: setting1=value1,setting2=value2 """ return ",".join(f"{k}={input_dict[k]}" for k in sorted(input_dict.keys())) def _reconfigure_clone(vm_, vmid): """ If we cloned a machine, see if we need to reconfigure any of the options such as net0, ide2, etc. This enables us to have a different cloud-init ISO mounted for each VM that's brought up :param vm_: :return: """ if not vm_.get("technology") == "qemu": log.warning("Reconfiguring clones is only available under `qemu`") return # Determine which settings can be reconfigured. query_path = "nodes/{}/qemu/{}/config" valid_settings = set(_get_properties(query_path.format("{node}", "{vmid}"), "POST")) log.info("Configuring cloned VM") # Modify the settings for the VM one at a time so we can see any problems with the values # as quickly as possible for setting in vm_: postParams = None if setting == "vmid": pass # vmid gets passed in the URL and can't be reconfigured elif re.match(r"^net(\d+)$", setting): # net strings are a list of comma seperated settings. We need to merge the settings so that # the setting in the profile only changes the settings it touches and the other settings # are left alone. An example of why this is necessary is because the MAC address is set # in here and generally you don't want to alter or have to know the MAC address of the new # instance, but you may want to set the VLAN bridge data = query("get", "nodes/{}/qemu/{}/config".format(vm_["host"], vmid)) # Generate a dictionary of settings from the existing string new_setting = {} if setting in data: new_setting.update(_stringlist_to_dictionary(data[setting])) # Merge the new settings (as a dictionary) into the existing dictionary to get the # new merged settings new_setting.update(_stringlist_to_dictionary(vm_[setting])) # Convert the dictionary back into a string list postParams = {setting: _dictionary_to_stringlist(new_setting)} elif setting == "sshkeys": postParams = {setting: urllib.parse.quote(vm_[setting], safe="")} elif setting in valid_settings: postParams = {setting: vm_[setting]} if postParams: query( "post", "nodes/{}/qemu/{}/config".format(vm_["host"], vmid), postParams, ) def create(vm_): """ Create a single VM from a data dict CLI Example: .. code-block:: bash salt-cloud -p proxmox-ubuntu vmhostname """ 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 "proxmox", vm_["profile"], vm_=vm_, ) is False ): return False except AttributeError: pass ret = {} __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", vm_["name"]) if "use_dns" in vm_ and "ip_address" not in vm_: use_dns = vm_["use_dns"] if use_dns: from socket import gaierror, gethostbyname try: ip_address = gethostbyname(str(vm_["name"])) except gaierror: log.debug("Resolving of %s failed", vm_["name"]) else: vm_["ip_address"] = str(ip_address) try: newid = _get_next_vmid() data = create_node(vm_, newid) except Exception as exc: # pylint: disable=broad-except msg = str(exc) if ( isinstance(exc, requests.exceptions.RequestException) and exc.response is not None ): msg = msg + "\n" + exc.response.text log.error( "Error creating %s on PROXMOX\n\n" "The following exception was thrown when trying to " "run the initial deployment: \n%s", vm_["name"], msg, # Show the traceback if the debug logging level is enabled exc_info_on_loglevel=logging.DEBUG, ) return False ret["creation_data"] = data name = vm_["name"] # hostname which we know vmid = data["vmid"] # vmid which we have received host = data["node"] # host which we have received nodeType = data["technology"] # VM tech (Qemu / OpenVZ) agent_get_ip = vm_.get("agent_get_ip", False) if agent_get_ip is False: # Determine which IP to use in order of preference: if "ip_address" in vm_: ip_address = str(vm_["ip_address"]) elif "public_ips" in data: ip_address = str(data["public_ips"][0]) # first IP elif "private_ips" in data: ip_address = str(data["private_ips"][0]) # first IP else: raise SaltCloudExecutionFailure("Could not determine an IP address to use") log.debug("Using IP address %s", ip_address) # wait until the vm has been created so we can start it if not wait_for_created(data["upid"], timeout=300): return {"Error": f"Unable to create {name}, command timed out"} if vm_.get("clone") is True: _reconfigure_clone(vm_, vmid) # VM has been created. Starting.. if not start(name, vmid, call="action"): log.error("Node %s (%s) failed to start!", name, vmid) raise SaltCloudExecutionFailure # Wait until the VM has fully started log.debug('Waiting for state "running" for vm %s on %s', vmid, host) if not wait_for_state(vmid, "running"): return {"Error": f"Unable to start {name}, command timed out"} if agent_get_ip is True: try: ip_address = salt.utils.cloud.wait_for_fun( _find_agent_ip, vm_=vm_, vmid=vmid ) except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc: try: # If VM was created but we can't connect, destroy it. destroy(vm_["name"]) except SaltCloudSystemExit: pass finally: raise SaltCloudSystemExit(str(exc)) log.debug("Using IP address %s", ip_address) ssh_username = config.get_cloud_config_value( "ssh_username", vm_, __opts__, default="root" ) ssh_password = config.get_cloud_config_value( "password", vm_, __opts__, ) ret["ip_address"] = ip_address ret["username"] = ssh_username ret["password"] = ssh_password vm_["ssh_host"] = ip_address vm_["password"] = ssh_password ret = __utils__["cloud.bootstrap"](vm_, __opts__) # Report success! log.info("Created Cloud VM '%s'", vm_["name"]) log.debug("'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data)) __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"], ) return ret def preferred_ip(vm_, ips): """ Return either an 'ipv4' (default) or 'ipv6' address depending on 'protocol' option. The list of 'ipv4' IPs is filtered by ignore_cidr() to remove any unreachable private addresses. """ proto = config.get_cloud_config_value( "protocol", vm_, __opts__, default="ipv4", search_global=False ) family = socket.AF_INET if proto == "ipv6": family = socket.AF_INET6 for ip in ips: ignore_ip = ignore_cidr(vm_, ip) if ignore_ip: continue try: socket.inet_pton(family, ip) return ip except Exception: # pylint: disable=broad-except continue return False def ignore_cidr(vm_, ip): """ Return True if we are to ignore the specified IP. """ from ipaddress import ip_address, ip_network cidrs = config.get_cloud_config_value( "ignore_cidr", vm_, __opts__, default=[], search_global=False ) if cidrs and isinstance(cidrs, str): cidrs = [cidrs] for cidr in cidrs or []: if ip_address(ip) in ip_network(cidr): log.warning("IP %r found within %r; ignoring it.", ip, cidr) return True return False def _find_agent_ip(vm_, vmid): """ If VM is started we would return the IP-addresses that are returned by the qemu agent on the VM. """ # This functionality is only available on qemu if not vm_.get("technology") == "qemu": log.warning("Find agent IP is only available under `qemu`") return # Create an empty list of IP-addresses: ips = [] endpoint = "nodes/{}/qemu/{}/agent/network-get-interfaces".format(vm_["host"], vmid) interfaces = query("get", endpoint) # If we get a result from the agent, parse it for interface in interfaces["result"]: # Skip interface if hardware-address is 00:00:00:00:00:00 (loopback interface) if str(interface.get("hardware-address")) == "00:00:00:00:00:00": continue # Skip entries without ip-addresses information if "ip-addresses" not in interface: continue for if_addr in interface["ip-addresses"]: ip_addr = if_addr.get("ip-address") if ip_addr is not None: ips.append(str(ip_addr)) if len(ips) > 0: return preferred_ip(vm_, ips) raise SaltCloudExecutionFailure def _import_api(): """ Download https://<url>/pve-docs/api-viewer/apidoc.js Extract content of pveapi var (json formatted) Load this json content into global variable "api" """ global api full_url = f"https://{url}:{port}/pve-docs/api-viewer/apidoc.js" returned_data = requests.get(full_url, verify=verify_ssl, timeout=120) re_filter = re.compile(" (?:pveapi|apiSchema) = (.*)^;", re.DOTALL | re.MULTILINE) api_json = re_filter.findall(returned_data.text)[0] api = salt.utils.json.loads(api_json) def _get_properties(path="", method="GET", forced_params=None): """ Return the parameter list from api for defined path and HTTP method """ if api is None: _import_api() sub = api path_levels = [level for level in path.split("/") if level != ""] search_path = "" props = [] parameters = set([] if forced_params is None else forced_params) # Browse all path elements but last for elem in path_levels[:-1]: search_path += "/" + elem # Lookup for a dictionary with path = "requested path" in list" and return its children sub = next(item for item in sub if item["path"] == search_path)["children"] # Get leaf element in path search_path += "/" + path_levels[-1] sub = next(item for item in sub if item["path"] == search_path) try: # get list of properties for requested method props = sub["info"][method]["parameters"]["properties"].keys() except KeyError as exc: log.error('method not found: "%s"', exc) for prop in props: numerical = re.match(r"(\w+)\[n\]", prop) # generate (arbitrarily) 10 properties for duplicatable properties identified by: # "prop[n]" if numerical: for i in range(10): parameters.add(numerical.group(1) + str(i)) else: parameters.add(prop) return parameters def create_node(vm_, newid): """ Build and submit the requestdata to create a new node """ newnode = {} if "technology" not in vm_: vm_["technology"] = "openvz" # default virt tech if none is given if vm_["technology"] not in ["qemu", "openvz", "lxc"]: # Wrong VM type given log.error( "Wrong VM type. Valid options are: qemu, openvz (proxmox3) or lxc" " (proxmox4)" ) raise SaltCloudExecutionFailure if "host" not in vm_: # Use globally configured/default location vm_["host"] = config.get_cloud_config_value( "default_host", get_configured_provider(), __opts__, search_global=False ) if vm_["host"] is None: # No location given for the profile log.error("No host given to create this VM on") raise SaltCloudExecutionFailure # Required by both OpenVZ and Qemu (KVM) vmhost = vm_["host"] newnode["vmid"] = newid for prop in "cpuunits", "description", "memory", "onboot": if prop in vm_: # if the property is set, use it for the VM request newnode[prop] = vm_[prop] if vm_["technology"] == "openvz": # OpenVZ related settings, using non-default names: newnode["hostname"] = vm_["name"] newnode["ostemplate"] = vm_["image"] # optional VZ settings for prop in ( "cpus", "disk", "ip_address", "nameserver", "password", "swap", "poolid", "storage", ): if prop in vm_: # if the property is set, use it for the VM request newnode[prop] = vm_[prop] elif vm_["technology"] == "lxc": # LXC related settings, using non-default names: newnode["hostname"] = vm_["name"] newnode["ostemplate"] = vm_["image"] static_props = ( "cpuunits", "cpulimit", "rootfs", "cores", "description", "memory", "onboot", "net0", "password", "nameserver", "swap", "storage", "rootfs", ) for prop in _get_properties("/nodes/{node}/lxc", "POST", static_props): if prop in vm_: # if the property is set, use it for the VM request newnode[prop] = vm_[prop] if "pubkey" in vm_: newnode["ssh-public-keys"] = vm_["pubkey"] # inform user the "disk" option is not supported for LXC hosts if "disk" in vm_: log.warning( 'The "disk" option is not supported for LXC hosts and was ignored' ) # LXC specific network config # OpenVZ allowed specifying IP and gateway. To ease migration from # Proxmox 3, I've mapped the ip_address and gw to a generic net0 config. # If you need more control, please use the net0 option directly. # This also assumes a /24 subnet. if "ip_address" in vm_ and "net0" not in vm_: newnode["net0"] = ( "bridge=vmbr0,ip=" + vm_["ip_address"] + "/24,name=eth0,type=veth" ) # gateway is optional and does not assume a default if "gw" in vm_: newnode["net0"] = newnode["net0"] + ",gw=" + vm_["gw"] elif vm_["technology"] == "qemu": # optional Qemu settings static_props = ( "acpi", "cores", "cpu", "pool", "storage", "sata0", "ostype", "ide2", "net0", ) for prop in _get_properties("/nodes/{node}/qemu", "POST", static_props): if prop in vm_: # if the property is set, use it for the VM request # If specified, vmid will override newid. newnode[prop] = vm_[prop] # The node is ready. Lets request it to be added __utils__["cloud.fire_event"]( "event", "requesting instance", "salt/cloud/{}/requesting".format(vm_["name"]), args={ "kwargs": __utils__["cloud.filter_event"]( "requesting", newnode, list(newnode) ), }, sock_dir=__opts__["sock_dir"], ) log.debug("Preparing to generate a node using these parameters: %s ", newnode) if "clone" in vm_ and vm_["clone"] is True and vm_["technology"] == "qemu": postParams = {} postParams["newid"] = newnode["vmid"] if "pool" in vm_: postParams["pool"] = vm_["pool"] for prop in "description", "format", "full", "name": if ( "clone_" + prop in vm_ ): # if the property is set, use it for the VM request postParams[prop] = vm_["clone_" + prop] try: int(vm_["clone_from"]) except ValueError: if ":" in vm_["clone_from"]: vmhost = vm_["clone_from"].split(":")[0] vm_["clone_from"] = vm_["clone_from"].split(":")[1] node = query( "post", "nodes/{}/qemu/{}/clone".format(vmhost, vm_["clone_from"]), postParams, ) else: node = query("post", "nodes/{}/{}".format(vmhost, vm_["technology"]), newnode) result = _parse_proxmox_upid(node, vm_) # When cloning, the upid contains the clone_from vmid instead of the new vmid result["vmid"] = newnode["vmid"] return result def show_instance(name, call=None): """ Show the details from Proxmox 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_vmconfig(vmid, node=None, node_type="openvz"): """ Get VM configuration """ if node is None: # We need to figure out which node this VM is on. for host_name, host_details in avail_locations().items(): for item in query("get", f"nodes/{host_name}/{node_type}"): if item["vmid"] == vmid: node = host_name # If we reached this point, we have all the information we need data = query("get", f"nodes/{node}/{node_type}/{vmid}/config") return data def wait_for_created(upid, timeout=300): """ Wait until a the vm has been created successfully """ start_time = time.time() info = _lookup_proxmox_task(upid) if not info: log.error( "wait_for_created: No task information retrieved based on given criteria." ) raise SaltCloudExecutionFailure while True: if "status" in info and info["status"] == "OK": log.debug("Host has been created!") return True time.sleep(3) # Little more patience, we're not in a hurry if time.time() - start_time > timeout: log.debug("Timeout reached while waiting for host to be created") return False info = _lookup_proxmox_task(upid) def wait_for_state(vmid, state, timeout=300): """ Wait until a specific state has been reached on a node """ start_time = time.time() node = get_vm_status(vmid=vmid) if not node: log.error("wait_for_state: No VM retrieved based on given criteria.") raise SaltCloudExecutionFailure while True: if node["status"] == state: log.debug('Host %s is now in "%s" state!', node["name"], state) return True time.sleep(1) if time.time() - start_time > timeout: log.debug( "Timeout reached while waiting for %s to become %s", node["name"], state ) return False node = get_vm_status(vmid=vmid) log.debug( 'State for %s is: "%s" instead of "%s"', node["name"], node["status"], state ) def destroy(name, call=None): """ Destroy a node. CLI Example: .. code-block:: bash salt-cloud --destroy mymachine """ 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"], ) vmobj = _get_vm_by_name(name) if vmobj is not None: # stop the vm if get_vm_status(vmid=vmobj["vmid"])["status"] != "stopped": stop(name, vmobj["vmid"], "action") # wait until stopped if not wait_for_state(vmobj["vmid"], "stopped"): return {"Error": f"Unable to stop {name}, command timed out"} # required to wait a bit here, otherwise the VM is sometimes # still locked and destroy fails. time.sleep(3) query("delete", "nodes/{}/{}".format(vmobj["node"], vmobj["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 {"Destroyed": f"{name} was destroyed."} def set_vm_status(status, name=None, vmid=None): """ Convenience function for setting VM status """ log.debug("Set status to %s for %s (%s)", status, name, vmid) if vmid is not None: log.debug("set_vm_status: via ID - VMID %s (%s): %s", vmid, name, status) vmobj = _get_vm_by_id(vmid) else: log.debug("set_vm_status: via name - VMID %s (%s): %s", vmid, name, status) vmobj = _get_vm_by_name(name) if not vmobj or "node" not in vmobj or "type" not in vmobj or "vmid" not in vmobj: log.error("Unable to set status %s for %s (%s)", status, name, vmid) raise SaltCloudExecutionTimeout log.debug("VM_STATUS: Has desired info (%s). Setting status..", vmobj) data = query( "post", "nodes/{}/{}/{}/status/{}".format( vmobj["node"], vmobj["type"], vmobj["vmid"], status ), ) result = _parse_proxmox_upid(data, vmobj) if result is not False and result is not None: log.debug("Set_vm_status action result: %s", result) return True return False def get_vm_status(vmid=None, name=None): """ Get the status for a VM, either via the ID or the hostname """ if vmid is not None: log.debug("get_vm_status: VMID %s", vmid) vmobj = _get_vm_by_id(vmid) elif name is not None: log.debug("get_vm_status: name %s", name) vmobj = _get_vm_by_name(name) else: log.debug("get_vm_status: No ID or NAME given") raise SaltCloudExecutionFailure log.debug("VM found: %s", vmobj) if vmobj is not None and "node" in vmobj: log.debug("VM_STATUS: Has desired info. Retrieving.. (%s)", vmobj["name"]) data = query( "get", "nodes/{}/{}/{}/status/current".format( vmobj["node"], vmobj["type"], vmobj["vmid"] ), ) return data log.error("VM or requested status not found..") return False def start(name, vmid=None, call=None): """ Start a node. CLI Example: .. code-block:: bash salt-cloud -a start mymachine """ if call != "action": raise SaltCloudSystemExit( "The start action must be called with -a or --action." ) log.debug("Start: %s (%s) = Start", name, vmid) if not set_vm_status("start", name, vmid=vmid): log.error("Unable to bring VM %s (%s) up..", name, vmid) raise SaltCloudExecutionFailure # xxx: TBD: Check here whether the status was actually changed to 'started' return {"Started": f"{name} was started."} def stop(name, vmid=None, call=None): """ Stop a node ("pulling the plug"). CLI Example: .. code-block:: bash salt-cloud -a stop mymachine """ if call != "action": raise SaltCloudSystemExit("The stop action must be called with -a or --action.") if not set_vm_status("stop", name, vmid=vmid): log.error("Unable to bring VM %s (%s) down..", name, vmid) raise SaltCloudExecutionFailure # xxx: TBD: Check here whether the status was actually changed to 'stopped' return {"Stopped": f"{name} was stopped."} def shutdown(name=None, vmid=None, call=None): """ Shutdown a node via ACPI. CLI Example: .. code-block:: bash salt-cloud -a shutdown mymachine """ if call != "action": raise SaltCloudSystemExit( "The shutdown action must be called with -a or --action." ) if not set_vm_status("shutdown", name, vmid=vmid): log.error("Unable to shut VM %s (%s) down..", name, vmid) raise SaltCloudExecutionFailure # xxx: TBD: Check here whether the status was actually changed to 'stopped' return {"Shutdown": f"{name} was shutdown."}