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/libvirt.py
""" Libvirt Cloud Module ==================== Example provider: .. code-block:: yaml # A provider maps to a libvirt instance my-libvirt-config: driver: libvirt # url: "qemu+ssh://user@remotekvm/system?socket=/var/run/libvirt/libvirt-sock" url: qemu:///system Example profile: .. code-block:: yaml base-itest: # points back at provider configuration e.g. the libvirt daemon to talk to provider: my-libvirt-config base_domain: base-image # ip_source = [ ip-learning | qemu-agent ] ip_source: ip-learning # clone_strategy = [ quick | full ] clone_strategy: quick ssh_username: vagrant # has_ssh_agent: True password: vagrant # if /tmp is mounted noexec do workaround deploy_command: sh /tmp/.saltcloud/deploy.sh # -F makes the bootstrap script overwrite existing config # which make reprovisioning a box work script_args: -F grains: sushi: more tasty # point at the another master at another port minion: master: 192.168.16.1 master_port: 5506 Tested on: - Fedora 26 (libvirt 3.2.1, qemu 2.9.1) - Fedora 25 (libvirt 1.3.3.2, qemu 2.6.1) - Fedora 23 (libvirt 1.2.18, qemu 2.4.1) - Centos 7 (libvirt 1.2.17, qemu 1.5.3) """ # TODO: look at event descriptions here: # https://docs.saltproject.io/en/latest/topics/cloud/reactor.html # TODO: support reboot? salt-cloud -a reboot vm1 vm2 vm2 # TODO: by using metadata tags in the libvirt XML we could make provider only # manage domains that we actually created import logging import os import uuid from xml.etree import ElementTree import salt.config as config import salt.utils.cloud from salt.exceptions import ( SaltCloudConfigError, SaltCloudExecutionFailure, SaltCloudNotFound, SaltCloudSystemExit, ) try: import libvirt # pylint: disable=import-error # pylint: disable=no-name-in-module from libvirt import libvirtError # pylint: enable=no-name-in-module HAS_LIBVIRT = True except ImportError: HAS_LIBVIRT = False VIRT_STATE_NAME_MAP = { 0: "running", 1: "running", 2: "running", 3: "paused", 4: "shutdown", 5: "shutdown", 6: "crashed", } IP_LEARNING_XML = """<filterref filter='clean-traffic'> <parameter name='CTRL_IP_LEARNING' value='any'/> </filterref>""" __virtualname__ = "libvirt" # Set up logging log = logging.getLogger(__name__) def libvirt_error_handler(ctx, error): # pylint: disable=unused-argument """ Redirect stderr prints from libvirt to salt logging. """ log.debug("libvirt error %s", error) if HAS_LIBVIRT: libvirt.registerErrorHandler(f=libvirt_error_handler, ctx=None) def __virtual__(): """ This function determines whether or not to make this cloud module available upon execution. Most often, it uses get_configured_provider() to determine if the necessary configuration has been set up. It may also check for necessary imports decide whether to load the module. In most cases, it will return a True or False value. If the name of the driver used does not match the filename, then that name should be returned instead of True. @return True|False|str """ if not HAS_LIBVIRT: return False, "Unable to locate or import python libvirt library." if get_configured_provider() is False: return False, "The 'libvirt' provider is not configured." 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__, ("url",) ) def __get_conn(url): # This has only been tested on kvm and xen, it needs to be expanded to # support all vm layers supported by libvirt try: conn = libvirt.open(url) except Exception: # pylint: disable=broad-except raise SaltCloudExecutionFailure( "Sorry, {} failed to open a connection to the hypervisor " "software at {}".format(__grains__["fqdn"], url) ) return conn def list_nodes(call=None): """ Return a list of the VMs id (str) image (str) size (str) state (str) private_ips (list) public_ips (list) """ if call == "action": raise SaltCloudSystemExit( "The list_nodes function must be called with -f or --function." ) providers = __opts__.get("providers", {}) ret = {} providers_to_check = [ _f for _f in [cfg.get("libvirt") for cfg in providers.values()] if _f ] for provider in providers_to_check: conn = __get_conn(provider["url"]) domains = conn.listAllDomains() for domain in domains: data = { "id": domain.UUIDString(), "image": "", "size": "", "state": VIRT_STATE_NAME_MAP[domain.state()[0]], "private_ips": [], "public_ips": get_domain_ips( domain, libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE ), } # TODO: Annoyingly name is not guaranteed to be unique, but the id will not work in other places ret[domain.name()] = data return ret def list_nodes_full(call=None): """ Because this module is not specific to any cloud providers, there will be no nodes to list. """ if call == "action": raise SaltCloudSystemExit( "The list_nodes_full function must be called with -f or --function." ) return list_nodes(call) def list_nodes_select(call=None): """ Return a list of the VMs that are on the provider, with select fields """ if call == "action": raise SaltCloudSystemExit( "The list_nodes_select function must be called with -f or --function." ) selection = __opts__.get("query.selection") if not selection: raise SaltCloudSystemExit("query.selection not found in /etc/salt/cloud") # TODO: somewhat doubt the implementation of cloud.list_nodes_select return salt.utils.cloud.list_nodes_select( list_nodes_full(), selection, call, ) def to_ip_addr_type(addr_type): if addr_type == libvirt.VIR_IP_ADDR_TYPE_IPV4: return "ipv4" elif addr_type == libvirt.VIR_IP_ADDR_TYPE_IPV6: return "ipv6" def get_domain_ips(domain, ip_source): ips = [] state = domain.state(0) if state[0] != libvirt.VIR_DOMAIN_RUNNING: return ips try: addresses = domain.interfaceAddresses(ip_source, 0) except libvirt.libvirtError as error: log.info("Exception polling address %s", error) return ips for name, val in addresses.items(): if val["addrs"]: for addr in val["addrs"]: tp = to_ip_addr_type(addr["type"]) log.info("Found address %s", addr) if tp == "ipv4": ips.append(addr["addr"]) return ips def get_domain_ip(domain, idx, ip_source, skip_loopback=True): ips = get_domain_ips(domain, ip_source) if skip_loopback: ips = [ip for ip in ips if not ip.startswith("127.")] if not ips or len(ips) <= idx: return None return ips[idx] def create(vm_): """ Provision a single machine """ clone_strategy = vm_.get("clone_strategy") or "full" if clone_strategy not in ("quick", "full"): raise SaltCloudSystemExit( "'clone_strategy' must be one of quick or full. Got '{}'".format( clone_strategy ) ) ip_source = vm_.get("ip_source") or "ip-learning" if ip_source not in ("ip-learning", "qemu-agent"): raise SaltCloudSystemExit( "'ip_source' must be one of qemu-agent or ip-learning. Got '{}'".format( ip_source ) ) validate_xml = ( vm_.get("validate_xml") if vm_.get("validate_xml") is not None else True ) log.info( "Cloning '%s' with strategy '%s' validate_xml='%s'", vm_["name"], clone_strategy, validate_xml, ) 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 "libvirt", vm_["profile"] ) is False ): return False except AttributeError: pass # TODO: check name qemu/libvirt will choke on some characters (like '/')? name = vm_["name"] __utils__["cloud.fire_event"]( "event", "starting create", f"salt/cloud/{name}/creating", args=__utils__["cloud.filter_event"]( "creating", vm_, ["name", "profile", "provider", "driver"] ), sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) key_filename = config.get_cloud_config_value( "private_key", vm_, __opts__, search_global=False, default=None ) if key_filename is not None and not os.path.isfile(key_filename): raise SaltCloudConfigError( f"The defined key_filename '{key_filename}' does not exist" ) vm_["key_filename"] = key_filename # wait_for_instance requires private_key vm_["private_key"] = key_filename cleanup = [] try: # clone the vm base = vm_["base_domain"] conn = __get_conn(vm_["url"]) try: # for idempotency the salt-bootstrap needs -F argument # script_args: -F clone_domain = conn.lookupByName(name) except libvirtError as e: domain = conn.lookupByName(base) # TODO: ensure base is shut down before cloning xml = domain.XMLDesc(0) kwargs = { "name": name, "base_domain": base, } __utils__["cloud.fire_event"]( "event", "requesting instance", f"salt/cloud/{name}/requesting", args={ "kwargs": __utils__["cloud.filter_event"]( "requesting", kwargs, list(kwargs) ), }, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) log.debug("Source machine XML '%s'", xml) domain_xml = ElementTree.fromstring(xml) domain_xml.find("./name").text = name if domain_xml.find("./description") is None: description_elem = ElementTree.Element("description") domain_xml.insert(0, description_elem) description = domain_xml.find("./description") description.text = f"Cloned from {base}" domain_xml.remove(domain_xml.find("./uuid")) for iface_xml in domain_xml.findall("./devices/interface"): iface_xml.remove(iface_xml.find("./mac")) # enable IP learning, this might be a default behaviour... # Don't always enable since it can cause problems through libvirt-4.5 if ( ip_source == "ip-learning" and iface_xml.find( "./filterref/parameter[@name='CTRL_IP_LEARNING']" ) is None ): iface_xml.append(ElementTree.fromstring(IP_LEARNING_XML)) # If a qemu agent is defined we need to fix the path to its socket # <channel type='unix'> # <source mode='bind' path='/var/lib/libvirt/qemu/channel/target/domain-<dom-name>/org.qemu.guest_agent.0'/> # <target type='virtio' name='org.qemu.guest_agent.0'/> # <address type='virtio-serial' controller='0' bus='0' port='2'/> # </channel> for agent_xml in domain_xml.findall("""./devices/channel[@type='unix']"""): # is org.qemu.guest_agent.0 an option? if ( agent_xml.find( """./target[@type='virtio'][@name='org.qemu.guest_agent.0']""" ) is not None ): source_element = agent_xml.find("""./source[@mode='bind']""") # see if there is a path element that needs rewriting if source_element and "path" in source_element.attrib: path = source_element.attrib["path"] new_path = path.replace(f"/domain-{base}/", f"/domain-{name}/") log.debug("Rewriting agent socket path to %s", new_path) source_element.attrib["path"] = new_path for disk in domain_xml.findall( """./devices/disk[@device='disk'][@type='file']""" ): # print "Disk: ", ElementTree.tostring(disk) # check if we can clone driver = disk.find("./driver[@name='qemu']") if driver is None: # Err on the safe side raise SaltCloudExecutionFailure( "Non qemu driver disk encountered bailing out." ) disk_type = driver.attrib.get("type") log.info("disk attributes %s", disk.attrib) if disk_type == "qcow2": source = disk.find("./source").attrib["file"] pool, volume = find_pool_and_volume(conn, source) if clone_strategy == "quick": new_volume = pool.createXML( create_volume_with_backing_store_xml(volume), 0 ) else: new_volume = pool.createXMLFrom( create_volume_xml(volume), volume, 0 ) cleanup.append({"what": "volume", "item": new_volume}) disk.find("./source").attrib["file"] = new_volume.path() elif disk_type == "raw": source = disk.find("./source").attrib["file"] pool, volume = find_pool_and_volume(conn, source) # TODO: more control on the cloned disk type new_volume = pool.createXMLFrom( create_volume_xml(volume), volume, 0 ) cleanup.append({"what": "volume", "item": new_volume}) disk.find("./source").attrib["file"] = new_volume.path() else: raise SaltCloudExecutionFailure( f"Disk type '{disk_type}' not supported" ) clone_xml = salt.utils.stringutils.to_str(ElementTree.tostring(domain_xml)) log.debug("Clone XML '%s'", clone_xml) validate_flags = libvirt.VIR_DOMAIN_DEFINE_VALIDATE if validate_xml else 0 clone_domain = conn.defineXMLFlags(clone_xml, validate_flags) cleanup.append({"what": "domain", "item": clone_domain}) clone_domain.createWithFlags(libvirt.VIR_DOMAIN_START_FORCE_BOOT) log.debug("VM '%s'", vm_) if ip_source == "qemu-agent": ip_source = libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT elif ip_source == "ip-learning": ip_source = libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE address = salt.utils.cloud.wait_for_ip( get_domain_ip, update_args=(clone_domain, 0, ip_source), timeout=config.get_cloud_config_value( "wait_for_ip_timeout", vm_, __opts__, default=10 * 60 ), interval=config.get_cloud_config_value( "wait_for_ip_interval", vm_, __opts__, default=10 ), interval_multiplier=config.get_cloud_config_value( "wait_for_ip_interval_multiplier", vm_, __opts__, default=1 ), ) log.info("Address = %s", address) vm_["ssh_host"] = address # the bootstrap script needs to be installed first in /etc/salt/cloud.deploy.d/ # salt-cloud -u is your friend ret = __utils__["cloud.bootstrap"](vm_, __opts__) __utils__["cloud.fire_event"]( "event", "created instance", f"salt/cloud/{name}/created", args=__utils__["cloud.filter_event"]( "created", vm_, ["name", "profile", "provider", "driver"] ), sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) return ret except Exception: # pylint: disable=broad-except do_cleanup(cleanup) # throw the root cause after cleanup raise def do_cleanup(cleanup): """ Clean up clone domain leftovers as much as possible. Extra robust clean up in order to deal with some small changes in libvirt behavior over time. Passed in volumes and domains are deleted, any errors are ignored. Used when cloning/provisioning a domain fails. :param cleanup: list containing dictionaries with two keys: 'what' and 'item'. If 'what' is domain the 'item' is a libvirt domain object. If 'what' is volume then the item is a libvirt volume object. Returns: none .. versionadded:: 2017.7.3 """ log.info("Cleaning up after exception") for leftover in cleanup: what = leftover["what"] item = leftover["item"] if what == "domain": log.info("Cleaning up %s %s", what, item.name()) try: item.destroy() log.debug("%s %s forced off", what, item.name()) except libvirtError: pass try: item.undefineFlags( libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE + libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA + libvirt.VIR_DOMAIN_UNDEFINE_NVRAM ) log.debug("%s %s undefined", what, item.name()) except libvirtError: pass if what == "volume": try: item.delete() log.debug("%s %s cleaned up", what, item.name()) except libvirtError: pass def destroy(name, call=None): """ This function irreversibly destroys a virtual machine on the cloud provider. Before doing so, it should fire an event on the Salt event bus. The tag for this event is `salt/cloud/<vm name>/destroying`. Once the virtual machine has been destroyed, another event is fired. The tag for that event is `salt/cloud/<vm name>/destroyed`. Dependencies: list_nodes @param name: @type name: str @param call: @type call: @return: True if all went well, otherwise an error message @rtype: bool|str """ log.info("Attempting to delete instance %s", name) if call == "function": raise SaltCloudSystemExit( "The destroy action must be called with -d, --destroy, -a or --action." ) found = [] providers = __opts__.get("providers", {}) providers_to_check = [ _f for _f in [cfg.get("libvirt") for cfg in providers.values()] if _f ] for provider in providers_to_check: conn = __get_conn(provider["url"]) log.info("looking at %s", provider["url"]) try: domain = conn.lookupByName(name) found.append({"domain": domain, "conn": conn}) except libvirtError: pass if not found: return f"{name} doesn't exist and can't be deleted" if len(found) > 1: return f"{name} doesn't identify a unique machine leaving things" __utils__["cloud.fire_event"]( "event", "destroying instance", f"salt/cloud/{name}/destroying", args={"name": name}, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) destroy_domain(found[0]["conn"], found[0]["domain"]) __utils__["cloud.fire_event"]( "event", "destroyed instance", f"salt/cloud/{name}/destroyed", args={"name": name}, sock_dir=__opts__["sock_dir"], transport=__opts__["transport"], ) def destroy_domain(conn, domain): log.info("Destroying domain %s", domain.name()) try: domain.destroy() except libvirtError: pass volumes = get_domain_volumes(conn, domain) for volume in volumes: log.debug("Removing volume %s", volume.name()) volume.delete() log.debug("Undefining domain %s", domain.name()) domain.undefineFlags( libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE + libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA + libvirt.VIR_DOMAIN_UNDEFINE_NVRAM ) def create_volume_xml(volume): template = """<volume> <name>n</name> <capacity>c</capacity> <allocation>0</allocation> <target> <path>p</path> <format type='qcow2'/> <compat>1.1</compat> </target> </volume> """ volume_xml = ElementTree.fromstring(template) # TODO: generate name volume_xml.find("name").text = generate_new_name(volume.name()) log.debug("Volume: %s", dir(volume)) volume_xml.find("capacity").text = str(volume.info()[1]) volume_xml.find("./target/path").text = volume.path() xml_string = salt.utils.stringutils.to_str(ElementTree.tostring(volume_xml)) log.debug("Creating %s", xml_string) return xml_string def create_volume_with_backing_store_xml(volume): template = """<volume> <name>n</name> <capacity>c</capacity> <allocation>0</allocation> <target> <format type='qcow2'/> <compat>1.1</compat> </target> <backingStore> <format type='qcow2'/> <path>p</path> </backingStore> </volume> """ volume_xml = ElementTree.fromstring(template) # TODO: generate name volume_xml.find("name").text = generate_new_name(volume.name()) log.debug("volume: %s", dir(volume)) volume_xml.find("capacity").text = str(volume.info()[1]) volume_xml.find("./backingStore/path").text = volume.path() xml_string = salt.utils.stringutils.to_str(ElementTree.tostring(volume_xml)) log.debug("Creating %s", xml_string) return xml_string def find_pool_and_volume(conn, path): # active and persistent storage pools # TODO: should we filter on type? for sp in conn.listAllStoragePools(2 + 4): for v in sp.listAllVolumes(): if v.path() == path: return sp, v raise SaltCloudNotFound(f"Could not find volume for path {path}") def generate_new_name(orig_name): if "." not in orig_name: return f"{orig_name}-{uuid.uuid1()}" name, ext = orig_name.rsplit(".", 1) return f"{name}-{uuid.uuid1()}.{ext}" def get_domain_volumes(conn, domain): volumes = [] xml = ElementTree.fromstring(domain.XMLDesc(0)) for disk in xml.findall("""./devices/disk[@device='disk'][@type='file']"""): if disk.find("./driver[@name='qemu'][@type='qcow2']") is not None: source = disk.find("./source").attrib["file"] try: pool, volume = find_pool_and_volume(conn, source) volumes.append(volume) except libvirtError: log.warning("Disk not found '%s'", source) return volumes