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/states
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/states/boto_s3_bucket.py
""" Manage S3 Buckets ================= .. versionadded:: 2016.3.0 Create and destroy S3 buckets. Be aware that this interacts with Amazon's services, and so may incur charges. :depends: - boto - boto3 The dependencies listed above can be installed via package or pip. This module accepts explicit vpc credentials but can also utilize IAM roles assigned to the instance through Instance Profiles. Dynamic credentials are then automatically obtained from AWS API and no further configuration is necessary. More information available `here <http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_. If IAM roles are not used you need to specify them either in a pillar file or in the minion's config file: .. code-block:: yaml vpc.keyid: GKTADJGHEIQSXMKKRBJ08H vpc.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile, either passed in as a dict, or as a string to pull from pillars or minion config: .. code-block:: yaml myprofile: keyid: GKTADJGHEIQSXMKKRBJ08H key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs region: us-east-1 .. code-block:: text Ensure bucket exists: boto_s3_bucket.present: - Bucket: mybucket - LocationConstraint: EU - ACL: - GrantRead: "uri=http://acs.amazonaws.com/groups/global/AllUsers" - CORSRules: - AllowedHeaders: [] AllowedMethods: ["GET"] AllowedOrigins: ["*"] ExposeHeaders: [] MaxAgeSeconds: 123 - LifecycleConfiguration: - Expiration: Days: 123 ID: "idstring" Prefix: "prefixstring" Status: "enabled", ID: "lc1" Transitions: - Days: 123 StorageClass: "GLACIER" NoncurrentVersionTransitions: - NoncurrentDays: 123 StorageClass: "GLACIER" NoncurrentVersionExpiration: NoncurrentDays: 123 - Logging: TargetBucket: log_bucket TargetPrefix: prefix TargetGrants: - Grantee: DisplayName: "string" EmailAddress: "string" ID: "string" Type: "AmazonCustomerByEmail" URI: "string" Permission: "READ" - NotificationConfiguration: LambdaFunctionConfiguration: - Id: "string" LambdaFunctionArn: "string" Events: - "s3:ObjectCreated:*" Filter: Key: FilterRules: - Name: "prefix" Value: "string" - Policy: Version: "2012-10-17" Statement: - Sid: "String" Effect: "Allow" Principal: AWS: "arn:aws:iam::133434421342:root" Action: "s3:PutObject" Resource: "arn:aws:s3:::my-bucket/*" - Replication: Role: myrole Rules: - ID: "string" Prefix: "string" Status: "Enabled" Destination: Bucket: "arn:aws:s3:::my-bucket" - RequestPayment: Payer: Requester - Tagging: tag_name: tag_value tag_name_2: tag_value - Versioning: Status: "Enabled" - Website: ErrorDocument: Key: "error.html" IndexDocument: Suffix: "index.html" RedirectAllRequestsTo: Hostname: "string" Protocol: "http" RoutingRules: - Condition: HttpErrorCodeReturnedEquals: "string" KeyPrefixEquals: "string" Redirect: HostName: "string" HttpRedirectCode: "string" Protocol: "http" ReplaceKeyPrefixWith: "string" ReplaceKeyWith: "string" - region: us-east-1 - keyid: GKTADJGHEIQSXMKKRBJ08H - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs """ import copy import logging import salt.utils.json log = logging.getLogger(__name__) def __virtual__(): """ Only load if boto is available. """ if "boto_s3_bucket.exists" in __salt__: return "boto_s3_bucket" return (False, "boto_s3_bucket module could not be loaded") def _normalize_user(user_dict): ret = copy.deepcopy(user_dict) # 'Type' is required as input to the AWS API, but not returned as output. So # we ignore it everywhere. if "Type" in ret: del ret["Type"] return ret def _get_canonical_id(region, key, keyid, profile): ret = __salt__["boto_s3_bucket.list"]( region=region, key=key, keyid=keyid, profile=profile ).get("Owner") return _normalize_user(ret) def _prep_acl_for_compare(ACL): """ Prepares the ACL returned from the AWS API for comparison with a given one. """ ret = copy.deepcopy(ACL) ret["Owner"] = _normalize_user(ret["Owner"]) for item in ret.get("Grants", ()): item["Grantee"] = _normalize_user(item.get("Grantee")) return ret def _acl_to_grant(ACL, owner_canonical_id): if "AccessControlPolicy" in ACL: ret = copy.deepcopy(ACL["AccessControlPolicy"]) ret["Owner"] = _normalize_user(ret["Owner"]) for item in ACL.get("Grants", ()): item["Grantee"] = _normalize_user(item.get("Grantee")) # If AccessControlPolicy is set, other options are not allowed return ret owner_canonical_grant = copy.deepcopy(owner_canonical_id) owner_canonical_grant.update({"Type": "CanonicalUser"}) ret = {"Grants": [], "Owner": owner_canonical_id} if "ACL" in ACL: # This is syntactic sugar; expand it out acl = ACL["ACL"] if acl in ("public-read", "public-read-write"): ret["Grants"].append( { "Grantee": { "Type": "Group", "URI": "http://acs.amazonaws.com/groups/global/AllUsers", }, "Permission": "READ", } ) if acl == "public-read-write": ret["Grants"].append( { "Grantee": { "Type": "Group", "URI": "http://acs.amazonaws.com/groups/global/AllUsers", }, "Permission": "WRITE", } ) if acl == "aws-exec-read": ret["Grants"].append( { "Grantee": { "Type": "CanonicalUser", "DisplayName": "za-team", "ID": "6aa5a366c34c1cbe25dc49211496e913e0351eb0e8c37aa3477e40942ec6b97c", }, "Permission": "READ", } ) if acl == "authenticated-read": ret["Grants"].append( { "Grantee": { "Type": "Group", "URI": ( "http://acs.amazonaws.com/groups/global/AuthenticatedUsers" ), }, "Permission": "READ", } ) if acl == "log-delivery-write": for permission in ("WRITE", "READ_ACP"): ret["Grants"].append( { "Grantee": { "Type": "Group", "URI": "http://acs.amazonaws.com/groups/s3/LogDelivery", }, "Permission": permission, } ) for key, permission in ( ("GrantFullControl", "FULL_CONTROL"), ("GrantRead", "READ"), ("GrantReadACP", "READ_ACP"), ("GrantWrite", "WRITE"), ("GrantWriteACP", "WRITE_ACP"), ): if key in ACL: for item in ACL[key].split(","): kind, val = item.split("=") if kind == "uri": grantee = {"Type": "Group", "URI": val} elif kind == "id": grantee = { # No API provides this info, so the result will never # match, and we will always update. Result is still # idempotent # 'DisplayName': ???, "Type": "CanonicalUser", "ID": val, } else: grantee = { # No API provides this info, so the result will never # match, and we will always update. Result is still # idempotent # 'DisplayName': ???, # 'ID': ??? } ret["Grants"].append({"Grantee": grantee, "Permission": permission}) # Boto only seems to list the default Grants when no other Grants are defined if not ret["Grants"]: ret["Grants"] = [ {"Grantee": owner_canonical_grant, "Permission": "FULL_CONTROL"} ] return ret def _get_role_arn(name, region=None, key=None, keyid=None, profile=None): if name.startswith("arn:aws:iam:"): return name account_id = __salt__["boto_iam.get_account_id"]( region=region, key=key, keyid=keyid, profile=profile ) if profile and "region" in profile: region = profile["region"] if region is None: region = "us-east-1" return f"arn:aws:iam::{account_id}:role/{name}" def _compare_json(current, desired, region, key, keyid, profile): return __utils__["boto3.json_objs_equal"](current, desired) def _compare_acl(current, desired, region, key, keyid, profile): """ ACLs can be specified using macro-style names that get expanded to something more complex. There's no predictable way to reverse it. So expand all syntactic sugar in our input, and compare against that rather than the input itself. """ ocid = _get_canonical_id(region, key, keyid, profile) return __utils__["boto3.json_objs_equal"](current, _acl_to_grant(desired, ocid)) def _compare_policy(current, desired, region, key, keyid, profile): return current == desired def _compare_replication(current, desired, region, key, keyid, profile): """ Replication accepts a non-ARN role name, but always returns an ARN """ if desired is not None and desired.get("Role"): desired = copy.deepcopy(desired) desired["Role"] = _get_role_arn( desired["Role"], region=region, key=key, keyid=keyid, profile=profile ) return __utils__["boto3.json_objs_equal"](current, desired) def present( name, Bucket, LocationConstraint=None, ACL=None, CORSRules=None, LifecycleConfiguration=None, Logging=None, NotificationConfiguration=None, Policy=None, Replication=None, RequestPayment=None, Tagging=None, Versioning=None, Website=None, region=None, key=None, keyid=None, profile=None, ): """ Ensure bucket exists. name The name of the state definition Bucket Name of the bucket. LocationConstraint 'EU'|'eu-west-1'|'us-west-1'|'us-west-2'|'ap-southeast-1'|'ap-southeast-2'|'ap-northeast-1'|'sa-east-1'|'cn-north-1'|'eu-central-1' ACL The permissions on a bucket using access control lists (ACL). CORSRules The cors configuration for a bucket. LifecycleConfiguration Lifecycle configuration for your bucket Logging The logging parameters for a bucket and to specify permissions for who can view and modify the logging parameters. NotificationConfiguration notifications of specified events for a bucket Policy Policy on the bucket Replication Replication rules. You can add as many as 1,000 rules. Total replication configuration size can be up to 2 MB RequestPayment The request payment configuration for a bucket. By default, the bucket owner pays for downloads from the bucket. This configuration parameter enables the bucket owner (only) to specify that the person requesting the download will be charged for the download Tagging A dictionary of tags that should be set on the bucket Versioning The versioning state of the bucket Website The website configuration of the bucket region Region to connect to. key Secret key to be used. keyid Access key to be used. profile A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. """ ret = {"name": Bucket, "result": True, "comment": "", "changes": {}} if ACL is None: ACL = {"ACL": "private"} if NotificationConfiguration is None: NotificationConfiguration = {} if RequestPayment is None: RequestPayment = {"Payer": "BucketOwner"} if Policy: if isinstance(Policy, str): Policy = salt.utils.json.loads(Policy) Policy = __utils__["boto3.ordered"](Policy) r = __salt__["boto_s3_bucket.exists"]( Bucket=Bucket, region=region, key=key, keyid=keyid, profile=profile ) if "error" in r: ret["result"] = False ret["comment"] = "Failed to create bucket: {}.".format(r["error"]["message"]) return ret if not r.get("exists"): if __opts__["test"]: ret["comment"] = f"S3 bucket {Bucket} is set to be created." ret["result"] = None return ret r = __salt__["boto_s3_bucket.create"]( Bucket=Bucket, LocationConstraint=LocationConstraint, region=region, key=key, keyid=keyid, profile=profile, ) if not r.get("created"): ret["result"] = False ret["comment"] = "Failed to create bucket: {}.".format( r["error"]["message"] ) return ret for setter, testval, funcargs in ( ("put_acl", ACL, ACL), ("put_cors", CORSRules, {"CORSRules": CORSRules}), ( "put_lifecycle_configuration", LifecycleConfiguration, {"Rules": LifecycleConfiguration}, ), ("put_logging", Logging, Logging), ( "put_notification_configuration", NotificationConfiguration, NotificationConfiguration, ), ("put_policy", Policy, {"Policy": Policy}), # versioning must be set before replication ("put_versioning", Versioning, Versioning), ("put_replication", Replication, Replication), ("put_request_payment", RequestPayment, RequestPayment), ("put_tagging", Tagging, Tagging), ("put_website", Website, Website), ): if testval is not None: r = __salt__[f"boto_s3_bucket.{setter}"]( Bucket=Bucket, region=region, key=key, keyid=keyid, profile=profile, **funcargs, ) if not r.get("updated"): ret["result"] = False ret["comment"] = "Failed to create bucket: {}.".format( r["error"]["message"] ) return ret _describe = __salt__["boto_s3_bucket.describe"]( Bucket, region=region, key=key, keyid=keyid, profile=profile ) ret["changes"]["old"] = {"bucket": None} ret["changes"]["new"] = _describe ret["comment"] = f"S3 bucket {Bucket} created." return ret # bucket exists, ensure config matches ret["comment"] = " ".join([ret["comment"], f"S3 bucket {Bucket} is present."]) ret["changes"] = {} _describe = __salt__["boto_s3_bucket.describe"]( Bucket=Bucket, region=region, key=key, keyid=keyid, profile=profile ) if "error" in _describe: ret["result"] = False ret["comment"] = "Failed to update bucket: {}.".format( _describe["error"]["message"] ) ret["changes"] = {} return ret _describe = _describe["bucket"] # Once versioning has been enabled, it can't completely go away, it can # only be suspended if not bool(Versioning) and bool(_describe.get("Versioning")): Versioning = {"Status": "Suspended"} config_items = [ ("ACL", "put_acl", _describe.get("ACL"), _compare_acl, ACL, None), ( "CORS", "put_cors", _describe.get("CORS"), _compare_json, {"CORSRules": CORSRules} if CORSRules else None, "delete_cors", ), ( "LifecycleConfiguration", "put_lifecycle_configuration", _describe.get("LifecycleConfiguration"), _compare_json, {"Rules": LifecycleConfiguration} if LifecycleConfiguration else None, "delete_lifecycle_configuration", ), ( "Logging", "put_logging", _describe.get("Logging", {}).get("LoggingEnabled"), _compare_json, Logging, None, ), ( "NotificationConfiguration", "put_notification_configuration", _describe.get("NotificationConfiguration"), _compare_json, NotificationConfiguration, None, ), ( "Policy", "put_policy", _describe.get("Policy"), _compare_policy, {"Policy": Policy} if Policy else None, "delete_policy", ), ( "RequestPayment", "put_request_payment", _describe.get("RequestPayment"), _compare_json, RequestPayment, None, ), ( "Tagging", "put_tagging", _describe.get("Tagging"), _compare_json, Tagging, "delete_tagging", ), ( "Website", "put_website", _describe.get("Website"), _compare_json, Website, "delete_website", ), ] versioning_item = ( "Versioning", "put_versioning", _describe.get("Versioning"), _compare_json, Versioning or {}, None, ) # Substitute full ARN into desired state for comparison replication_item = ( "Replication", "put_replication", _describe.get("Replication", {}).get("ReplicationConfiguration"), _compare_replication, Replication, "delete_replication", ) # versioning must be turned on before replication can be on, thus replication # must be turned off before versioning can be off if Replication is not None: # replication will be on, must deal with versioning first config_items.append(versioning_item) config_items.append(replication_item) else: # replication will be off, deal with it first config_items.append(replication_item) config_items.append(versioning_item) update = False for varname, setter, current, comparator, desired, deleter in config_items: if varname == "Policy": if current is not None: temp = current.get("Policy") # Policy description is always returned as a JSON string. # Convert it to JSON now for ease of comparisons later. if isinstance(temp, str): current = __utils__["boto3.ordered"]( {"Policy": salt.utils.json.loads(temp)} ) if not comparator(current, desired, region, key, keyid, profile): update = True if varname == "ACL": ret["changes"].setdefault("new", {})[varname] = _acl_to_grant( desired, _get_canonical_id(region, key, keyid, profile) ) else: ret["changes"].setdefault("new", {})[varname] = desired ret["changes"].setdefault("old", {})[varname] = current if not __opts__["test"]: if deleter and desired is None: # Setting can be deleted, so use that to unset it r = __salt__[f"boto_s3_bucket.{deleter}"]( Bucket=Bucket, region=region, key=key, keyid=keyid, profile=profile, ) if not r.get("deleted"): ret["result"] = False ret["comment"] = "Failed to update bucket: {}.".format( r["error"]["message"] ) ret["changes"] = {} return ret else: r = __salt__[f"boto_s3_bucket.{setter}"]( Bucket=Bucket, region=region, key=key, keyid=keyid, profile=profile, **(desired or {}), ) if not r.get("updated"): ret["result"] = False ret["comment"] = "Failed to update bucket: {}.".format( r["error"]["message"] ) ret["changes"] = {} return ret if update and __opts__["test"]: msg = f"S3 bucket {Bucket} set to be modified." ret["comment"] = msg ret["result"] = None return ret # Since location can't be changed, try that last so at least the rest of # the things are correct by the time we fail here. Fail so the user will # notice something mismatches their desired state. if _describe.get("Location", {}).get("LocationConstraint") != LocationConstraint: msg = ( "Bucket {} location does not match desired configuration, but cannot be" " changed".format(LocationConstraint) ) log.warning(msg) ret["result"] = False ret["comment"] = f"Failed to update bucket: {msg}." return ret return ret def absent(name, Bucket, Force=False, region=None, key=None, keyid=None, profile=None): """ Ensure bucket with passed properties is absent. name The name of the state definition. Bucket Name of the bucket. Force Empty the bucket first if necessary - Boolean. region Region to connect to. key Secret key to be used. keyid Access key to be used. profile A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. """ ret = {"name": Bucket, "result": True, "comment": "", "changes": {}} r = __salt__["boto_s3_bucket.exists"]( Bucket, region=region, key=key, keyid=keyid, profile=profile ) if "error" in r: ret["result"] = False ret["comment"] = "Failed to delete bucket: {}.".format(r["error"]["message"]) return ret if r and not r["exists"]: ret["comment"] = f"S3 bucket {Bucket} does not exist." return ret if __opts__["test"]: ret["comment"] = f"S3 bucket {Bucket} is set to be removed." ret["result"] = None return ret r = __salt__["boto_s3_bucket.delete"]( Bucket, Force=Force, region=region, key=key, keyid=keyid, profile=profile ) if not r["deleted"]: ret["result"] = False ret["comment"] = "Failed to delete bucket: {}.".format(r["error"]["message"]) return ret ret["changes"]["old"] = {"bucket": Bucket} ret["changes"]["new"] = {"bucket": None} ret["comment"] = f"S3 bucket {Bucket} deleted." return ret