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/cache
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/cache/redis_cache.py
r""" Redis ===== Redis plugin for the Salt caching subsystem. .. versionadded:: 2017.7.0 .. versionchanged:: 3005 To enable this cache plugin, the master will need the python client for redis installed. This can be easily installed with pip: .. code-block:: bash salt \* pip.install redis As Redis provides a simple mechanism for very fast key-value store, in order to provide the necessary features for the Salt caching subsystem, the following conventions are used: - A Redis key consists of the bank name and the cache key separated by ``/``, e.g.: ``$KEY_minions/alpha/stuff`` where ``minions/alpha`` is the bank name and ``stuff`` is the key name. - As the caching subsystem is organised as a tree, we need to store the caching path and identify the bank and its offspring. At the same time, Redis is linear and we need to avoid doing ``keys <pattern>`` which is very inefficient as it goes through all the keys on the remote Redis server. Instead, each bank hierarchy has a Redis SET associated which stores the list of sub-banks. By default, these keys begin with ``$BANK_``. - In addition, each key name is stored in a separate SET of all the keys within a bank. By default, these SETs begin with ``$BANKEYS_``. For example, to store the key ``my-key`` under the bank ``root-bank/sub-bank/leaf-bank``, the following hierarchy will be built: .. code-block:: text 127.0.0.1:6379> SMEMBERS $BANK_root-bank 1) "sub-bank" 127.0.0.1:6379> SMEMBERS $BANK_root-bank/sub-bank 1) "leaf-bank" 127.0.0.1:6379> SMEMBERS $BANKEYS_root-bank/sub-bank/leaf-bank 1) "my-key" 127.0.0.1:6379> GET $KEY_root-bank/sub-bank/leaf-bank/my-key "my-value" There are four types of keys stored: - ``$BANK_*`` is a Redis SET containing the list of banks under the current bank. - ``$BANKEYS_*`` is a Redis SET containing the list of keys under the current bank. - ``$KEY_*`` keeps the value of the key. - ``$TSTAMP_*`` stores the last updated timestamp of the key. These prefixes and the separator can be adjusted using the configuration options: bank_prefix: ``$BANK`` The prefix used for the name of the Redis key storing the list of sub-banks. bank_keys_prefix: ``$BANKEYS`` The prefix used for the name of the Redis key storing the list of keys under a certain bank. key_prefix: ``$KEY`` The prefix of the Redis keys having the value of the keys to be cached under a certain bank. timestamp_prefix: ``$TSTAMP`` The prefix for the last modified timestamp for keys. .. versionadded:: 3005 separator: ``_`` The separator between the prefix and the key body. The connection details can be specified using: host: ``localhost`` The hostname of the Redis server. port: ``6379`` The Redis server port. cluster_mode: ``False`` Whether cluster_mode is enabled or not cluster.startup_nodes: A list of host, port dictionaries pointing to cluster members. At least one is required but multiple nodes are better .. code-block:: yaml cache.redis.cluster.startup_nodes - host: redis-member-1 port: 6379 - host: redis-member-2 port: 6379 cluster.skip_full_coverage_check: ``False`` Some cluster providers restrict certain redis commands such as CONFIG for enhanced security. Set this option to true to skip checks that required advanced privileges. .. note:: Most cloud hosted redis clusters will require this to be set to ``True`` db: ``'0'`` The database index. .. note:: The database index must be specified as string not as integer value! password: Redis connection password. unix_socket_path: .. versionadded:: 2018.3.1 Path to a UNIX socket for access. Overrides `host` / `port`. Configuration Example: .. code-block:: yaml cache.redis.host: localhost cache.redis.port: 6379 cache.redis.db: '0' cache.redis.password: my pass cache.redis.bank_prefix: #BANK cache.redis.bank_keys_prefix: #BANKEYS cache.redis.key_prefix: #KEY cache.redis.timestamp_prefix: #TICKS cache.redis.separator: '@' Cluster Configuration Example: .. code-block:: yaml cache.redis.cluster_mode: true cache.redis.cluster.skip_full_coverage_check: true cache.redis.cluster.startup_nodes: - host: redis-member-1 port: 6379 - host: redis-member-2 port: 6379 cache.redis.db: '0' cache.redis.password: my pass cache.redis.bank_prefix: #BANK cache.redis.bank_keys_prefix: #BANKEYS cache.redis.key_prefix: #KEY cache.redis.separator: '@' """ import itertools import logging import time import salt.payload import salt.utils.stringutils from salt.exceptions import SaltCacheError # Import salt try: import redis from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ResponseError as RedisResponseError HAS_REDIS = True except ImportError: HAS_REDIS = False try: from rediscluster import RedisCluster # pylint: disable=no-name-in-module HAS_REDIS_CLUSTER = True except ImportError: HAS_REDIS_CLUSTER = False # ----------------------------------------------------------------------------- # module properties # ----------------------------------------------------------------------------- __virtualname__ = "redis" __func_alias__ = {"list_": "list"} log = logging.getLogger(__file__) _BANK_PREFIX = "$BANK" _KEY_PREFIX = "$KEY" _TIMESTAMP_PREFIX = "$TSTAMP" _BANK_KEYS_PREFIX = "$BANKEYS" _SEPARATOR = "_" REDIS_SERVER = None # ----------------------------------------------------------------------------- # property functions # ----------------------------------------------------------------------------- def __virtual__(): """ The redis library must be installed for this module to work. The redis redis cluster library must be installed if cluster_mode is True """ if not HAS_REDIS: return (False, "Please install the python-redis package.") if not HAS_REDIS_CLUSTER and _get_redis_cache_opts()["cluster_mode"]: return (False, "Please install the redis-py-cluster package.") return __virtualname__ # ----------------------------------------------------------------------------- # helper functions -- will not be exported # ----------------------------------------------------------------------------- def init_kwargs(kwargs): """ Effectively a noop. Return an empty dictionary. """ return {} def _get_redis_cache_opts(): """ Return the Redis server connection details from the __opts__. """ return { "host": __opts__.get("cache.redis.host", "localhost"), "port": __opts__.get("cache.redis.port", 6379), "unix_socket_path": __opts__.get("cache.redis.unix_socket_path", None), "db": __opts__.get("cache.redis.db", "0"), "password": __opts__.get("cache.redis.password", ""), "cluster_mode": __opts__.get("cache.redis.cluster_mode", False), "startup_nodes": __opts__.get("cache.redis.cluster.startup_nodes", {}), "skip_full_coverage_check": __opts__.get( "cache.redis.cluster.skip_full_coverage_check", False ), } def _get_redis_server(opts=None): """ Return the Redis server instance. Caching the object instance. """ global REDIS_SERVER if REDIS_SERVER: return REDIS_SERVER if not opts: opts = _get_redis_cache_opts() if opts["cluster_mode"]: REDIS_SERVER = RedisCluster( startup_nodes=opts["startup_nodes"], skip_full_coverage_check=opts["skip_full_coverage_check"], ) else: REDIS_SERVER = redis.StrictRedis( opts["host"], opts["port"], unix_socket_path=opts["unix_socket_path"], db=opts["db"], password=opts["password"], ) return REDIS_SERVER def _get_redis_keys_opts(): """ Build the key opts based on the user options. """ return { "bank_prefix": __opts__.get("cache.redis.bank_prefix", _BANK_PREFIX), "bank_keys_prefix": __opts__.get( "cache.redis.bank_keys_prefix", _BANK_KEYS_PREFIX ), "key_prefix": __opts__.get("cache.redis.key_prefix", _KEY_PREFIX), "separator": __opts__.get("cache.redis.separator", _SEPARATOR), "timestamp_prefix": __opts__.get( "cache.redis.timestamp_prefix", _TIMESTAMP_PREFIX ), } def _get_bank_redis_key(bank): """ Return the Redis key for the bank given the name. """ opts = _get_redis_keys_opts() return "{prefix}{separator}{bank}".format( prefix=opts["bank_prefix"], separator=opts["separator"], bank=bank ) def _get_timestamp_key(bank, key): opts = _get_redis_keys_opts() return "{}{}{}/{}".format( opts["timestamp_prefix"], opts["separator"], {bank}, {key} ) # Use this line when we can use modern python # return f"{opts['timestamp_prefix']}{opts['separator']}{bank}/{key}" def _get_key_redis_key(bank, key): """ Return the Redis key given the bank name and the key name. """ opts = _get_redis_keys_opts() return "{prefix}{separator}{bank}/{key}".format( prefix=opts["key_prefix"], separator=opts["separator"], bank=bank, key=salt.utils.stringutils.to_str(key), ) def _get_bank_keys_redis_key(bank): """ Return the Redis key for the SET of keys under a certain bank, given the bank name. """ opts = _get_redis_keys_opts() return "{prefix}{separator}{bank}".format( prefix=opts["bank_keys_prefix"], separator=opts["separator"], bank=bank ) def _build_bank_hier(bank, redis_pipe): """ Build the bank hierarchy from the root of the tree. If already exists, it won't rewrite. It's using the Redis pipeline, so there will be only one interaction with the remote server. """ def joinbanks(*banks): return "/".join(banks) for bank_path in itertools.accumulate(bank.split("/"), joinbanks): bank_set = _get_bank_redis_key(bank_path) log.debug("Adding %s to %s", bank, bank_set) redis_pipe.sadd(bank_set, ".") def _get_banks_to_remove(redis_server, bank, path=""): """ A simple tree traversal algorithm that builds the list of banks to remove, starting from an arbitrary node in the tree. """ current_path = bank if not path else f"{path}/{bank}" bank_paths_to_remove = [current_path] # as you got here, you'll be removed bank_key = _get_bank_redis_key(current_path) child_banks = redis_server.smembers(bank_key) if not child_banks: return bank_paths_to_remove # this bank does not have any child banks so we stop here for child_bank in child_banks: bank_paths_to_remove.extend( _get_banks_to_remove(redis_server, child_bank, path=current_path) ) # go one more level deeper # and also remove the children of this child bank (if any) return bank_paths_to_remove # ----------------------------------------------------------------------------- # cache subsystem functions # ----------------------------------------------------------------------------- def store(bank, key, data): """ Store the data in a Redis key. """ redis_server = _get_redis_server() redis_pipe = redis_server.pipeline() redis_key = _get_key_redis_key(bank, key) redis_bank_keys = _get_bank_keys_redis_key(bank) try: _build_bank_hier(bank, redis_pipe) value = salt.payload.dumps(data) redis_pipe.set(redis_key, value) log.debug("Setting the value for %s under %s (%s)", key, bank, redis_key) redis_pipe.sadd(redis_bank_keys, key) # localfs cache truncates the timestamp to int only. We'll do the same. redis_pipe.set( _get_timestamp_key(bank=bank, key=key), salt.payload.dumps(int(time.time())), ) log.debug("Adding %s to %s", key, redis_bank_keys) redis_pipe.execute() except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot set the Redis cache key {rkey}: {rerr}".format( rkey=redis_key, rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) def fetch(bank, key): """ Fetch data from the Redis cache. """ redis_server = _get_redis_server() redis_key = _get_key_redis_key(bank, key) redis_value = None try: redis_value = redis_server.get(redis_key) except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot fetch the Redis cache key {rkey}: {rerr}".format( rkey=redis_key, rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) if redis_value is None: return {} return salt.payload.loads(redis_value) def flush(bank, key=None): """ Remove the key from the cache bank with all the key content. If no key is specified, remove the entire bank with all keys and sub-banks inside. This function is using the Redis pipelining for best performance. However, when removing a whole bank, in order to re-create the tree, there are a couple of requests made. In total: - one for node in the hierarchy sub-tree, starting from the bank node - one pipelined request to get the keys under all banks in the sub-tree - one pipeline request to remove the corresponding keys This is not quite optimal, as if we need to flush a bank having a very long list of sub-banks, the number of requests to build the sub-tree may grow quite big. An improvement for this would be loading a custom Lua script in the Redis instance of the user (using the ``register_script`` feature) and call it whenever we flush. This script would only need to build this sub-tree causing problems. It can be added later and the behaviour should not change as the user needs to explicitly allow Salt inject scripts in their Redis instance. """ redis_server = _get_redis_server() redis_pipe = redis_server.pipeline() if key is None: # will remove all bank keys bank_paths_to_remove = _get_banks_to_remove(redis_server, bank) # tree traversal to get all bank hierarchy for bank_to_remove in bank_paths_to_remove: bank_keys_redis_key = _get_bank_keys_redis_key(bank_to_remove) # Redis key of the SET that stores the bank keys redis_pipe.smembers(bank_keys_redis_key) # fetch these keys log.debug( "Fetching the keys of the %s bank (%s)", bank_to_remove, bank_keys_redis_key, ) try: log.debug("Executing the pipe...") subtree_keys = ( redis_pipe.execute() ) # here are the keys under these banks to be removed # this retunrs a list of sets, e.g.: # [set([]), set(['my-key']), set(['my-other-key', 'yet-another-key'])] # one set corresponding to a bank except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot retrieve the keys under these cache banks: {rbanks}: {rerr}".format( rbanks=", ".join(bank_paths_to_remove), rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) total_banks = len(bank_paths_to_remove) # bank_paths_to_remove and subtree_keys have the same length (see above) for index in range(total_banks): bank_keys = subtree_keys[index] # all the keys under this bank bank_path = bank_paths_to_remove[index] for key in bank_keys: redis_key = _get_key_redis_key(bank_path, key) redis_pipe.delete(redis_key) # kill 'em all! timestamp_key = _get_timestamp_key(bank=bank_path, key=key.decode()) redis_pipe.delete(timestamp_key) log.debug( "Removing the key %s under the %s bank (%s)", key, bank_path, redis_key, ) bank_keys_redis_key = _get_bank_keys_redis_key(bank_path) redis_pipe.delete(bank_keys_redis_key) log.debug( "Removing the bank-keys key for the %s bank (%s)", bank_path, bank_keys_redis_key, ) # delete the Redis key where are stored # the list of keys under this bank bank_key = _get_bank_redis_key(bank_path) redis_pipe.delete(bank_key) log.debug("Removing the %s bank (%s)", bank_path, bank_key) # delete the bank key itself else: redis_key = _get_key_redis_key(bank, key) redis_pipe.delete(redis_key) # delete the key cached timestamp_key = _get_timestamp_key(bank=bank, key=key) redis_pipe.delete(timestamp_key) log.debug("Removing the key %s under the %s bank (%s)", key, bank, redis_key) bank_keys_redis_key = _get_bank_keys_redis_key(bank) redis_pipe.srem(bank_keys_redis_key, key) log.debug( "De-referencing the key %s from the bank-keys of the %s bank (%s)", key, bank, bank_keys_redis_key, ) # but also its reference from $BANKEYS list try: redis_pipe.execute() # Fluuuush except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot flush the Redis cache bank {rbank}: {rerr}".format( rbank=bank, rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) return True def list_(bank): """ Lists entries stored in the specified bank. """ redis_server = _get_redis_server() bank_redis_key = _get_bank_keys_redis_key(bank) try: banks = redis_server.smembers(bank_redis_key) except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot list the Redis cache key {rkey}: {rerr}".format( rkey=bank_redis_key, rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) if not banks: return [] return [bank.decode() for bank in banks if bank != b"."] def contains(bank, key): """ Checks if the specified bank contains the specified key. """ redis_server = _get_redis_server() bank_redis_key = _get_bank_keys_redis_key(bank) try: if key is None: return ( salt.utils.stringutils.to_str(redis_server.type(bank_redis_key)) != "none" ) else: return redis_server.sismember(bank_redis_key, key) except (RedisConnectionError, RedisResponseError) as rerr: mesg = "Cannot retrieve the Redis cache key {rkey}: {rerr}".format( rkey=bank_redis_key, rerr=rerr ) log.error(mesg) raise SaltCacheError(mesg) def updated(bank, key): """ Return the Unix Epoch timestamp of when the key was last updated. Return None if key is not found. """ redis_server = _get_redis_server() timestamp_key = _get_timestamp_key(bank=bank, key=key) value = redis_server.get(timestamp_key) if value is not None: value = salt.payload.loads(value) return value