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/modules
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/cron.py
""" Work with cron .. note:: Salt does not escape cron metacharacters automatically. You should backslash-escape percent characters and any other metacharacters that might be interpreted incorrectly by the shell. """ import logging import os import random import salt.utils.data import salt.utils.files import salt.utils.functools import salt.utils.path import salt.utils.stringutils TAG = "# Lines below here are managed by Salt, do not edit\n" SALT_CRON_IDENTIFIER = "SALT_CRON_IDENTIFIER" SALT_CRON_NO_IDENTIFIER = "NO ID SET" log = logging.getLogger(__name__) def __virtual__(): if salt.utils.path.which("crontab"): return True else: return (False, "Cannot load cron module: crontab command not found") def _ensure_string(val): # Account for cases where the identifier is not a string # which would cause to_unicode to fail. if not isinstance(val, str): val = str(val) try: return salt.utils.stringutils.to_unicode(val) except TypeError: return "" def _cron_id(cron): """SAFETYBELT, Only set if we really have an identifier""" cid = None if cron["identifier"]: cid = cron["identifier"] else: cid = SALT_CRON_NO_IDENTIFIER if cid: return _ensure_string(cid) def _cron_matched(cron, cmd, identifier=None): """Check if: - we find a cron with same cmd, old state behavior - but also be smart enough to remove states changed crons where we do not removed priorly by a cron.absent by matching on the provided identifier. We assure retrocompatibility by only checking on identifier if and only if an identifier was set on the serialized crontab """ ret, id_matched = False, None cid = _cron_id(cron) if cid: if not identifier: identifier = SALT_CRON_NO_IDENTIFIER eidentifier = _ensure_string(identifier) # old style second round # after saving crontab, we must check that if # we have not the same command, but the default id # to not set that as a match if ( cron.get("cmd", None) != cmd and cid == SALT_CRON_NO_IDENTIFIER and eidentifier == SALT_CRON_NO_IDENTIFIER ): id_matched = False else: # on saving, be sure not to overwrite a cron # with specific identifier but also track # crons where command is the same # but with the default if that we gonna overwrite if ( cron.get("cmd", None) == cmd and cid == SALT_CRON_NO_IDENTIFIER and identifier ): cid = eidentifier id_matched = eidentifier == cid if ((id_matched is None) and cmd == cron.get("cmd", None)) or id_matched: ret = True return ret def _needs_change(old, new): if old != new: if new == "random": # Allow switch from '*' or not present to 'random' if old == "*": return True elif new is not None: return True return False def _render_tab(lst): """ Takes a tab list structure and renders it to a list for applying it to a file """ ret = [] for pre in lst["pre"]: ret.append(f"{pre}\n") if ret: if ret[-1] != TAG: ret.append(TAG) else: ret.append(TAG) for env in lst["env"]: if (env["value"] is None) or (env["value"] == ""): ret.append('{}=""\n'.format(env["name"])) else: ret.append("{}={}\n".format(env["name"], env["value"])) for cron in lst["crons"]: if cron["comment"] is not None or cron["identifier"] is not None: comment = "#" if cron["comment"]: comment += " {}".format(cron["comment"].replace("\n", "\n# ")) if cron["identifier"]: comment += " {}:{}".format(SALT_CRON_IDENTIFIER, cron["identifier"]) comment += "\n" ret.append(comment) ret.append( "{}{} {} {} {} {} {}\n".format( cron["commented"] is True and "#DISABLED#" or "", cron["minute"], cron["hour"], cron["daymonth"], cron["month"], cron["dayweek"], cron["cmd"], ) ) for cron in lst["special"]: if cron["comment"] is not None or cron["identifier"] is not None: comment = "#" if cron["comment"]: comment += " {}".format(cron["comment"].rstrip().replace("\n", "\n# ")) if cron["identifier"]: comment += " {}:{}".format(SALT_CRON_IDENTIFIER, cron["identifier"]) comment += "\n" ret.append(comment) ret.append( "{}{} {}\n".format( cron["commented"] is True and "#DISABLED#" or "", cron["spec"], cron["cmd"], ) ) return ret def _get_cron_cmdstr(path, user=None): """ Returns a format string, to be used to build a crontab command. """ if user: cmd = f"crontab -u {user}" else: cmd = "crontab" return f"{cmd} {path}" def _check_instance_uid_match(user): """ Returns true if running instance's UID matches the specified user UID """ return os.geteuid() == __salt__["file.user_to_uid"](user) def write_cron_file(user, path): """ Writes the contents of a file to a user's crontab CLI Example: .. code-block:: bash salt '*' cron.write_cron_file root /tmp/new_cron .. versionchanged:: 2015.8.9 .. note:: Some OS' do not support specifying user via the `crontab` command i.e. (Solaris, AIX) """ # Some OS' do not support specifying user via the `crontab` command if __grains__.get("os_family") in ("Solaris", "AIX"): return ( __salt__["cmd.retcode"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) == 0 ) # If Salt is running from same user as requested in cron module we don't need any user switch elif _check_instance_uid_match(user): return __salt__["cmd.retcode"](_get_cron_cmdstr(path), python_shell=False) == 0 # If Salt is running from root user it could modify any user's crontab elif _check_instance_uid_match("root"): return ( __salt__["cmd.retcode"](_get_cron_cmdstr(path, user), python_shell=False) == 0 ) # Edge cases here, let's try do a runas else: return ( __salt__["cmd.retcode"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) == 0 ) def write_cron_file_verbose(user, path): """ Writes the contents of a file to a user's crontab and return error message on error CLI Example: .. code-block:: bash salt '*' cron.write_cron_file_verbose root /tmp/new_cron .. versionchanged:: 2015.8.9 .. note:: Some OS' do not support specifying user via the `crontab` command i.e. (Solaris, AIX) """ # Some OS' do not support specifying user via the `crontab` command if __grains__.get("os_family") in ("Solaris", "AIX"): return __salt__["cmd.run_all"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) # If Salt is running from same user as requested in cron module we don't need any user switch elif _check_instance_uid_match(user): return __salt__["cmd.run_all"](_get_cron_cmdstr(path), python_shell=False) # If Salt is running from root user it could modify any user's crontab elif _check_instance_uid_match("root"): return __salt__["cmd.run_all"](_get_cron_cmdstr(path, user), python_shell=False) # Edge cases here, let's try do a runas else: return __salt__["cmd.run_all"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) def _write_cron_lines(user, lines): """ Takes a list of lines to be committed to a user's crontab and writes it """ lines = [salt.utils.stringutils.to_str(_l) for _l in lines] path = salt.utils.files.mkstemp() # Some OS' do not support specifying user via the `crontab` command if __grains__.get("os_family") in ("Solaris", "AIX"): with salt.utils.files.fpopen( path, "w+", uid=__salt__["file.user_to_uid"](user), mode=0o600 ) as fp_: fp_.writelines(lines) ret = __salt__["cmd.run_all"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) # If Salt is running from same user as requested in cron module we don't need any user switch elif _check_instance_uid_match(user): with salt.utils.files.fpopen(path, "w+", mode=0o600) as fp_: fp_.writelines(lines) ret = __salt__["cmd.run_all"](_get_cron_cmdstr(path), python_shell=False) # If Salt is running from root user it could modify any user's crontab elif _check_instance_uid_match("root"): with salt.utils.files.fpopen(path, "w+", mode=0o600) as fp_: fp_.writelines(lines) ret = __salt__["cmd.run_all"](_get_cron_cmdstr(path, user), python_shell=False) # Edge cases here, let's try do a runas else: with salt.utils.files.fpopen( path, "w+", uid=__salt__["file.user_to_uid"](user), mode=0o600 ) as fp_: fp_.writelines(lines) ret = __salt__["cmd.run_all"]( _get_cron_cmdstr(path), runas=user, python_shell=False ) os.remove(path) return ret def _date_time_match(cron, **kwargs): """ Returns true if the minute, hour, etc. params match their counterparts from the dict returned from list_tab(). """ return all( [ kwargs.get(x) is None or cron[x] == str(kwargs[x]) or (str(kwargs[x]).lower() == "random" and cron[x] != "*") for x in ("minute", "hour", "daymonth", "month", "dayweek") ] ) def raw_cron(user): """ Return the contents of the user's crontab CLI Example: .. code-block:: bash salt '*' cron.raw_cron root """ # Some OS' do not support specifying user via the `crontab` command if __grains__.get("os_family") in ("Solaris", "AIX"): cmd = "crontab -l" # Preserve line endings lines = salt.utils.data.decode( __salt__["cmd.run_stdout"]( cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False ) ).splitlines(True) # If Salt is running from same user as requested in cron module we don't need any user switch elif _check_instance_uid_match(user): cmd = "crontab -l" # Preserve line endings lines = salt.utils.data.decode( __salt__["cmd.run_stdout"]( cmd, ignore_retcode=True, rstrip=False, python_shell=False ) ).splitlines(True) # If Salt is running from root user it could modify any user's crontab elif _check_instance_uid_match("root"): cmd = f"crontab -u {user} -l" # Preserve line endings lines = salt.utils.data.decode( __salt__["cmd.run_stdout"]( cmd, ignore_retcode=True, rstrip=False, python_shell=False ) ).splitlines(True) # Edge cases here, let's try do a runas else: cmd = "crontab -l" # Preserve line endings lines = salt.utils.data.decode( __salt__["cmd.run_stdout"]( cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False ) ).splitlines(True) if lines and lines[0].startswith( "# DO NOT EDIT THIS FILE - edit the master and reinstall." ): del lines[0:3] return "".join(lines) def list_tab(user): """ Return the contents of the specified user's crontab CLI Example: .. code-block:: bash salt '*' cron.list_tab root """ data = raw_cron(user) ret = {"pre": [], "crons": [], "special": [], "env": []} flag = False comment = None identifier = None for line in data.splitlines(): if line == "# Lines below here are managed by Salt, do not edit": flag = True continue if flag: commented_cron_job = False if line.startswith("#DISABLED#"): # It's a commented cron job line = line[10:] commented_cron_job = True if line.startswith("@"): # Its a "special" line dat = {} comps = line.split() if len(comps) < 2: # Invalid line continue dat["spec"] = comps[0] dat["cmd"] = " ".join(comps[1:]) dat["identifier"] = identifier dat["comment"] = comment dat["commented"] = False if commented_cron_job: dat["commented"] = True ret["special"].append(dat) identifier = None comment = None commented_cron_job = False elif line.startswith("#"): # It's a comment! Catch it! comment_line = line.lstrip("# ") # load the identifier if any if SALT_CRON_IDENTIFIER in comment_line: parts = comment_line.split(SALT_CRON_IDENTIFIER) comment_line = parts[0].rstrip() # skip leading : if len(parts[1]) > 1: identifier = parts[1][1:] if comment is None: comment = comment_line else: comment += "\n" + comment_line elif line.find("=") > 0 and ( " " not in line or line.index("=") < line.index(" ") ): # Appears to be a ENV setup line comps = line.split("=", 1) dat = {} dat["name"] = comps[0] dat["value"] = comps[1] ret["env"].append(dat) elif len(line.split(" ")) > 5: # Appears to be a standard cron line comps = line.split(" ") dat = { "minute": comps[0], "hour": comps[1], "daymonth": comps[2], "month": comps[3], "dayweek": comps[4], "identifier": identifier, "cmd": " ".join(comps[5:]), "comment": comment, "commented": False, } if commented_cron_job: dat["commented"] = True ret["crons"].append(dat) identifier = None comment = None commented_cron_job = False else: ret["pre"].append(line) return ret # For consistency's sake ls = salt.utils.functools.alias_function(list_tab, "ls") def get_entry(user, identifier=None, cmd=None): """ Return the specified entry from user's crontab. identifier will be used if specified, otherwise will lookup cmd Either identifier or cmd should be specified. user: User's crontab to query identifier: Search for line with identifier cmd: Search for cron line with cmd CLI Example: .. code-block:: bash salt '*' cron.get_entry root identifier=task1 """ if identifier and cmd: log.warning("Both identifier and cmd are specified. Only using identifier.") cmd = None cron_entries = list_tab(user).get("crons", []) + list_tab(user).get("special", []) for cron_entry in cron_entries: if identifier and cron_entry.get("identifier") == identifier: return cron_entry elif cmd and cron_entry.get("cmd") == cmd: return cron_entry return False def set_special(user, special, cmd, commented=False, comment=None, identifier=None): """ Set up a special command in the crontab. CLI Example: .. code-block:: bash salt '*' cron.set_special root @hourly 'echo foobar' """ lst = list_tab(user) for cron in lst["crons"] + lst["special"]: cid = _cron_id(cron) if _cron_matched(cron, cmd, identifier): test_setted_id = ( cron["identifier"] is None and SALT_CRON_NO_IDENTIFIER or cron["identifier"] ) tests = [ (cron["comment"], comment), (cron["commented"], commented), (identifier, test_setted_id), (cron.get("minute"), None), (cron.get("hour"), None), (cron.get("daymonth"), None), (cron.get("month"), None), (cron.get("dayweek"), None), (cron.get("spec"), special), ] if cid or identifier: tests.append((cron["cmd"], cmd)) if any([_needs_change(x, y) for x, y in tests]): if "spec" in cron: rm_special(user, cmd, identifier=cid) else: rm_job(user, cmd, identifier=cid) # Use old values when setting the new job if there was no # change needed for a given parameter if not _needs_change(cron.get("spec"), special): special = cron.get("spec") if not _needs_change(cron.get("commented"), commented): commented = cron.get("commented") if not _needs_change(cron.get("comment"), comment): comment = cron.get("comment") if not _needs_change(cron["cmd"], cmd): cmd = cron["cmd"] if cid == SALT_CRON_NO_IDENTIFIER: if identifier: cid = identifier if ( cid == SALT_CRON_NO_IDENTIFIER and cron["identifier"] is None ): cid = None cron["identifier"] = cid if not cid or (cid and not _needs_change(cid, identifier)): identifier = cid jret = set_special( user, special, cmd, commented=commented, comment=comment, identifier=identifier, ) if jret == "new": return "updated" else: return jret return "present" cron = { "spec": special, "cmd": cmd, "identifier": identifier, "comment": comment, "commented": commented, } lst["special"].append(cron) comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return "new" def _get_cron_date_time(**kwargs): """ Returns a dict of date/time values to be used in a cron entry """ # Define ranges (except daymonth, as it depends on the month) range_max = { "minute": list(list(range(60))), "hour": list(list(range(24))), "month": list(list(range(1, 13))), "dayweek": list(list(range(7))), } ret = {} for param in ("minute", "hour", "month", "dayweek"): value = str(kwargs.get(param, "1")).lower() if value == "random": ret[param] = str(random.sample(range_max[param], 1)[0]) elif len(value.split(":")) == 2: cron_range = sorted(value.split(":")) start, end = int(cron_range[0]), int(cron_range[1]) ret[param] = str(random.randint(start, end)) else: ret[param] = value if ret["month"] in "1 3 5 7 8 10 12".split(): daymonth_max = 31 elif ret["month"] in "4 6 9 11".split(): daymonth_max = 30 else: # This catches both '2' and '*' daymonth_max = 28 daymonth = str(kwargs.get("daymonth", "1")).lower() if daymonth == "random": ret["daymonth"] = str( random.sample(list(list(range(1, (daymonth_max + 1)))), 1)[0] ) else: ret["daymonth"] = daymonth return ret def set_job( user, minute, hour, daymonth, month, dayweek, cmd, commented=False, comment=None, identifier=None, ): """ Sets a cron job up for a specified user. CLI Example: .. code-block:: bash salt '*' cron.set_job root '*' '*' '*' '*' 1 /usr/local/weekly """ # Scrub the types minute = str(minute).lower() hour = str(hour).lower() daymonth = str(daymonth).lower() month = str(month).lower() dayweek = str(dayweek).lower() lst = list_tab(user) for cron in lst["crons"] + lst["special"]: cid = _cron_id(cron) if _cron_matched(cron, cmd, identifier): test_setted_id = ( cron["identifier"] is None and SALT_CRON_NO_IDENTIFIER or cron["identifier"] ) tests = [ (cron["comment"], comment), (cron["commented"], commented), (identifier, test_setted_id), (cron.get("minute"), minute), (cron.get("hour"), hour), (cron.get("daymonth"), daymonth), (cron.get("month"), month), (cron.get("dayweek"), dayweek), (cron.get("spec"), None), ] if cid or identifier: tests.append((cron["cmd"], cmd)) if any([_needs_change(x, y) for x, y in tests]): if "spec" in cron: rm_special(user, cmd, identifier=cid) else: rm_job(user, cmd, identifier=cid) # Use old values when setting the new job if there was no # change needed for a given parameter if not _needs_change(cron.get("minute"), minute): minute = cron.get("minute") if not _needs_change(cron.get("hour"), hour): hour = cron.get("hour") if not _needs_change(cron.get("daymonth"), daymonth): daymonth = cron.get("daymonth") if not _needs_change(cron.get("month"), month): month = cron.get("month") if not _needs_change(cron.get("dayweek"), dayweek): dayweek = cron.get("dayweek") if not _needs_change(cron["commented"], commented): commented = cron["commented"] if not _needs_change(cron["comment"], comment): comment = cron["comment"] if not _needs_change(cron["cmd"], cmd): cmd = cron["cmd"] if cid == SALT_CRON_NO_IDENTIFIER: if identifier: cid = identifier if ( cid == SALT_CRON_NO_IDENTIFIER and cron["identifier"] is None ): cid = None cron["identifier"] = cid if not cid or (cid and not _needs_change(cid, identifier)): identifier = cid jret = set_job( user, minute, hour, daymonth, month, dayweek, cmd, commented=commented, comment=comment, identifier=identifier, ) if jret == "new": return "updated" else: return jret return "present" cron = { "cmd": cmd, "identifier": identifier, "comment": comment, "commented": commented, } cron.update( _get_cron_date_time( minute=minute, hour=hour, daymonth=daymonth, month=month, dayweek=dayweek ) ) lst["crons"].append(cron) comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return "new" def rm_special(user, cmd, special=None, identifier=None): """ Remove a special cron job for a specified user. CLI Example: .. code-block:: bash salt '*' cron.rm_special root /usr/bin/foo """ lst = list_tab(user) ret = "absent" rm_ = None for ind, val in enumerate(lst["special"]): if rm_ is not None: break if _cron_matched(val, cmd, identifier=identifier): if special is None: # No special param was specified rm_ = ind else: if val["spec"] == special: rm_ = ind if rm_ is not None: lst["special"].pop(rm_) ret = "removed" comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return ret def rm_job( user, cmd, minute=None, hour=None, daymonth=None, month=None, dayweek=None, identifier=None, ): """ Remove a cron job for a specified user. If any of the day/time params are specified, the job will only be removed if the specified params match. CLI Example: .. code-block:: bash salt '*' cron.rm_job root /usr/local/weekly salt '*' cron.rm_job root /usr/bin/foo dayweek=1 """ lst = list_tab(user) ret = "absent" rm_ = None for ind, val in enumerate(lst["crons"]): if rm_ is not None: break if _cron_matched(val, cmd, identifier=identifier): if not any( [x is not None for x in (minute, hour, daymonth, month, dayweek)] ): # No date/time params were specified rm_ = ind else: if _date_time_match( val, minute=minute, hour=hour, daymonth=daymonth, month=month, dayweek=dayweek, ): rm_ = ind if rm_ is not None: lst["crons"].pop(rm_) ret = "removed" comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return ret rm = salt.utils.functools.alias_function(rm_job, "rm") def set_env(user, name, value=None): """ Set up an environment variable in the crontab. CLI Example: .. code-block:: bash salt '*' cron.set_env root MAILTO user@example.com """ lst = list_tab(user) for env in lst["env"]: if name == env["name"]: if value != env["value"]: rm_env(user, name) jret = set_env(user, name, value) if jret == "new": return "updated" else: return jret return "present" env = {"name": name, "value": value} lst["env"].append(env) comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return "new" def rm_env(user, name): """ Remove cron environment variable for a specified user. CLI Example: .. code-block:: bash salt '*' cron.rm_env root MAILTO """ lst = list_tab(user) ret = "absent" rm_ = None for ind, val in enumerate(lst["env"]): if name == val["name"]: rm_ = ind if rm_ is not None: lst["env"].pop(rm_) ret = "removed" comdat = _write_cron_lines(user, _render_tab(lst)) if comdat["retcode"]: # Failed to commit, return the error return comdat["stderr"] return ret