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/spm
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/spm/__init__.py
""" This module provides the point of entry to SPM, the Salt Package Manager .. versionadded:: 2015.8.0 """ import hashlib import logging import os import shutil import sys import tarfile import salt.cache import salt.client import salt.config import salt.loader import salt.syspaths as syspaths import salt.utils.files import salt.utils.http as http import salt.utils.path import salt.utils.platform import salt.utils.win_functions import salt.utils.yaml from salt.template import compile_template try: import grp import pwd except ImportError: pass log = logging.getLogger(__name__) FILE_TYPES = ("c", "d", "g", "l", "r", "s", "m") # c: config file # d: documentation file # g: ghost file (i.e. the file contents are not included in the package payload) # l: license file # r: readme file # s: SLS file # m: Salt module class SPMException(Exception): """ Base class for SPMClient exceptions """ class SPMInvocationError(SPMException): """ Wrong number of arguments or other usage error """ class SPMPackageError(SPMException): """ Problem with package file or package installation """ class SPMDatabaseError(SPMException): """ SPM database not found, etc """ class SPMOperationCanceled(SPMException): """ SPM install or uninstall was canceled """ class SPMClient: """ Provide an SPM Client """ def __init__(self, ui, opts=None): # pylint: disable=W0231 self.ui = ui if not opts: opts = salt.config.spm_config(os.path.join(syspaths.CONFIG_DIR, "spm")) self.opts = opts self.db_prov = self.opts.get("spm_db_provider", "sqlite3") self.files_prov = self.opts.get("spm_files_provider", "local") self._prep_pkgdb() self._prep_pkgfiles() self.db_conn = None self.files_conn = None self._init() def _prep_pkgdb(self): self.pkgdb = salt.loader.pkgdb(self.opts) def _prep_pkgfiles(self): self.pkgfiles = salt.loader.pkgfiles(self.opts) def _init(self): if not self.db_conn: self.db_conn = self._pkgdb_fun("init") if not self.files_conn: self.files_conn = self._pkgfiles_fun("init") def _close(self): if self.db_conn: self.db_conn.close() def run(self, args): """ Run the SPM command """ command = args[0] try: if command == "install": self._install(args) elif command == "local": self._local(args) elif command == "repo": self._repo(args) elif command == "remove": self._remove(args) elif command == "build": self._build(args) elif command == "update_repo": self._download_repo_metadata(args) elif command == "create_repo": self._create_repo(args) elif command == "files": self._list_files(args) elif command == "info": self._info(args) elif command == "list": self._list(args) elif command == "close": self._close() else: raise SPMInvocationError(f"Invalid command '{command}'") except SPMException as exc: self.ui.error(str(exc)) def _pkgdb_fun(self, func, *args, **kwargs): try: return getattr(getattr(self.pkgdb, self.db_prov), func)(*args, **kwargs) except AttributeError: return self.pkgdb[f"{self.db_prov}.{func}"](*args, **kwargs) def _pkgfiles_fun(self, func, *args, **kwargs): try: return getattr(getattr(self.pkgfiles, self.files_prov), func)( *args, **kwargs ) except AttributeError: return self.pkgfiles[f"{self.files_prov}.{func}"](*args, **kwargs) def _list(self, args): """ Process local commands """ args.pop(0) command = args[0] if command == "packages": self._list_packages(args) elif command == "files": self._list_files(args) elif command == "repos": self._repo_list(args) else: raise SPMInvocationError(f"Invalid list command '{command}'") def _local(self, args): """ Process local commands """ args.pop(0) command = args[0] if command == "install": self._local_install(args) elif command == "files": self._local_list_files(args) elif command == "info": self._local_info(args) else: raise SPMInvocationError(f"Invalid local command '{command}'") def _repo(self, args): """ Process repo commands """ args.pop(0) command = args[0] if command == "list": self._repo_list(args) elif command == "packages": self._repo_packages(args) elif command == "search": self._repo_packages(args, search=True) elif command == "update": self._download_repo_metadata(args) elif command == "create": self._create_repo(args) else: raise SPMInvocationError(f"Invalid repo command '{command}'") def _repo_packages(self, args, search=False): """ List packages for one or more configured repos """ packages = [] repo_metadata = self._get_repo_metadata() for repo in repo_metadata: for pkg in repo_metadata[repo]["packages"]: if args[1] in pkg: version = repo_metadata[repo]["packages"][pkg]["info"]["version"] release = repo_metadata[repo]["packages"][pkg]["info"]["release"] packages.append((pkg, version, release, repo)) for pkg in sorted(packages): self.ui.status(f"{pkg[0]}\t{pkg[1]}-{pkg[2]}\t{pkg[3]}") return packages def _repo_list(self, args): """ List configured repos This can be called either as a ``repo`` command or a ``list`` command """ repo_metadata = self._get_repo_metadata() for repo in repo_metadata: self.ui.status(repo) def _install(self, args): """ Install a package from a repo """ if len(args) < 2: raise SPMInvocationError("A package must be specified") caller_opts = self.opts.copy() caller_opts["file_client"] = "local" self.caller = salt.client.Caller(mopts=caller_opts) self.client = salt.client.get_local_client(self.opts["conf_file"]) cache = salt.cache.Cache(self.opts) packages = args[1:] file_map = {} optional = [] recommended = [] to_install = [] for pkg in packages: if pkg.endswith(".spm"): if self._pkgfiles_fun("path_exists", pkg): comps = pkg.split("-") comps = os.path.split("-".join(comps[:-2])) pkg_name = comps[-1] formula_tar = tarfile.open(pkg, "r:bz2") formula_ref = formula_tar.extractfile(f"{pkg_name}/FORMULA") formula_def = salt.utils.yaml.safe_load(formula_ref) file_map[pkg_name] = pkg to_, op_, re_ = self._check_all_deps( pkg_name=pkg_name, pkg_file=pkg, formula_def=formula_def ) to_install.extend(to_) optional.extend(op_) recommended.extend(re_) formula_tar.close() else: raise SPMInvocationError(f"Package file {pkg} not found") else: to_, op_, re_ = self._check_all_deps(pkg_name=pkg) to_install.extend(to_) optional.extend(op_) recommended.extend(re_) optional = set(filter(len, optional)) if optional: self.ui.status( "The following dependencies are optional:\n\t{}\n".format( "\n\t".join(optional) ) ) recommended = set(filter(len, recommended)) if recommended: self.ui.status( "The following dependencies are recommended:\n\t{}\n".format( "\n\t".join(recommended) ) ) to_install = set(filter(len, to_install)) msg = "Installing packages:\n\t{}\n".format("\n\t".join(to_install)) if not self.opts["assume_yes"]: self.ui.confirm(msg) repo_metadata = self._get_repo_metadata() dl_list = {} for package in to_install: if package in file_map: self._install_indv_pkg(package, file_map[package]) else: for repo in repo_metadata: repo_info = repo_metadata[repo] if package in repo_info["packages"]: dl_package = False repo_ver = repo_info["packages"][package]["info"]["version"] repo_rel = repo_info["packages"][package]["info"]["release"] repo_url = repo_info["info"]["url"] if package in dl_list: # Check package version, replace if newer version if repo_ver == dl_list[package]["version"]: # Version is the same, check release if repo_rel > dl_list[package]["release"]: dl_package = True elif repo_rel == dl_list[package]["release"]: # Version and release are the same, give # preference to local (file://) repos if dl_list[package]["source"].startswith("file://"): if not repo_url.startswith("file://"): dl_package = True elif repo_ver > dl_list[package]["version"]: dl_package = True else: dl_package = True if dl_package is True: # Put together download directory cache_path = os.path.join(self.opts["spm_cache_dir"], repo) # Put together download paths dl_url = "{}/{}".format( repo_info["info"]["url"], repo_info["packages"][package]["filename"], ) out_file = os.path.join( cache_path, repo_info["packages"][package]["filename"] ) dl_list[package] = { "version": repo_ver, "release": repo_rel, "source": dl_url, "dest_dir": cache_path, "dest_file": out_file, } for package in dl_list: dl_url = dl_list[package]["source"] cache_path = dl_list[package]["dest_dir"] out_file = dl_list[package]["dest_file"] # Make sure download directory exists if not os.path.exists(cache_path): os.makedirs(cache_path) # Download the package if dl_url.startswith("file://"): dl_url = dl_url.replace("file://", "") shutil.copyfile(dl_url, out_file) else: with salt.utils.files.fopen(out_file, "wb") as outf: outf.write( self._query_http(dl_url, repo_info["info"], decode_body=False) ) # First we download everything, then we install for package in dl_list: out_file = dl_list[package]["dest_file"] # Kick off the install self._install_indv_pkg(package, out_file) return def _local_install(self, args, pkg_name=None): """ Install a package from a file """ if len(args) < 2: raise SPMInvocationError("A package file must be specified") self._install(args) def _check_all_deps(self, pkg_name=None, pkg_file=None, formula_def=None): """ Starting with one package, check all packages for dependencies """ if pkg_file and not os.path.exists(pkg_file): raise SPMInvocationError(f"Package file {pkg_file} not found") self.repo_metadata = self._get_repo_metadata() if not formula_def: for repo in self.repo_metadata: if not isinstance(self.repo_metadata[repo]["packages"], dict): continue if pkg_name in self.repo_metadata[repo]["packages"]: formula_def = self.repo_metadata[repo]["packages"][pkg_name]["info"] if not formula_def: raise SPMInvocationError(f"Unable to read formula for {pkg_name}") # Check to see if the package is already installed pkg_info = self._pkgdb_fun("info", pkg_name, self.db_conn) pkgs_to_install = [] if pkg_info is None or self.opts["force"]: pkgs_to_install.append(pkg_name) elif pkg_info is not None and not self.opts["force"]: raise SPMPackageError( "Package {} already installed, not installing again".format( formula_def["name"] ) ) optional_install = [] recommended_install = [] if ( "dependencies" in formula_def or "optional" in formula_def or "recommended" in formula_def ): self.avail_pkgs = {} for repo in self.repo_metadata: if not isinstance(self.repo_metadata[repo]["packages"], dict): continue for pkg in self.repo_metadata[repo]["packages"]: self.avail_pkgs[pkg] = repo needs, unavail, optional, recommended = self._resolve_deps(formula_def) if len(unavail) > 0: raise SPMPackageError( "Cannot install {}, the following dependencies are needed:\n\n{}".format( formula_def["name"], "\n".join(unavail) ) ) if optional: optional_install.extend(optional) for dep_pkg in optional: pkg_info = self._pkgdb_fun("info", formula_def["name"]) msg = dep_pkg if isinstance(pkg_info, dict): msg = f"{dep_pkg} [Installed]" optional_install.append(msg) if recommended: recommended_install.extend(recommended) for dep_pkg in recommended: pkg_info = self._pkgdb_fun("info", formula_def["name"]) msg = dep_pkg if isinstance(pkg_info, dict): msg = f"{dep_pkg} [Installed]" recommended_install.append(msg) if needs: pkgs_to_install.extend(needs) for dep_pkg in needs: pkg_info = self._pkgdb_fun("info", formula_def["name"]) msg = dep_pkg if isinstance(pkg_info, dict): msg = f"{dep_pkg} [Installed]" return pkgs_to_install, optional_install, recommended_install def _install_indv_pkg(self, pkg_name, pkg_file): """ Install one individual package """ self.ui.status(f"... installing {pkg_name}") formula_tar = tarfile.open(pkg_file, "r:bz2") formula_ref = formula_tar.extractfile(f"{pkg_name}/FORMULA") formula_def = salt.utils.yaml.safe_load(formula_ref) for field in ("version", "release", "summary", "description"): if field not in formula_def: raise SPMPackageError(f"Invalid package: the {field} was not found") pkg_files = formula_tar.getmembers() # First pass: check for files that already exist existing_files = self._pkgfiles_fun( "check_existing", pkg_name, pkg_files, formula_def ) if existing_files and not self.opts["force"]: raise SPMPackageError( "Not installing {} due to existing files:\n\n{}".format( pkg_name, "\n".join(existing_files) ) ) # We've decided to install self._pkgdb_fun("register_pkg", pkg_name, formula_def, self.db_conn) # Run the pre_local_state script, if present if "pre_local_state" in formula_def: high_data = self._render(formula_def["pre_local_state"], formula_def) ret = self.caller.cmd("state.high", data=high_data) if "pre_tgt_state" in formula_def: log.debug("Executing pre_tgt_state script") high_data = self._render(formula_def["pre_tgt_state"]["data"], formula_def) tgt = formula_def["pre_tgt_state"]["tgt"] ret = self.client.run_job( tgt=formula_def["pre_tgt_state"]["tgt"], fun="state.high", tgt_type=formula_def["pre_tgt_state"].get("tgt_type", "glob"), timout=self.opts["timeout"], data=high_data, ) # No defaults for this in config.py; default to the current running # user and group if salt.utils.platform.is_windows(): uname = gname = salt.utils.win_functions.get_current_user() uname_sid = salt.utils.win_functions.get_sid_from_name(uname) uid = self.opts.get("spm_uid", uname_sid) gid = self.opts.get("spm_gid", uname_sid) else: uid = self.opts.get("spm_uid", os.getuid()) gid = self.opts.get("spm_gid", os.getgid()) uname = pwd.getpwuid(uid)[0] gname = grp.getgrgid(gid)[0] # Second pass: install the files for member in pkg_files: member.uid = uid member.gid = gid member.uname = uname member.gname = gname out_path = self._pkgfiles_fun( "install_file", pkg_name, formula_tar, member, formula_def, self.files_conn, ) if out_path is not False: if member.isdir(): digest = "" else: self._verbose( f"Installing file {member.name} to {out_path}", log.trace, ) file_hash = hashlib.sha1() digest = self._pkgfiles_fun( "hash_file", os.path.join(out_path, member.name), file_hash, self.files_conn, ) self._pkgdb_fun( "register_file", pkg_name, member, out_path, digest, self.db_conn ) # Run the post_local_state script, if present if "post_local_state" in formula_def: log.debug("Executing post_local_state script") high_data = self._render(formula_def["post_local_state"], formula_def) self.caller.cmd("state.high", data=high_data) if "post_tgt_state" in formula_def: log.debug("Executing post_tgt_state script") high_data = self._render(formula_def["post_tgt_state"]["data"], formula_def) tgt = formula_def["post_tgt_state"]["tgt"] ret = self.client.run_job( tgt=formula_def["post_tgt_state"]["tgt"], fun="state.high", tgt_type=formula_def["post_tgt_state"].get("tgt_type", "glob"), timout=self.opts["timeout"], data=high_data, ) formula_tar.close() def _resolve_deps(self, formula_def): """ Return a list of packages which need to be installed, to resolve all dependencies """ pkg_info = self.pkgdb[f"{self.db_prov}.info"](formula_def["name"]) if not isinstance(pkg_info, dict): pkg_info = {} can_has = {} cant_has = [] if "dependencies" in formula_def and formula_def["dependencies"] is None: formula_def["dependencies"] = "" for dep in formula_def.get("dependencies", "").split(","): dep = dep.strip() if not dep: continue if self.pkgdb[f"{self.db_prov}.info"](dep): continue if dep in self.avail_pkgs: can_has[dep] = self.avail_pkgs[dep] else: cant_has.append(dep) optional = formula_def.get("optional", "").split(",") recommended = formula_def.get("recommended", "").split(",") inspected = [] to_inspect = can_has.copy() while len(to_inspect) > 0: dep = next(iter(to_inspect.keys())) del to_inspect[dep] # Don't try to resolve the same package more than once if dep in inspected: continue inspected.append(dep) repo_contents = self.repo_metadata.get(can_has[dep], {}) repo_packages = repo_contents.get("packages", {}) dep_formula = repo_packages.get(dep, {}).get("info", {}) also_can, also_cant, opt_dep, rec_dep = self._resolve_deps(dep_formula) can_has.update(also_can) cant_has = sorted(set(cant_has + also_cant)) optional = sorted(set(optional + opt_dep)) recommended = sorted(set(recommended + rec_dep)) return can_has, cant_has, optional, recommended def _traverse_repos(self, callback, repo_name=None): """ Traverse through all repo files and apply the functionality provided in the callback to them """ repo_files = [] if os.path.exists(self.opts["spm_repos_config"]): repo_files.append(self.opts["spm_repos_config"]) for dirpath, dirnames, filenames in salt.utils.path.os_walk( "{}.d".format(self.opts["spm_repos_config"]) ): for repo_file in filenames: if not repo_file.endswith(".repo"): continue repo_files.append(repo_file) for repo_file in repo_files: repo_path = "{}.d/{}".format(self.opts["spm_repos_config"], repo_file) with salt.utils.files.fopen(repo_path) as rph: repo_data = salt.utils.yaml.safe_load(rph) for repo in repo_data: if repo_data[repo].get("enabled", True) is False: continue if repo_name is not None and repo != repo_name: continue callback(repo, repo_data[repo]) def _query_http(self, dl_path, repo_info, decode_body=True): """ Download files via http """ query = None response = None try: if "username" in repo_info: try: if "password" in repo_info: query = http.query( dl_path, text=True, username=repo_info["username"], password=repo_info["password"], decode_body=decode_body, ) else: raise SPMException( "Auth defined, but password is not set for username: '{}'".format( repo_info["username"] ) ) except SPMException as exc: self.ui.error(str(exc)) else: query = http.query(dl_path, text=True, decode_body=decode_body) except SPMException as exc: self.ui.error(str(exc)) try: if query: if "SPM-METADATA" in dl_path: response = salt.utils.yaml.safe_load(query.get("text", "{}")) else: response = query.get("text") else: raise SPMException("Response is empty, please check for Errors above.") except SPMException as exc: self.ui.error(str(exc)) return response def _download_repo_metadata(self, args): """ Connect to all repos and download metadata """ cache = salt.cache.Cache(self.opts, self.opts["spm_cache_dir"]) def _update_metadata(repo, repo_info): dl_path = "{}/SPM-METADATA".format(repo_info["url"]) if dl_path.startswith("file://"): dl_path = dl_path.replace("file://", "") with salt.utils.files.fopen(dl_path, "r") as rpm: metadata = salt.utils.yaml.safe_load(rpm) else: metadata = self._query_http(dl_path, repo_info) cache.store(".", repo, metadata) repo_name = args[1] if len(args) > 1 else None self._traverse_repos(_update_metadata, repo_name) def _get_repo_metadata(self): """ Return cached repo metadata """ cache = salt.cache.Cache(self.opts, self.opts["spm_cache_dir"]) metadata = {} def _read_metadata(repo, repo_info): if cache.updated(".", repo) is None: log.warning("Updating repo metadata") self._download_repo_metadata({}) metadata[repo] = { "info": repo_info, "packages": cache.fetch(".", repo), } self._traverse_repos(_read_metadata) return metadata def _create_repo(self, args): """ Scan a directory and create an SPM-METADATA file which describes all of the SPM files in that directory. """ if len(args) < 2: raise SPMInvocationError("A path to a directory must be specified") if args[1] == ".": repo_path = os.getcwd() else: repo_path = args[1] old_files = [] repo_metadata = {} for dirpath, dirnames, filenames in salt.utils.path.os_walk(repo_path): for spm_file in filenames: if not spm_file.endswith(".spm"): continue spm_path = f"{repo_path}/{spm_file}" if not tarfile.is_tarfile(spm_path): continue comps = spm_file.split("-") spm_name = "-".join(comps[:-2]) spm_fh = tarfile.open(spm_path, "r:bz2") formula_handle = spm_fh.extractfile(f"{spm_name}/FORMULA") formula_conf = salt.utils.yaml.safe_load(formula_handle.read()) use_formula = True if spm_name in repo_metadata: # This package is already in the repo; use the latest cur_info = repo_metadata[spm_name]["info"] new_info = formula_conf if int(new_info["version"]) == int(cur_info["version"]): # Version is the same, check release if int(new_info["release"]) < int(cur_info["release"]): # This is an old release; don't use it use_formula = False elif int(new_info["version"]) < int(cur_info["version"]): # This is an old version; don't use it use_formula = False if use_formula is True: # Ignore/archive/delete the old version log.debug( "%s %s-%s had been added, but %s-%s will replace it", spm_name, cur_info["version"], cur_info["release"], new_info["version"], new_info["release"], ) old_files.append(repo_metadata[spm_name]["filename"]) else: # Ignore/archive/delete the new version log.debug( "%s %s-%s has been found, but is older than %s-%s", spm_name, new_info["version"], new_info["release"], cur_info["version"], cur_info["release"], ) old_files.append(spm_file) if use_formula is True: log.debug( "adding %s-%s-%s to the repo", formula_conf["name"], formula_conf["version"], formula_conf["release"], ) repo_metadata[spm_name] = { "info": formula_conf.copy(), } repo_metadata[spm_name]["filename"] = spm_file metadata_filename = f"{repo_path}/SPM-METADATA" with salt.utils.files.fopen(metadata_filename, "w") as mfh: salt.utils.yaml.safe_dump( repo_metadata, mfh, indent=4, canonical=False, default_flow_style=False, ) log.debug("Wrote %s", metadata_filename) for file_ in old_files: if self.opts["spm_repo_dups"] == "ignore": # ignore old packages, but still only add the latest log.debug("%s will be left in the directory", file_) elif self.opts["spm_repo_dups"] == "archive": # spm_repo_archive_path is where old packages are moved if not os.path.exists("./archive"): try: os.makedirs("./archive") log.debug("%s has been archived", file_) except OSError: log.error("Unable to create archive directory") try: shutil.move(file_, "./archive") except OSError: log.error("Unable to archive %s", file_) elif self.opts["spm_repo_dups"] == "delete": # delete old packages from the repo try: os.remove(file_) log.debug("%s has been deleted", file_) except OSError: log.error("Unable to delete %s", file_) except OSError: # pylint: disable=duplicate-except # The file has already been deleted pass def _remove(self, args): """ Remove a package """ if len(args) < 2: raise SPMInvocationError("A package must be specified") packages = args[1:] msg = "Removing packages:\n\t{}".format("\n\t".join(packages)) if not self.opts["assume_yes"]: self.ui.confirm(msg) for package in packages: self.ui.status(f"... removing {package}") if not self._pkgdb_fun("db_exists", self.opts["spm_db"]): raise SPMDatabaseError( "No database at {}, cannot remove {}".format( self.opts["spm_db"], package ) ) # Look at local repo index pkg_info = self._pkgdb_fun("info", package, self.db_conn) if pkg_info is None: raise SPMInvocationError(f"Package {package} not installed") # Find files that have not changed and remove them files = self._pkgdb_fun("list_files", package, self.db_conn) dirs = [] for filerow in files: if self._pkgfiles_fun("path_isdir", filerow[0]): dirs.append(filerow[0]) continue file_hash = hashlib.sha1() digest = self._pkgfiles_fun( "hash_file", filerow[0], file_hash, self.files_conn ) if filerow[1] == digest: self._verbose(f"Removing file {filerow[0]}", log.trace) self._pkgfiles_fun("remove_file", filerow[0], self.files_conn) else: self._verbose(f"Not removing file {filerow[0]}", log.trace) self._pkgdb_fun("unregister_file", filerow[0], package, self.db_conn) # Clean up directories for dir_ in sorted(dirs, reverse=True): self._pkgdb_fun("unregister_file", dir_, package, self.db_conn) try: self._verbose(f"Removing directory {dir_}", log.trace) os.rmdir(dir_) except OSError: # Leave directories in place that still have files in them self._verbose( f"Cannot remove directory {dir_}, probably not empty", log.trace, ) self._pkgdb_fun("unregister_pkg", package, self.db_conn) def _verbose(self, msg, level=log.debug): """ Display verbose information """ if self.opts.get("verbose", False) is True: self.ui.status(msg) level(msg) def _local_info(self, args): """ List info for a package file """ if len(args) < 2: raise SPMInvocationError("A package filename must be specified") pkg_file = args[1] if not os.path.exists(pkg_file): raise SPMInvocationError(f"Package file {pkg_file} not found") comps = pkg_file.split("-") comps = "-".join(comps[:-2]).split("/") name = comps[-1] formula_tar = tarfile.open(pkg_file, "r:bz2") formula_ref = formula_tar.extractfile(f"{name}/FORMULA") formula_def = salt.utils.yaml.safe_load(formula_ref) self.ui.status(self._get_info(formula_def)) formula_tar.close() def _info(self, args): """ List info for a package """ if len(args) < 2: raise SPMInvocationError("A package must be specified") package = args[1] pkg_info = self._pkgdb_fun("info", package, self.db_conn) if pkg_info is None: raise SPMPackageError(f"package {package} not installed") self.ui.status(self._get_info(pkg_info)) def _get_info(self, formula_def): """ Get package info """ fields = ( "name", "os", "os_family", "release", "version", "dependencies", "os_dependencies", "os_family_dependencies", "summary", "description", ) for item in fields: if item not in formula_def: formula_def[item] = "None" if "installed" not in formula_def: formula_def["installed"] = "Not installed" return ( "Name: {name}\n" "Version: {version}\n" "Release: {release}\n" "Install Date: {installed}\n" "Supported OSes: {os}\n" "Supported OS families: {os_family}\n" "Dependencies: {dependencies}\n" "OS Dependencies: {os_dependencies}\n" "OS Family Dependencies: {os_family_dependencies}\n" "Summary: {summary}\n" "Description:\n" "{description}".format(**formula_def) ) def _local_list_files(self, args): """ List files for a package file """ if len(args) < 2: raise SPMInvocationError("A package filename must be specified") pkg_file = args[1] if not os.path.exists(pkg_file): raise SPMPackageError(f"Package file {pkg_file} not found") formula_tar = tarfile.open(pkg_file, "r:bz2") pkg_files = formula_tar.getmembers() for member in pkg_files: self.ui.status(member.name) def _list_packages(self, args): """ List files for an installed package """ packages = self._pkgdb_fun("list_packages", self.db_conn) for package in packages: if self.opts["verbose"]: status_msg = ",".join(package) else: status_msg = package[0] self.ui.status(status_msg) def _list_files(self, args): """ List files for an installed package """ if len(args) < 2: raise SPMInvocationError("A package name must be specified") package = args[-1] files = self._pkgdb_fun("list_files", package, self.db_conn) if files is None: raise SPMPackageError(f"package {package} not installed") else: for file_ in files: if self.opts["verbose"]: status_msg = ",".join(file_) else: status_msg = file_[0] self.ui.status(status_msg) def _build(self, args): """ Build a package """ if len(args) < 2: raise SPMInvocationError("A path to a formula must be specified") self.abspath = args[1].rstrip("/") comps = self.abspath.split("/") self.relpath = comps[-1] formula_path = f"{self.abspath}/FORMULA" if not os.path.exists(formula_path): raise SPMPackageError(f"Formula file {formula_path} not found") with salt.utils.files.fopen(formula_path) as fp_: formula_conf = salt.utils.yaml.safe_load(fp_) for field in ("name", "version", "release", "summary", "description"): if field not in formula_conf: raise SPMPackageError(f"Invalid package: a {field} must be defined") out_path = "{}/{}-{}-{}.spm".format( self.opts["spm_build_dir"], formula_conf["name"], formula_conf["version"], formula_conf["release"], ) if not os.path.exists(self.opts["spm_build_dir"]): os.mkdir(self.opts["spm_build_dir"]) self.formula_conf = formula_conf formula_tar = tarfile.open(out_path, "w:bz2") if "files" in formula_conf: # This allows files to be added to the SPM file in a specific order. # It also allows for files to be tagged as a certain type, as with # RPM files. This tag is ignored here, but is used when installing # the SPM file. if isinstance(formula_conf["files"], list): formula_dir = tarfile.TarInfo(formula_conf["name"]) formula_dir.type = tarfile.DIRTYPE formula_tar.addfile(formula_dir) for file_ in formula_conf["files"]: for ftype in FILE_TYPES: if file_.startswith(f"{ftype}|"): file_ = file_.lstrip(f"{ftype}|") formula_tar.add( os.path.join(os.getcwd(), file_), os.path.join(formula_conf["name"], file_), ) else: # If no files are specified, then the whole directory will be added. try: formula_tar.add( formula_path, formula_conf["name"], filter=self._exclude ) formula_tar.add( self.abspath, formula_conf["name"], filter=self._exclude ) except TypeError: formula_tar.add( formula_path, formula_conf["name"], exclude=self._exclude ) formula_tar.add( self.abspath, formula_conf["name"], exclude=self._exclude ) formula_tar.close() self.ui.status(f"Built package {out_path}") def _exclude(self, member): """ Exclude based on opts """ if isinstance(member, str): return None for item in self.opts["spm_build_exclude"]: if member.name.startswith("{}/{}".format(self.formula_conf["name"], item)): return None elif member.name.startswith(f"{self.abspath}/{item}"): return None return member def _render(self, data, formula_def): """ Render a [pre|post]_local_state or [pre|post]_tgt_state script """ # FORMULA can contain a renderer option renderer = formula_def.get("renderer", self.opts.get("renderer", "jinja|yaml")) rend = salt.loader.render(self.opts, {}) blacklist = self.opts.get("renderer_blacklist") whitelist = self.opts.get("renderer_whitelist") template_vars = formula_def.copy() template_vars["opts"] = self.opts.copy() return compile_template( ":string:", rend, renderer, blacklist, whitelist, input_data=data, **template_vars, ) class SPMUserInterface: """ Handle user interaction with an SPMClient object """ def status(self, msg): """ Report an SPMClient status message """ raise NotImplementedError() def error(self, msg): """ Report an SPM error message """ raise NotImplementedError() def confirm(self, action): """ Get confirmation from the user before performing an SPMClient action. Return if the action is confirmed, or raise SPMOperationCanceled(<msg>) if canceled. """ raise NotImplementedError() class SPMCmdlineInterface(SPMUserInterface): """ Command-line interface to SPMClient """ def status(self, msg): print(msg) def error(self, msg): print(msg, file=sys.stderr) def confirm(self, action): print(action) res = input("Proceed? [N/y] ") if not res.lower().startswith("y"): raise SPMOperationCanceled("canceled")