JFIFC   %# , #&')*)-0-(0%()(C   (((((((((((((((((((((((((((((((((((((((((((((((((((" ,.Fh Ch@ 10D``DBB h4 @dX bD iD ІI$TBB'$"`I)Eb`(m9@0hb&!1114  b` Dh "lTH)TAiN  A" hf%n£!aY4hcC"5J2#Tզ@ #(a`QI+JHB8h@!!!hSMNhC4$11SB!`&2Dc(p*`"XE b!IJ&0C41 b `hL0JHLi1L -XX`ݚb% )*Cp& ! $40)!b䜢hC@D 6JJቨ4B!`b `0@ b`&ё^IÆ LO7dX h@)A "I`6H !L'@ DQ B!Bj4  L@ @hb&%$ D LQ~7ҜtZ&pӘ b `&)F؆` 7DBB&qI:LVF2B1 5iL4$ mj4 @ @ b`0b iS` 14V1l˦I7 @` L&ȒB[lC!FlIY +@!"!%$  HX J00CبDE18! L r2ϳ>Tس:=8Ӓb  & !`) "0$EMSIAL6D$B`&BBX&1C CT4h! @@4 0Yf |,tCE\T}nn` b$1AN&$ &IS`0118` 4  9_^8B14yꞿ3wlK 7 &@ 0@ @ `Ȓ b( +$2DR:]Z3cqcAȴNb11@#@18b`!upyt|z8lZ+]}3:zKcwA9SUU5AJ   2LUp*HR+EUEvF2qIW8)-JYDUQ  b `16!B& n$I9y~yntpX"QE,m[&C44 b ``@  BQ0&:Qud J7*"S-5(J7U@`  b1n.2/| ZrJY]3~ڕyצ1Ͳʬ3}[9NΨWVun}Tc~g6g=Mq6}GKsx b``L!nu"6ڬQ}_4 4IMtSҫ(610 b`],k4r:\_GOn骻q[,C*ͳԖzhUݐ9w L01 L& hQm(4d]nNiF wfG&ܱx*uθIbBʤSnܢaFj(@`8箄Ꝿ&IltgxgɻM%Mږ{z)]vSqUټ& b`0CT 8&`% '** -L/(4$cךRjp.h @1b!  0n7ʮB Kt}UF˞tr\7Jϖ~%Ҹ[!hUqp!&7Č1] *O4צN.Ǽt0!J%S101CC&1 Lh b bey ?fW7Ƨ,ʒ2t}֚m[PzvvF@ʀ  hb @%(#!!bBâM4BF=x Pցdd'YS̷ͬ 118h`bSv\>}Ux/ޝ7UI5h,pܞ^[U9=&v8@I!(XjaS,S3]av(KWP4j` -#ݒ7Jն&W"1t!^0 ! &X2y=yomNz.zVwfKݚж26ϗMa5L0C]q$8EQTl;yj]\U:znT62U f%uLb!  o7Q/{jyϣCJgS[oޮOO>_W6O~oC,2T`44\3zc(B A\cuݛU4컗AK2B6vǷ\n9WXQ,y:Bz` `4 @C&r_'RdxyNu <SQUM+#S⎬7v㦩K]Jy:KX5b`!!)*d 1RYn+έӚKUJX7U3˟EA}lŪe6@@  b b4x2\>|z^WvB{3^S׺Np^kέ㜅VզhW6rw{xz=)@h+ !daYZC.~mQniڲ7|0Qgj_J}l;8Po)Ά>4 @dtsNqОgͷ>ǻ \T`ыfNf7(pu9|]͙c{#(h1 @ @}6yn;*SHI*Bj"9̻&{y]4գ7>Wf~םZ0niMRsTH/NL` @ `/9ywVY-tkZJ~sGCz|z[cV-KX+csSTWu6kK2"2QiM b6y֝^]k׍ʻK=U**MVK2R.ZE9}v6{i1m]jZҌRUJ)De%dR*K~eS>-у$eͮsuκh%lGNl8#~:n5Yߎqf?L'@ @ojȲ*d.ܴn3q$ngլNKbS%{ߓ\qM(zOk=R͕zX_~=hE'J]\YA&]ƣLk4>5tdUFm8ʋ+7T+K-%3oU]kRKV=cNjkCiGY)s󝧂뫟CX=na\^ RgOA5F|-P_ew9jWM;暜Q}rUh;p_>|+ng<%̙uӧ>phss.SE67FH[W+8sc<=3Z_FJ^Mz('.Rǖ=<}<=hr7Z6v"pV-:jS٩}vf2UeYN\K JN*|y.!~O{ k#;1rt݃:>8sVL]*gs*-dY*Wdnb b&@?=1Ms*|ZW3VY.+ӋcSZg EWfgvZNDeSBWʋ$ӟLu?CԎvܚ/\hُR]zu3&UWZRvj^l[֢3u[ةZ2=Ox]wԥΛbyu͝p뚫3UsaVX;I>7~xgpa;_կM5yĔ1dD׳<K}*D&P&@18{N]n)E=Mg_811YGE) "J cMQ]e3>_Q=:f]IzTQS US-izΛ$Iv3Q]]JM$[VT *N5-eBHJO<侴euRVzseOv--m(JƬi`jKڹW+n}1Z^.sLyq9}4/sw@ZH!]M&y،l-nq沯Ets'mi9E: Q"Z 5ֽC^mkV[ʝ>]3n2,#\B `T(U6-N,gF~&[bB^w*<=UÎ+mBePW:IPڪ7䫲anm J0 Pg=iQpڎz\~-kRqXl9]O.w}Ku&kSuHS $BRee:̢r fnYmSE9Hr3PQuVE 6AM "vty|yU.Y!nm4kqB.N4UdF鶫,qLں[e ⒅kYknpwBϓU>^Ѳ+214E8,:"=YվٛG\N{UǭJ1؆( -Rd [ۏͣ1f^6%fF$sB̠YUӲs]0 &\Z\_dL)f{!f7}6_w5SYŵUUYe]=73uԌybv#3]ё+fXx?ί'jĪZ'KZCOmVg ٚ5![omjbїxue ؒuU̔g5ziW:7':]Uˎ:ur;ês솅Dq#$BGVQ}cWQd.ŋZ5yrhgg^1ʎxGo|u?=%[V63fH41ӿFBwwnlӯǵ*vp$FJdi::qӏ^|{sF5skb+b;+ɳǽy9mIAJ1ɚz9j]<+htU!lNZ`tafcʍ4⁳G/LJ|TZ5%TͲBLSd-.ط%ؓ5ˡæRdĉV bc@$::v֋oV\fwtr~.V:2.8n.YX͎hk1.Jvտ}ڸm볧-%\s^Lݾ}fƥ<;9 o-^,/B9T,ųXҬ o,4 hxiӛfR-zlFfR&oSG/G=fl"#o %$4W٫#1e;Y(62+W4:lt#:;1[G3YfzseN8dًI8Oy@ԉ``&!#8Hs3_OFRثRulvth;Ì:dl @TqVR* ˣnsuX4%y:f2h]KƣVi%:f'w?LkU?,iÑIg]B%6aUiUg&>zuƧM_5^^Z役:stNg\Y+6ٞEֹgZγV5vkD-d=y55(&: F%`Ȏ-@ 9}l|dNPGDWmp%܍=mbZFlӺ23jqъuپY|| FxiP+$'*싶M+oEșPBf x8O;)3:!319t5!K kϥ:o 鞖3;=QY٣ܘ0JCM`I5f|֭sb)[b6xe8Ne!Bq2c8&(Nv񺭁TmdB6AI"^OOA(D#4o,i󶞼 ^ϯɽEz{κչ  J2# J0lewn~̚!)N(џLbU9:x}qҲ6m~/LmҘ>F蛖޿q]V FbRF|qV]ب5ltO՜&e\u5N\&\تP ʕ^dKN}!F'3ԌIT-!Ќ\%||&zcy].:yٿ,n㨍vL1I"5I4ЇJ+y_4t[Aݦ>f:i2\2eP۱kqED1g۟NxǫOMJ4uH\EūB ]I!["IHl>GW t0peEN]2_g:nm#7S{qR7.ŲAVL,qhJ A$n,iօ7>]0g3MiKkK^#PJ8@LjVD,kU yz̪|NKυI@.v}5wy}~cLIWw!o )E(JT1RjґW{!#4}g(CD%bJ+WKO+ &3doFtr걤Zabb!ͫ7%ѯךU-Ăj*ÿУTҷ=|<=X[q6*iC"(d'"$- yyTnh-|z]fSn'dZ1Ky} />u_3\8 Nz8~GLP;iHvL@`SM"1`8x`q/mAI}E9qOןơ^r2U`JP,cBkW!$I)d+bܩir+уXJ-)~tc>&ĂVB-K_?z$. h0R)F@9"ʑe>\z\;5P:M9u9ɮsaOz{qҬsq6ȦN@gm ;\$8' #R#%M_28ІU[j,#"˟P=++| g!4n^䪶 i5P$ϮYCc`Wr^010#Њr3$H ۀ29# ?ӯ ,q=ی;G0O,, 4A@83s3o !<5-׼ 1?430D$a ;8cO4 ̲9G&o4 1ͫ?8<3w>9? 6 8E Ǡ~ߙs,< ,/1\O8<:Հn:,ӽDb.4'8+Jr<<9]+rˑ0 <8"CP/ < s c?2<O;x7}000 Á(N5M0ϯFo<Q!w0 # L4Ҏ +1`=LѨAuM 8 @h  Ϊg0[8d_o|n00 8 whhtS/-ŸsC8 0 07o8$ڍ"ʘq{ T2ѱa0sFsrљu[ ?Nz2"8fɒ{Oc1+3vzM|"D:I}KYaLω` 0 G+(+f?)ŖR+}0q@{1'7#:w4VO0 $βէFS4LBer JeN*/ =A1=$l\Ӯ@j.檄kz%eqe^PU콹4x=3` X?Rʺn.Z׍x)y"ř?21l6oW5O䐘eނ͠@{B2y^%kZ*ogxBVW`h9mh]zXX,нP,ۍ44&}=fJ4E6~JC 06}+n'Ui1᠗$ClLE՝)[T@Ub̶&R3[gXPB =J(B41|xs}Px蒲@[5"J۲syo#$;X#L z\,;tEfwҸ,=ěeӽ'O (7=u~*"x(Q$I0Nm5ͬz hEb0?%0+l2ͻXl RH#rA/TmXb̪?>޻|P:}f}Sb*QnW4{5\@9I{;MWjMxs1;1dY~>r[WRlW2 UսKzrIv6G'1gglOrm"(zLfo`Tx0fbhmNW= [c3 $'4jy32`$^vԩWW|[|{TFg4CPaڝ {X6]0[Ö4W`'LqϊJ.,3U[1[v Q!!FuZe$ eQw?ieg]TL-N @X-nqBٸGV'd H- 47O3y=Q ,swwF%"wXMhO{5! p:;K(o;1O6`.9I~hŶͱ]Yqưpmaƾk^'y; S!",`8t侑5qGZw)Ayw/<^?Oz1tӪ($S]n91#T2yJφ |R|3sJ(]U+G{a&Pd>i6ClR|2Ռ7Cgخurڛgs.3uo=p,!5bh-?KM)UzUk81ְ` ZIy6 qJN-ե5ymχ7cl,iX .CR oz⯫y/R褻kPp20%˄c`6HapC[q7C(Dz0DG ϴu{m[˯Ac" i;?vGms$יg,h?(sc}^?Z׼s8&IGhDm?Kosy[r)| Cq{د}4} /{ePE4]s։* -?ۉ붻]:+m'Wum}njj(l*J }mKVT~ 5o|YYLPK,"jC*6i<}}mg*ޏ<2cs|㐓bI/v}Hvw gM$YQm<}}}}ڡ, 5`na%mv}]}UhQۼu,0<2}5uSU[-3lMUQ }d[a-9qLԻƥŻIu<<}g}mD#͓[}3qԗq[\^|+ (ŵP }qqe5=߷ (Bt597=#a*8^ȁ 2y`Åqi}<887w]Գ=xQD\}Dr)XI 1ϻ( cK<u6YqJ|4tu<o0[$-| 4&'=M}R&ʨLs.Uij M\_҂B!wh,o3g]o4Q7u ?o.\o(iˣ&CpLTz7ʙqoyv1 ԄajAĕ]ȘYB1/&aжiv1$J]I1ڂ#y ہ+-AiVmmRYY̺}S*_އ #׭qrBv YR̿XB=kqYH8Dvq%=j1 PUjץ0,#>!Tף,8Ns|i,:$BWpہ0NNʼTrJkY?4@K_oYa @1沩(SgaA4Q6HwF!J`7pVhָe"֬jO>$,JdLTf9BV;(L\ h7 6: /[)+R1.?`2UM|r*Mѫ/-?H@l!M*"% d͖<HbģHo몯H^nTG[-9#%9I"9MԾtd%yhGN Z`˿LJܘ1 3Zޥ0ږ[z hfɎ$X览N7a./m ՖG]8_:)]`9xw(F.&n$6NJ?[^F GYdn΄΋9>z nf`~@lֽL&".qj'1q8hWݎ's@;B ;fdBy|q=S$`RD>]F ig ^%"MHt4SIK+fe Tnf/޳tdy%[1!Jdx'@^PMoxMS{TPfB|^*}'sUC-JA!AFx(i؟.C` \nx<졆|nxYPd(n`/fL#2t>#DũE^?кq OmNkoȚ6Y?7*&-BA0QRj`鋘LϡL61O{˗&T܊TΛ7 q5tfԯ09mKxt\6j0"4x/\ҙ UL}%jXƄ QYgк87d]G#aPJHrCN\xKg 3]Jy1`\` Ә![MͅX\ΖABn %̃rd@fL*tf]>]x*G~|ˀ`1|>;;_`GEqIؔ嚜 o3TrUsqǣĭ`FC1No}~`?52%f o5P  B楩I<$̙G.4v|ͬa,U+)7v1yP&_6WcBa1g$љWx5G!TxHRbL>}UȈ26KNV_OAk-eT~0 ""3›O1Pg(>L<^F,hF㘘ټËlauVGW5$Švչ6b.3N?/4Ow!~& <~0"1rvb Qf0U5Ax=Fo3S1z9f|1/&E(q3dq1&F #(Ua<DM@Af.gI똏fnD$;2?05(B A) >0Kp|~ED6EFZFv癩>Pڥ0 ӛf. Tk3mGVc0Qf;,ƥT`B"ߖ7*s iN*3("U FbqBbZ - ma_\X5 3.(drb;R`@58q@T&bfj\|{Tӹ$4AهB9itbĪہ̮D{ud\%jc5Ɍw,Le"m̠\xꙮkC ʔLD鷕x*D1(~?P& f&3Y1[c`J LzARspa\|t(TWJlKT"z 3 +ɨ80&]>Mg;0Ll&Ll61Z { MRSn(-=:fP& 8]!ryI'U,ynX\ n?sB9$̈́ u6`6/Y3Sd%1)w< 54QO8nYSMd&@k&<[DkcQ>&\ [%N:VA g08AyG淟R4qډswD:AOyDm<*\Mg3zA0c"țDɄf. ,jH#U6;U}1M 83y|kcBk889?Lچpd4L\W 2g&SLue~2-2!¦"Ӗ]:)&m;4bA?0k8d?)}AkV,&E(hڅ۸2S5Aq0F"UbYө䉗JF:6ry.6CMw> b&'5M{P|]T}8_3P,X Bc{A(7g9кӅr(55HDRiS~I:M_Ǐ  ZÕZ-k54ZV3Mg2/̹ڢiYԊD͐c]#6чu>Lv"pAFaJh]Q,MFfܢ&)>@CG(X%ߒhF(m.U?i.q.مw2c('kXIcbZkXD&#lGu g\)KlP#B`P[y}sR(UpAn%MW>fqԳM/|5FOm?ٷbx*ٚ-X9BP16ճ0ԲPP^cuyLF*e Ù­ -Ar8ybf&l$>!WQHQ.TIL ,?y"zmճ65Vq|L_"Sgϸ 72Ϸ#5Rܾb{["R>#9&e0Nf|6s5 ]?3:`"Γm@'gfLyybXq #=7VsE2%ĠG,y,n~"T~ _eXt^Q70ٝA7%ÑAf%3Vr(ij7@{_a_}od橶bdgGGɈlw˜' u.fYRh96i[9!>2p@*1&ELf`B~TM3F᱂\N`J AAH |e0sPJ|1>'L1L t nLQ cqsLUIQu4^a )u; о`vNbJP!1&Œ,.Rݳe(Gb}ޠ06mŒ`Q5ܙ#[`hP 6 cu 36`־& :3>SmxuxNMFdԐ9$G 4;++!>L(bdGRG pgʟ)3cܻ ~e3Hdiѷ)"T??ŋU"aENbw .SP@@_&l*1<8arcj/POܰ!48v ˊ|w3PrՄmCju594"~Q:LA[&-ϊc:f#YQ)="T{ajW><>D8[ǃz ExF6IOb"/"e֜965 &x&c̍h ~X9|K`01gm; 0md<Ӏ*3 >%]%Ӷ>4\AC85LY|@(vu{]:d5c::VzjzF]g.1dM3]bQn* "'6Vӯ28 e? Lu3IcaZ|T؉fm5:gyd9Rq:nǗ0:6ZvsB`G˘Lh8Oèf^Jk깠kMԸ~"nCop&. mS̹p{3b\ML;1|op܍l~_~&7<+\4g a+2 k_ߙ[ S7"ĚU@ѬGP&=7]=(!>cdːa,:~\r?i>ij28̵=<vm<)pc `1CهX õ{0t91 _`'cQf(\ehP" #YA>Q>yzS7Jy 1X Bp8s,v|G";_1+ks7#LZpڐ ?81|+fB\OB8<aMf*ȃϻo~5yֻ$ïb91LA1{xU5") E`+byg<\_lZ3aE c`;O0f,KNDd0XHN3tW3 OlHCaoUܒ&"Pه[2 FZW0'KWOt/ A δ 㹇E5L,JJ6DmZcdtRf w'1h_bc|ith\h/{XWfg`r#v=s<\ĻlkRkaٚţO:[鿿ogʡ<|h1TqNau3.,Y`c! Di3qۙaVh~ˆE<4бWf A{g2rL>8ljocMCAMO ';Q|Tv8&h5nqW IUdO\9P6y<fG&OT|8А&-22fp\tl~4zllCq] L9wB tiX\Fܠo~h?y/~AP*~ OãR(q` SfKN gfE]4hɈ6c Bkܮ3p=; DBAg0? =ˁS|Kt2ci4F3gJpee˪*~qwՐۏa4b1}S55 Cs EbE˸Q#4yCv{L^%XЈN6 ǭª&H*qsWv+gFuAƢy)MfhB2@PC '%}k"Lϑ9"z]BUΝ5@9&5';\>%H;u tۄ8V`zo3{@>'_l6dG+f:;A]BM;@D87"u@r2}t[ ¥4ll&>r!\O6&}n0!=`8'pftؾ=k1Yf(|uOrZn4(cuQɞ /L - T؊ۅ<ZEb*~&vߐ%+Fa*YSFM7/n&d5&i\>0@c"h#h n` `8 >=Ȍ(u`RǑ46`4{&R(H``7 abo"`ݻ5,('7j =5f\ '``g1|L˜"Vi[3HbT1g>`|č米?5_r~IjY 2p=kX0L4jLP-!Pr/gQm37}魻N}Ri`C4ŋ6,]R=ӷL#)(eF'%i&0L| 2UOULjliIP|b:}Bd_]vjvSCWe5$Q0>6!1A "0Q2@a#PqBR$%3?l/;?(g=T3iMm#D =>J~¿h,%_\rB>Q_qSMi3*:t(h{TR|aYR[oϧESFZ5`ܿ07a_8")&])5cbzԯF7KGz(JHP(F3X>?T6ʄJJޞJ dͰp&a)x]R~7NɘY18hHRĝL|2~#갢Sn<ً1ѓr]ٴq'>[\LoQ`צeBTf[ٌxmcgr`_ؾ!ݐ660-EQ  Ɂg@SC^&\z'Q8B= a?)?P:U?N@*>4}BrgX:;\N7jМ QK&ZNܯT6a6oa㸍練0d8E+`rVuhhhD3q=x멯oݙQfg<x?ӦÉV2?=`͟H$DXt`?TEeS'5g !{Aw~O2k'%8?6 1bmxls48>Hx55T[|G"0~{L`KPT4oU1c6|OF. >"De? FasQ^ʬrMne@3`d4tDDn8?2VC+VxHLeV748M* qU?M7& r g.Ѵ'o&\̀]` MqF*D,hA14l"\"@&T.f<r.2)&}0i#Qdƃ&nD3L@|@r"&#ɍs Eړ:cHfvd"G*fA\YB@S\X Fngt&,Yr*E!CDf mbiwd49Аc2uPr%&PCLlw\EP?1BSO(7#(☎B V0h@0SQrfn!kv?uw5LT!E "+2%}eAv`@Wc͒30+26Tc>fn<RT(9ֱO+n&W˦?UDZJAdQ`ZBAq0e*`"㈊Ld0X36fR@, #aJ?a 00GQ B% jfe WώE7iF ӏ(2}1:&e3A:%E]DPT A(DF=YTUm%d EPy<@ k mF^ft *Gs\DmšmAK列N2?gI8.0#%0 Fb. .f(FqV&P:vhtCwb-& ˇ#Lώ&#E!Rp'Og( xXX%,[V`Y LX!65mG],|Y*> i )4wdDs\f44M5Al|J8 f 3ןfyeSl"3]1X̸O+s"saWqR)yTccLCP, ;qh 4}y!IɴEv{9T2EXٚlG&@&W,j 3ǽ@j&zG&bLTno'cƘ<OLx=?(ؙȵʴRZ/R<֝Mq, nTյ$Yk] e`.u'V-w!h cOc4Y61&Rǁ1)4bG ,Cs 3'B@+1bg[Q4‰'˽&, o'"T5=`UvOj?BrC 8C XP ɤ:kȌMԻ1&ogG[@@aQp34_B QP_ hVbb C:c-h.!A ω 81J[ل'&)(ۣ,'X)\A 8D=Bo]7[{1QCP3&#Ez/gܻc~]q`QRf,eT ͤL=5#MC:.1\PT-8w Gan|c"%Y0LMٛ&L rsSd8u+W/Rè@E\\٦զ}1zQ,b~;"k6)F:YWc2TLnjb6ۓ9.><~1,NEn '74o_(*lD+u wӜٕDƼ@G3(e&lQзd@l. ȪjÓlUٔHn!:l"fL9v5hIu ǍL-o7:7EK.crmը௙ u3c]XGȘS2}#XZ?dO)f!ɐAbfG8T3OfpiNrfX)4cN2"F4!ʕL֠]ZŇLɷ%~e3܄E؈a/D>zzc=5V>L[i'b 'C>R#I( eR@9修euaFt`ŊSN]#bqk 3 j75lU*n}jOMWfUT0nf"!%zːc&gڥJ*i؜BV n|@7:0}bf\7M@9@gPМ`i 4j&p1m5?Qfk!ְcd luR>L'м̄&*}?툊怇J 0MfM) NՎs;)rlT=" (?9ɅZul@;%R&}: ^yVԛ# g-@@PRLg94^C>`&\.G7gйUL1 *: QDsAjqDl2-4u7Eox`܌c˼/*'fd*9㱆 D ;da:K2gbeh4{FҡgL |v3 }P ?tz`Ț&&o"{preLB$5fl L6وAn &3za)ϑŴ(ϩɄMQ\ޣ 3>=v~n ԐVPTWLĹnt̛3W=ØuqyYؿp}:O?g'ŷY(vf ,gQ9AL^?1!+n$֢&*`հ3$Ծ &5)\\@:&0E!iS3y7/URA<~"1e0YFNioIB;?Lpl=1V1w0`Nd`$C#O-ϙp!vkZ˹.fn|NɁOBdԽ(݇_-l3i0FԢ7+fLe9*D h;Ob`F1<"ڑح(E`Owֻc(VUlY{slc5UGESowѹ oy0Q{v剷 lsI6 FƖ8cimJIΓ 7TQsQ9F $h1"U/]Ps2+7s73YO|U|ΠS vcB=Tǘ>aۧjN3(côRƣ){Z;_@\P#؜gK=2͸1-qs:"1+*~`Ρ r"6b&mη$j 6>aTPŚ#1`R FD֔My$fI`ʻC3. >aӱ*1%g'i1lJfxjPðvېnk_%8 Q)RY4SLoɧ>lhđ&*&"ϑWj 8f_Xv3#B:;116&@caS0?`1sR37-b!q7Fa+4cE;S&Z3t;rq34)jQAJ!}c]@>`E3w]ҳ/8pÌe\I›, "1!cjTfb[if_i~ߨ4=ndY(L5cGQ ̣!5l=S2w,π-Fk6&wľ͆,UCJgP9cswbPݍ&ǰq0o*`C ȕ cF+/@B˩5fU #62‰(h۩2b`m4xm1V/lZo}VjiT/n 05w2Ʃ7b`<> 5٠%z"wv.n,LYWjfJ3wBm(w@\D|)d=Yqs?=f} s}&W4&Ĵ Cj}34s\m 0 &, &Lm3 % LTʶ.<x>FTLP"u-8 x (@QJy̨ʂT7?1}?n (6u^#>eh@Nnf*I<)aaԛ{}֊c]f{ UGT;,ͦʠyӌr1#nZE标80a+`DT\˘O(ٔa7`N[#6H#[YIU>'͙(M:.e/ONfRk޿> aٿ`G?~!^a£SP)cPD"l& 6ȇ)1t&zn|0dAc#'Calumb 4 ֩L=fݸ2oy.2I$\@=%?.$γc.U' &6yhc4~DžYz<fc@X~è81]K n DRk Ž &QbƣP pэZ:ljX3ʕ?ݎѲ(G` (z(PԅB~`kS6xM,bmwQ_ 6rMp{CdСG՝K cmؓG'ӲTnTM4k#Q{1O\GeN(QbsDO{h:Kihc'\?3(PBgmA)g 9aTq%#8N0m^T\cXQ%…^(/au9羳:o=3 =n-~Q~މs>0f' dΟ?n:cW=vKCiREd]|E9=(faݹ[9d* 8۳ݣeJ0}BKtLdIPGcGTıa+6M/" e'af[:^ 03~z?7*TqG Px{e`Q+yh'*94omJ(1W/Ɠ^+LJӓNJy-_$^kS h&fn!fusB'j9pV!8 0(蹦*9U4Xgc{ZUVӧw=Wf8TXңe\we7D ;>@SZ;VjgqHjP9 z#"bȯpcv)(et+w4gQ ,{ Ži$5EX:m/\ntXG\Frv~,Ԩp5! cL%O*Lx yŦqdGh 7cnSz L mgZl0G\dSݼ.UiVm FVcNH9dעlUe^,.I&Q&] mѝL/賀A5aS4s|CQqpoIH;|e_ -!Rb}/2tu #r@Uy6Tܻ0Phu]Ļٹ_@lrLдt#T1OS76~uwliΧ[L}LLiM9!7_O? IuZuNv,/!P25䷴CrwhcVI *U5ϢDiYLtaUvp}:'{?]fmst' LzMVrMMʮ0uFS naW7Ԃe Fk;B>Ƕ5D7w2&dK~ُƂڴ,;2Z{CX S5Mh?-$JfID£&x,-q#DQO2tj@GI @!Sv.2JvY*d &V`~#=27Z,TrdBCdFW K Tz;x5qTæʙ`;*kHQ&\JyݳKBt fL :|zt^c;`3 +ɀ-V;t)>au?7Xw`GRwAWHSnNeII*pU)ai0i8i9ͧPѥfcXUV6zy4Hg]NJ<-=֪xbS3fYOC. Py֩+kYZylg 9j:D3d\އoipwa6@nbq#\A}#R5Bl p2U]& %QΓ~3TLG]BbIU MӒcl*T&t9c=eC'~:;6x5@M4 [BnդM0I>򫅨VOt525j=wS1 0b L{mkEUm O *c T̸AiD? N;Ӛ`s^ǀ檓N|sPL:6HI?Eޙ'sf2M}'TjhhwL)? 煨 J Nu'䎉 ԅB4NU|)Tm<8B0k{2`i &KCe~Mޚx!%OA!Wk|!;T$S%L;kiTxG ײ4AL&ТJ.#4hmGhZ$4LtUeˡK*r{:s^D\TwPnQ[t\5ee=UG7˞je0GC,T8|P}(lsjhvwdOWgUtr;\DM)eEOHM >Rxs|BM:M}6aFbD eQ1[߽?P 9ke6N|hyD:9jpj6?ڎ-Kz*sDKay %^цAoyK)4nj]ˮ78uXjz5"汲!QQI=75iͳu}Ձ;V*}!TKC8nys]0etPmaS`CT*Tvb:ױ9*g<L0ʧU|!&eaL'϶DM$g53ZLk)#-S'wDcwE{0G;Ii#6?CrnC +Ϟp$puC=2oY4t 4FC9/Q:~'ET\t?T󯺰u8{C3B+'^XdamZZM<:}imް׎D+rKanUi*e"ςZs2潤CsS]sNRζqFHh4asۍaxJG9 ̕"ks7u)ѽLOE>>.suU?&hTUy}T!@& ?%̣| K]yoU#p#ZUDdNl9!R hNNg0[[o&O@p/ݯC3ȦRf+9إM)džA:J7@ˌqOK y]^3cNDѠqkXX~Z*AW,&קk*qglamFtqJ-'B|m>hwk!6!S)yBcW NmUZUIˈ.Ϫj܌~vFG$Ba;fVm:k x]^GN 1t /Tۓ@h+¼rW2gOݗ.Jxl"y%MzE1[uy,s-p,MTo8xʯ k $1Y:ȉ&̨;ÃVߧi dˠ "9BOȧ訸; i]c-{p ԉZ°1gif2nv2ZCW5[Pup|i ԜRʨX}I4w@Mny'K):\全9O%KxA Fn_٪*&i {%.*È*m &gR$Le" .$8OTT\*H:~F_ 5BF1ͱNrk Und*.|sޓ=Ld*6x2 v@)I/S5=%_ACFTثZ.xÒכxfd']%J܈#Ul4:({.|uꦵ'j{a~0r9#U4!apAd@ȣ190&hM_Ē)UUmsM%d 3o8ɵ=k:T*qLwu]h]chʓK9:_ө41{y.n|-@Wgl(:\D4cm:d Ow<_T8biS{d.ڃ(!UߴUMo7LԨ}mp:O: ^5*|MSK~`zSm\T ]& Mq+HTK YQR`s3΋ "'NߚܷunKv2/o㖍`vl&iA:VmE,9zg`̞>G^IwyTШzOM%4&@*O4; ˾*=}Bk?BaoA6 Csns M`Yy,"=S3{qn*'ڵrrՊ||LUHnJC) ve1/-ѣ@U[Uȵb*omx`n.Ӵ>*SLUZ *Ea6L\UR\w{g6Dhk[g)LdeW02BV5U6U,— cG `LOU8}~ K op|4q9(\~_=m7CdxH9eMl*ᕉ1Թa2D(܏TaBTUՄf EPC ?Obu'5Hk8G$y*cCrՅrM9Nyk>GTXdnoh4Xj;]9#YM­2ڲ3*joEARzeh@rküOWwZoU^69)'Xی;kߒcN ]< s'{lp Vi3R[ T`LE^ GS: P L}} (႘Ӫ}AlwEZzev#޳)Ȯq}C{bSֻ;IT}6s]:y)\ֻ{`L;%{zFKO}Uv-oHhQ2һK Y~LeڻPrDMwbby'rcTL$Z\2ǸT+*8Qt8]R)cAsdxDz6CDagXܩvk̦uQsdPqa|2ۏ`V w u6Fzho]m&wtX|>!<]Q1Z\ӡ݀ aȄ[qasUM>XrN ~LJ 0H}&ɨj=ʿı ^_> us{39+Z~%iv#03uO_תUeq| f}eR:,E ۗ.{`U\@ dgM04c6cj R3Xl@{n5X1-fࠉïNg~~wsDR:(rJ"\yŻ_o=ʼnZ*ѻ.4a*1uaouُ$I,)c[F@^ TGR&Mbnc\wMw*vVS6yL8džjvU ԅ@Mvy{D3=rYSkC@V@3QbP!q|Vڎ#Pf2{F2}!pT{M7T{F\L_0[5ZtTgi]97Y.ըfJf,s'D:6Yt}U4q-4cB>+ cmƲpvmh:ZGf32'k#o<'' _Bcd'CM#fp5k,;OV=G3Muj\eԕA-e1 ʬ4l9l)hÏUITk- y޻PŹ}ڸ50yQ'U2O@!T<SF6/H z7?XN3 BS)0-9s!SiTUw.HҚL~)\eT{Fly*.,Qp:gy. h-O`]}1ZPkP t:rXϧ^⏉8CM2c-&,y9MvXމCp8fmJuY,tHS]k:&x`!ۊ`r)lKS».R~~J9iuuވK{лO]W ?"Nh(uBJm췍܀sToKM.Cyho6{`CCvYpv9be0U??D *X~ӡ_4T'"ԤX㪥JjtđIu)w犏aT @{6X|Ml.rGf!V3+UȔ %Z|Ujze0ֹ^1OT>0 %2HX'vN+Ҝ!7'hp8+v.,R=Qѻjxѧ'dMH?HnX\=3AuĻ>^G_m 8\K@XAAk" +YsUt5vi?cLhI_5 <)JC /Ak\6,0;&)s h&q9SKU@^5_,ͱ$&nȷOݷڧ 7u^G89y m _,Rg/pjǘҫu0\?J]#\w"b=G%γX7l8a-[QU}r=B$h MUJLƦGS3@*\"B4E /VnͩNw,**Z[V.p9gpXgյ̧kc)ou7Bh 5U@FJ&,~t7)%37 S@sns)YMtn w4'xisWXa>o%kV}G'TU0vg J~lp*D$&vgkTwU%Yi^!b\xF+' ! 3\:G5iDWhGO*iЫwMwD|qvc*5C-aDjp^k V5 (7kKi7ywYoTʨ,B9rriL]60QR *`mVaY.e1R뾩Yn5c/k{xuﵤhCFjپ冤Vh zPab4^eRbgB 4]+_񓪜SC9[QG:Q`Y+Qw̬ v@ R,^,. 짖W _yDu๲-b>.$ )'}^'. {jWR9hb(6IVh ӬxI6Z1U Nm.ޣ4E@f>Ues-)*3 u=UJna 8:~K M-ܰm̧wd+I*imw Ri&Uh>VysnԳ>"2yNv%Pb:T.a5T=\S({*G^EhZ>G5هع өrU 4XC_Ul8 o5 Vو|uDasuDO%-0n5XgUpK+#2UiR6N 3G uBײa5u9gIN+*7O$H0Tn{qla_4O@RÅkXItEGqpyjihOͭ'>IUQu6,vg)Qc~ùh=Ή*}Wq ~ձ|UEYTqW.-y&Sxl_%in&v_y{oTG#%xy,E==ځ}C -oRHxuk L%昦A(qf|N7%Li-+ j#柕O4a_n!'(o9wK{ UOV絽:wbsAbUq5˟$r uL '*75n MB- /u:܈Xj&QNMyNH0TOUJ U0u[$o__ 9hqftY'J8; ?A2F2\dYtWh^ke*ƅp^j +|+Z>DmH B]Ty,#<=s ֛MٔZHsPF\8TiK˹NӅ:h(H\Jc&tJ}jnlo}CI!::<5\wBQK"3Qp51:eZ=Ät*# [)ܺ֙Jߴ?6hӒTkZ*zu|8GSktN|62f'3FOڵҝSH0ZKnj=ڰr֕!U|`1è?-ph >Jr: ~M`ۼwC%(Nn{ h ht]\0tL|~Knhø.q7D G  jANt#EO.`89跜1isCUkATiSkq#O^L@y2E x',(3 JpM9,Q蟲aC1L dWg{x|;vbkd杈uVaT׫knK ˭h,q&VEPf3RygbP2rjS]G>I"AG&$.ϧ$Yk/r{s4ւO c&5 h[(u!7x*5&89?>wu$2CR5F_[>u*=QΥiH=f.4l@-}#54@ ΉO0 l2?ҝ#0iO̩>. L ~X[`L_iAAx[XbuY8GЪT&gp9X٦:&yh 5%pRG%8&jS&LTXvjyn`{<5x4Â\9kچ4Laa^֜X.M4íf%;`G2~s ƛ;aRWm7 y"b0,hSmggԬ\>X:~kVGftRGyoᕼc|װezER4GUFPě.WX he< Z>NO85L5Uꖵx9amٻE$9 PH7~JmLtJӣZUS*FEauV.9+hȧze&73#^yg 4ˮh"7k {af ?ȉ[tZ<-XTʹ q,YXurr=L#iӺj~fL--jkRDwGkRѧUFe+w/E+ O_ XXQ|韆Dh Uw S=2 X[IRL1a.V"lxXZFWQZ] Ht9:uGV5U1oJ0Ktb:2~v:ջ6;Իv4O5 pfts\78S)fD+y놚A' (դ$2YC*9Tu*bXG \/<P hWejgiemmf<5@u 2Uud4i# dm;03 y/SA@L1-Uh0sGDse:CYV͵̭p;Rqꁹ->#Ś)wn(X!^UfL.ȧxx*7Cx,%?*|T Ҥ zͻFJM5?򫦣ˀp>{aЅyzMZbmn4 QʂZV; pKNTr@*bJgFDd7Soxuf>eb 9`\ZM;; 1.NCCs>KXiLW=G1g-q Ku[<|a'U9fF){DU*ݝg+HeJdazDiZu1P /GCD=ե{G%"oCJm*!B."J D>J"ᜅO8ےy͟{@~C R\rS=!0ѧK#yT!(tE"hF$y J.*X\1{ySjATZAy*7,I-p/ ȱkH^U 0<5?6g.7,'{]?!°檃!c/uLko#]"3թSi{8F)u6{QRrjh^e`mQ.k nBZ.Сu wcoVm4+ԫj}793 wz|,;3!'a\4+}X OC:V: v&ʐܚj2$ƫ 75Nmi"K}eLC |fD?LZ DHQ-YLA t2tj81br`yp٢d*9pNLɎmAeUUmFSk9D,Vm+vMN +߽hsC~jMu f^7%xSkO|K`&[%G 25 pH*:xOUP 2ElЎרּ`AzAne%By&O2N~JfsЧ*urJk*}"O>ES9,8;zhUs-$Z*\ ᒢw"3Nmi.D[*jUBրT8wM#Q‹]}HzK]rWf^v4:j[}\5ge7M; ʁN\f2|YaZLĪJ5y$E9 a);҅[kKwFs|tXO>"%od.wXi TV+Y= YI瓳w*.اR=5V u DHIMl&G޽s3~S XpZ,N3.+NCϢ6)TkUCK/y[~ɞhb*Y!֓:}\;OTЈUܟe?Hȷ3AS0 OHd8c^e/+zK]N*0}BDDfh ҷ7u0Ч!5i9gT}hUˋTeg)T?#x,h$xD4MHj]hBT M~ z7Jm':[96K ;o-Ӝa-)BfM'5W(;iWR4*Zs\YեUeGR9*g!y5DqNx}{MZS{IM7G$Ԟ8Vlߛ5V#JSPĿLp22X?mLx7fsM&%vJMߝVq3&"u+i<\$'^cg-@緸<x[:5 i>iЧa̔Ǵ) ei.:+U:cȦ֋DѪh>iL}8N}\ BVΑ$!s44^hwW>:xpY[gPp:*ױ||y[we4d4ٜ±sM9@Jz$cPee>nMSԂn\Lڊ/S57ky-s ADn 57}`.7-J^x)eArW,۫mVHUi,EKDTWf;ܘ@b< @ve`m';Xu:02޷{[PTe@$$4[jqGhy'g@QRx,> O&BgdӦ$U,)؆9 f(*f?־\ڔTMʡZ|0V0]:A5 ǻNjnaSΙi'h26Vo5I3dpH' 쎬*K^ZǫM!6e 2FɏU(Xch'-e`0SшżDesFt \52䫋*} >)o\Wm)0ŷyD41akUK'MVّs'!S\tE[F\&b2Lg iJ{p橐Y颖FSFDt 0N!>yPi) j0P (TELW%  OB{u<0oJ״Nf+#%a⒨Pm4¹M0aC۟А_wUUgaw, 4wE q g$BV;eӪҗi晢J*)t'876x`8uL1˄B$nOC^ Ѱ` ^5.)o/erlRۏtn3oBegTc̷{1ZdWg*WԈ FTo?!Ҭ]0px*,j2SCT+E0%9‡bwaSӼCrsm0sG<UҘB9ܴ6ho!&!UC/e[=*UamVEa`p۷zEIM֞+TtpX8yLaJQr]\OT0 peCDw-k{߂Ԇ* fn0حŶePi{h9L&؀ByVSlpJq98[19*!Uwӵf,R mv3 AY -%i)UTCa4dcc{ydZ8\UnU>O9?v7k!kw]!aiT{ƖP[K|]TYXxzsLT2=.e=Z\;v9 Oc]rW^ ΩR;PLIsX^*-k2Ui1%R@˪uWI]PڦRH'1>OӚCȏ /rQq,cSjwIғbpQ$[O9se; ӣ[ao^hP%Fj'?%/|*FnwWt=>LZ(妨TyVLNl>.ᕏ;CMZTAkcf]QQn傘3 ohdKO|rXJ]Iܴ 8Z+\Vl/a, ³bIWɐ\3GhEmrU|DuԢiRBmUtmrjԫW1H](˻q.s_{ɓ0u4Tj?OfA:a&_&\<=Pi0{5*Lx^4oq: xUuwM@I\Fқ1iz%Ps olUsbӏSM9AT/ʂq%RDStt鳆 ]} }ZdIaM"JaW`gtF3$Ð9x+̆Ɠ@ˉJp,6Qu>]Sh#Lp  UTS#7yP&zǗX9.VsG4LTmHnAE0HP̭knp6p*w901qE5Bvlvj3Q2\z8L!'9+F߳ly'CQwݪ'޺X;iOS\l v-Yn!uXhiϚb|d%bu'2x&ccyJ-aOjwU ;)9BJ*fLseP4fn/U-dG ƈlysR_=*¹5o$8jT#^J{x5CڲJ *MDD:SD+Sp W{U3s'䯜<x!59[7ИCEVRᓪ[ӢX3> 9o)/]/" M`qچqopS\hQwo,Ȫnm~JSdnNtHY57_|Sʬ{rΪH1$+ʛYCFaSi/rMw77NeuFF\uR 1QO-9!9ʧLn'5S: uTE&G&T%ZR9yu'HPiקCZC ȂE;6-_s-ks `Q)6!KZ\yM8-b 4fߺw,Zv؍&Gka WFBteb[Ҍ@sXb7`~(" qqOڌ{@sWw=[k XSsia^O-7:}REĽ%֋bMXGT N\wqf_lh ~zdW 35cNH^- m~^J%Li U*=h*99 dNי m'4 =^ . gU,})H-2=\qvQBiܲ-TNSk槌5jA`v] ƔSl\nƗD Dp˞cU:&Gsfc0ċ[2+[ leUstO0+Mw,M9җOY~Ik,|k UAg*ė.W,M  ]jaҜrȭNU= W$2FP8H,8L;J" M~92~RhoO"2P a0Y!b ơӉt*FNe[48Sʁ#00!„uN!B%JjNS` fm<[L X]u6=murq ) HCŒuV?wݴz*2tM*Tis=ѥ1HSܖDo(yOsH*o1O(CLӜ-v{ˍ̢skU:z+`G$O g/?Z5Maf^0o v̑™d1Ls I|,[%ZrWDJ*خ.>ISME6sT%V͍ UIU5c龠]nssa eisne9w'D 璥y!N;NkF\]uE"lY%1!;@Fn赘9jNV$"5:c)d+FY&gT~#%Vg+[ {<ˉ4JB6ۡe0 #67'i"sa$oz-vy8ndYRyT4 ?ֹ/oSV>U8e &2s޷-p,NAnV ՍCy2skþisx ϼSZ2o S\`<t9"I俁Nvd֟MJFن>*R Sj Zŧz@]Y^wPYZO9ALr~MYsT(Ѷe5. X~*8+-CSih]#H,*=֪n-R4PsGBTE=i'(Md2Sv:mҟ{@DCT`ys*d3*38JwB9'l/v* WpƥS~jwCvVzQEPlrofyFI5<&Qu٫S\Ot#j"'gJZfUzuTkK}+_^ jnq7wS:bGIW&O%yuBC( ؎!U(L vRӞQSgRK%We4ܮlQ'V3r` M,SN5`^ MpR'`*TJ\RXzTMVΪ-<{ȈJim뾉棄{f曊#\kǂJ%BoEwR匩;Uk|r@iV ''Z-'#0eIg;U'dNÇ!hPUFj])Nb&TOlL> V]᧽,8mVĹT8+XִHh:'2m uAµRT*WRz&419tKP8dUVXF7yhGvy/]R(aB eU"ntLl:k}a4Ʃ97~ =IWtF\NM+To$é7@T$ocZ eW]Nn5淘iXBo&HE{JJEك\i,>6uH) ND&} r^S_&*Gk訷wE,gz 7xEu "M*Y OIr*`yU5r^TAQP*+Xp 몴oBuѣOMU570莫z,嘒\5 _CA8g$3PcJ!90V"s`}|2M[f p9YUpYF\Z/ԫ{9J;UYCZ9v7gOk9ͤ*oI̪o:j}1u^L]MډT=}ٜh~r͒ʬUJu@2{MIw sn*,IW0vxO+HtUM:cD2s䍆hr4ө5\"~ix鰦SW!9R)ٷg&)T{LL*$#;lz|Ka{KQ{Y=Rxie}Bc n2X иsrVku1~j)\w! 8rꯦ3sHTa>IvF״r5wODi <&4 S[̑MBtԩb+:QȕBduqU ^o//lt+zL;,ް=NY"%E)FHx&rN 2U M-m2fJLBf LtswŎ ijUo;MÒ0zQĨ;;D^rr_(duty-}Ӓjb~ٽZfa uE2=9F>J/en|?p$Z69;ܿ5%O}Jgx9qpRz]a>jCi>ͪKG;+°&ꆫZQ@dUVdi)Akp{ ENо.l`)W) %hMb~ xvU٧~k E$^NlIth1NkɸTs6RՍD4*nžpN&DHPEc*7xE1D(vFS*Ԩ[żAAJO)>2d?OOvz6~jj2Y rw\RG{!ba-=OagQy"XSs#+v?6rN ouT5c4V7*'s)ﵤԞM9s0uIR$~0LFY}U;iGXU:#2:"DTd*dR{8\Ce \Z b~yަ~mGxћ] ufb}nG%@ BNNl#]5S+D禊Ri><>f\^zrMhh:(VR$ǂ'6jaVUDGUwltU;E7x-6E4Vtk#6SHjR.DgR}:&Ϣ;Ng%15n7\(maiញԞM@t7{ \;C^HB2kS97 fBe+5G2WխRi.{Qh=G0 ,‘vAN$:W35W7U#z쪔Lw__:d*] O%~Sp5^[bhlQs2/)z;5iBq.Ъ}Xݐ7؆7VT, Io8}GnTZ> hXpXv统2TF4hiS{-tX|S4ƆC%jЂ湠>J@@X;6 _4>US6YFY咎0Sja$O##$ /4W4\Nj)r@N e8l+Mkipo~d'5{ajw^9m&M♘3bknYN/x,FWv޺WeaSRc0%7uXLs[L%f|4!n)wFeTQֶe'G u(ӧ<ƪu3LjKIbZp 0oi>ebnT(!4 U*m8 K2Dv%:Xz6.rprn'z.h-JɍXی9> LZ%awj# m26l'DFK٢Uz4m> Ana ھ|+轌&g.i'[ ^Is# LjeW3B&Ys'ՍO N i-2GEA tR⃏vSpd+<G=CSi6*r]6V|%Xs)-M7`l] a&e;CkOC#AYiXk 89љ j NB;=.yTR,7{\rv(U#019?-$NS0=ػ{f::AkI 0|D61.:}3rM|D˺ǒrBU|"}DڜUZ+u3+w=C -Dvnlޙ*e$4ݪ4(lg,NY >8y.-mrjpvbvJC$*gCaC(Or2`Ach&.U{xf6X;a,Bu?X7$O8Uq>ʙ u`Ԯs .7. $stnvpgM<􅀾7{+}]֗d2uh_Vv%E&Lu32lXaMsT7o{IwlߖʂXSgx0Ϻ83PܜUkR~G J\iʣ"|s>AT3=>JȊtۉ [FϚ̔GM}(rA \sC]-eaj8Sk^-QB%5 fsu%]"NjRITxl9,}FcWg׃roŰ_.iuNU{&-N?못OF3F&i4UټMdHtU sȬ%}rjmQ%B*(PBjjZVHżOD:W8s{Wd2EUnL)%6Ub,Y==9'ɐtld\!P֖˓qL!hAdN nmV2= ִ 3 Vnג Dq#T{aꂵ BÚ3eSWxM祖תv 3d-B. Xa;CmXSaUa*yM I=Zݮns|FjDfs@?UfYP^mSLU;mpӲ %5?Ui!a9Z׺ R֑~G)X"ThwsR=Ld7K@]a:g%b(U^915 A'xm$|kZ^i } sN{r懚U reZd4 tZl(hOU0O"4Ҫwµ̔@s|Tҏ{: koV!3r&ZnU}Wd}u.qz#Q !4!s@~~UЧv}Ou#;_"֛ޑ桽Uܙ|kN¯sT #Ԉ]J%=vTe S&9f,Pky1Z r6$?6)%Fi=Rw& L1؆C'4.h0>h_dSB&sxjNܼhahC[5T\Cs|M'-|wfL/7UPjI?W4*HwQĵǝ:UZoHt41 } wB%ak4`v T4f9ʕa9}wBZ2V:$5EbL>tO>˧jkʕAQw?%<_3"LB=#Ȧ,{f]$;kxО\:Cs#=`?]NkHeyhi҃2VB r\i<7FIĺteZhRXU.ͱ)aMa=&ӱ^շ=ӏZJqZWx&TU4DTЪjMKWiWsD(ŚIUAȝ=L..cs/Ɨ݀g'e&:>9oX4.n;pcSt-DHF[7 VW\Z4^X& m h@JWS(_4{6%bnCÚzrXVRa{X*"X=1 d^2(ves̯֗&}j=א⪁.>U80IgHT]}6A٠Z e;ӕZE:O8TϮ;FliCϼZgW mrźOS-ہn碝]TyܧLWqƆ3h>j/ 'bg,=[LǠޛBz)D<5G0}j̀B{H>\JjQUhTTi~ .mQ]u<7;Oo3)w\-FJ g`ZoD)x)9#s.i$w^a9X߆oHj6v\:Ueg7LFp/n\6g}jRCi#ԕ x,3s(ViNf*Ҡ*E혒R%FkLֹn nR):]ҪDCQM-yJ!qCp#CITm[!ScZchvjv75O;WeN|әMIn'{jpdHdQ.YAavlz Wn:'8#>ΛUJ֍tySCfnW6T#)we@(-i@V_ ?6P7 | ķyftx {FJv,sbp? #.5 R/5%4Gf8wBKxMzmNצT.oߪkq M8OZ_)~OK?/~[R⭯_U?]OS<U[E7˹t*r }\= xk+Ѷd VNhZNIxfۂk- sc< jՇKG4h:Tzʺ]tD>aO5lMk/!T3ƎMD0uw5ID*uYXCr(3iSwy9pQ_JNFEnb&2u]>jKFlgihEcYלx.h=Pc]*uNWJH+{L9/9*X~j`溵7̥N:vs)4O3_ُ5]ի'+O%rQLsWeL9'"B| 4 jA @6y*[ʏ n+qWe)c\k9gaSAѸj` <̗@3UO<; N qDCG4uz5/ShQPTSOQL`J>HY:\VEd tVΪ DL(CvMkG=Sapzc C mV6&zKsMt8N})i§!P:sDv^V'kwZgMT}'3HJ7QnP" zJTY2O%Ul @ ,c:mZT fB##`ZuPŴQt0BR:Xw @,fQć8>< f"Y49&fgeU~5^]:e`7':u,S\Y[1ⷕ(5j? |G#.|gzefx8ukB?i|X:#A([:FY;1 j-ϒ,{-O'-P3£e'Q06-ZG%IkTꝘ[[S[w3fٵ)B jhh@#cJM*T䝢MJi~i |3& תcZ4().ˬ*iS>P4ʡ2<¤׹+| .m@AM2ܑ;2Pa -Lw.+0!krK[iڴd}L vТgEKMU vasyƂ:ibiu\C1qUjƊW8{IO ݖQµ=:/@:Eu'f#U DʬᄤrzØ_uW[!zUPQ2m%vχ[cZτB2[ݟig4l[MZ [Fgލ ˸Jϓ^#{?U$1RŗU+L E%4{QŞuV=h{E5 EBSD>aǪ:.ĶTve4vB湁j4RUèR:UxWh>^4k @<9JFD#/0 rThv@)Z<iDɘPB2nav=3z_U|0U.l6Mit}5yoT;2?D;63{;ٍX:Peʥ0Z֋O*0>;]Й__CtE>s`\@N*q2F6hJK ^\ײ59(kNcz*"sr~ Ɩ "Tq[>}"|!n? ^Z]2|jW^+FJ(^ժߺj|@~[Y nqG^_ .+?g:Q؃zwRvD3qP~mLj+UDi4ieg:ފ"ȡ =QԔ@lm0S"v#Ѩ]Tc G*g'?S5s,4 C pOuMaΌ;\c]6MNwBJզXNuЅu,{90@>6㩕O Q[aT ->K?(#jM+z}nޡNm-<|dJ)^2\uL'*:Ҏ)ky(wTtxX_ItOJ8*cʂu+N-{rPA6i $CBn`p2U,F\\^2sAŎu>aqշ8wxTj ԘVsYEvM[t(EQ71Qy-E^S02Oe}ۺ5k=l%3ۢk5脑Fa= qtQ&;HC~%wGl>Hf5*3n~'r#[S#p02ۊl{@'(!f:#gʔwBO0SXV[fRZ%> L;NSfZW~ZJq4pqO5Zl+ Zk:m#qʻJ9BsI1FX snXogU!JvA(ӞIB+w6=T:XG=y\; R(s1f<=2@Ts](/P 6l;P5rR,LYV״ N4[4tN:X29UUN.}E,7>V1f%We,óy^:'CD a9>IτriK HFYl-uԴǩ 6J' Mͣc!=[̑Aˎy,vy#vR$Ss^L(ex{>6TSo)nYڭvu <[ .K~uG[y4%Ti܋2VG S堟 MG{sx;Y+kD OԢ2)ۄxڏQ.UZg,1('8eVX`KNzme[gS3MWyPUKtWgϥJ~ 17T^`d 'xRbI(=:nrpmo/Wdjk 7ef՝S]|DmV4R O4V)T/K9B5hѩikgO5%Nxy]Xcc*K4E܁i"-/$7lȢ@Q(#]|zq,daQ]Zg&ʻƏNXmPzT Q\HBm>(*7_, niGTiyx#De@MdO܁sO"@UO n#YAVoS^oǺy=dmi=q;SLWhewO|dn7mi*$ !5vJ^OiVh> lfuLIkD\xZ#6Hٸiin B^:L8OĆtFѭMq23T+==Jcԩ[ﻗU|p^J޿L/ɡ2{xA{gf@EG0\>9a˪K:Y#FӔs+-kE>\â8rz#D̦Sr\6́Xwq H-czϨTi>Jn\*j%sTܱ}E>J!Jķgc4Rö=IOe\JOY *?YXe?Sm ŸQ#O8o uHBwOG;=p SE6ZV'Z7ڿ̫r[*frD5+:ֹ*6l:x'9O;yu ?VUJUjS1nV5۷wg?lj0NDѬ#pU=Gj٨FBZrMAuC4Ji$D\Ni}z-T65ZV2*2+a麩68QiVgQ>"sRk"AL&*Ew֤s@:V xF$3 lA. hԮѪk /SMJG)O3(z)_$S7JJvO%g\@lڻLN}oq޳z}~~TfSNKS8"&ssP`cIB\G23Nhx!E[ta ,~6ia֕ڮ`jğl=iPlt~OɅ)J gfkS䫙x~}'dZ ݵnPbwڬKNGW0Sƒp,5Kvb^@BgogvӓSmw5jZUxty*bicXV_SCZ^~kxOwz%ժa&qmG谴i vchS mJv> -Leτlے_iĠe:簎FY N~W1q2VWa^#I޷?'o=m]vdB#ll$aooX'=<^ݟ߳ǧ/Oُ,jL=[ڟ)TK/\Q=/M]9 'UN&閫g JۿمʋUTuW0拭nj~A>V>ϟT)%4jUnr@,q2LftB 2d45'SkA5:ըiG)P>^)nwU  >4r $ ٔYk {Y8 sd7.J؟5E ]z#P8z!fT _4l)ԣj+jVJNBJrMRRj:tg/#%%wTP*V$P੘jWsF[v*3Uqg5T2ghpp*t5 WFU].m60]9O%ӻ8&sXvW8eĪ6ʎoC4V AD%T' ȢP7.kPw6Wl ܲV({a䩙` gَ~y>K5_F}@'ŧ}Kyo-?]OG'/Ysf>c|eLϑ> !7~  q脲\isSu> 0_B}E.[9gD.)Á̎$KLiR۞qJd^hV'*o$2%`"S Vj:ݍQCdeqdV;5jX[oSs΍`_Gn7hG캎s 7FĈapwL6AiT3!xI˒kČh OǾ|SqfPs DӁb!kD<q4؉k>W"F7_tQ0Q i꣢ᖪG"@o!o|ܚ#5],"Lpea>zɥj|ef4֎gԍ(F']MOoK]d inmȐ &юMyq@Ore_P, wzY&NtOUjS(CC"ouF\ Ag?(>4V z"SG4LS fs觢>/V%ًXnN+ RkT!W͖Q1]>O F(XoKmi~JFl:l"u m-k:h4Ph`6yf 2uD-LDSIaez.ਚtYhAKg%9ׯD5ѦFڬwB pGX_e//o;4^}܂VKEMRM,~v5=JƉ)}V^t2sӮjeTL.`,5Bg4doa*l}Gg2-:Ӓ&T8ŭ*t)Nt\ch戹nNJ '0z-lوL}.tG⩹|&dBu OA [q=%bg5 2!S{s69Ǫi%FQM}` *a c<pz S l:eRC$jUkEk8^܎[FpCVWihϚi FO{5s_ZZr7K\~m01K/`6EW@ EHwq;zpS2PKt(憋aX'~QQ%S9&Fg%sA4ӈUrnPV'V˧E!s]vL6BqP T6YQmkd=3i{X@Я{=eTeBr0+~Ti> d>m9I^!-?4j1úQ"mKX[mvk6 Dw3g }"~p&h6WqfU!i1: ǩnp[t`ٟrhsn-AZ*:´R.k]uIXnZUk3P{k6C:,V-ӺQH:tҦA7waaXvqqT60 Tk~4#":S,jù~F]7KXnqs`&"KXJ)ucg/P(Cdz`z`/͞MT1~s]/@r *-xcF'w% 5kC1;E@_Z@p!YW.yT:>a,U">%aulp8jqyu$Th~9*ȯLwEڎ=#[B~c jsV25C5$I/FK9xXa_ C搏Y2YHk k񚅏yL!QbX ;)TɹZNPĹjZr̦3ؓwąs:N9@Bn9Ŭs.M=o ]e\h4+=U3P(N e"MFl:,ڀO9 =76QPB!Fzt^=G jiw=!s|=NK IvxRcVKꏪ*Ylkn#6Pe&9uVs{܏Dw ̂{.XOrtw%OxSۙ'w^{NcT67˞9,!zӛKb|SiXB32Ng5V/cɸ]5iZvv/l'Nw M㫑mџuW4Ѹ'H@TWkv7R<KLKxHXw5ָ"i .]ƦsO5Ru_5GT0{1憐-vl(*Ttb)0xpiNPwPThiC|s)=f765x'8O-\ A9ce4xf)aȻX*)9SCEsRI$a'`^ݽj*1GcrUmqZrbiT47gjxl#UVT0C8Ӣ5 Ou{9rٮ4r*6I樄Ջz#IYQR ҡz=C9h门=q9 KaVeMt$ꛃ5#!apo5*ln5nchx˸}+K .szeU?86!cs^C K|psnnm*2eȾ26xFkgT hAiU1vIyק*x%# c(SvB1)j+EFWyW{g/dvBW*Y^iվ[)eT*;aoGs.qxqm-nz.4o:?xZsEUs5nOS^hQ tO*i0N(cIy'c@ آS<W#SPMny#5}ymw~Y*Ns.q19Q b|"dJZ=Hl5(}AN=HGc̹(u9M%*LiRDs cJ@{]@Wg*T)CM3G9&ϦMᎰ993:Pv"Bu0Ak9d`Y iR1ڂ}Ȋ̔D;1⭈s/oUw@ڔ\eBF8UB'ZySk|O Kď }IO]cdm:-T"9&^?Tlq0JnAv)g+m3ncU7|>ypmpSS+JohmkG Y5=Z14x("bUJUh+]T6x-dHkd4%Z.r)Xu2S h)7UzSsog7vzmHTm,> `4zXc5; lxNȠ!"%ǒrUS Xr%,%?[ 2~}-Cռ붅Vs`94N~g۱,6Pj4l.)9aRR(Z@Ca|@U<{"v 182*x8;w~yKOXSd\GQ)6VjԼc\s1RF}sAUw2+X37ѫU5hOd| uR`-3/F7 | Zb*ꅃ,iQBpU W yUl ;lzNUoe%¦q0n2h'\A iq^=BaG v=9k@U:' (b\cR'l+VKU!BQɡVw ~+TӤ7sUDt' Qq4.\1cZp194v"'N֟%OsV6:䱔ǫNg0<,}cy^is`8qb0Ŕ!#%P\ ṖRQf[7㘔ݐdl!T 8u`PB}z/7ʄju9l5ͧV p!qƣ" A&faL$&ytB'eM!"}HQjJ%\٪D"!{dj)5ֺ+vPg-[SuA n6g$pޅvk!ȃ$ϬK6{ksY(QȊm^s;ޘXjK\Rܲ@`1M4)]HAI$=*WYRC+c~}Uz>u_lмz4=c'g_~T._ @!ڷa@"U<^z_~~/@*0G~iF\n_?K_Ex$ ZUY>緈w~X\~z+^6#7~z5z]M0dܭhOAՍ_櫽K/1A .JT^z'Hѷ~VЊGp\Ki>?Uz\"EJIRz1as~'Mz>3юo ryBw6?1 >"[NE_/KYCRJ~?/1;r߸&ehk1;r)*ܗа oWԌ}.\r=JE~RT}ULFW[lsbp&low._EJr+֥~ z?c0{.RV&*[^(Korn>w kftbʑ??g2J5^*TQ%z+ֽ.z GzxQzo_ 4a*fg8éf2#wT|Ns>m!GЊٷϪ*GV R>Wj쎫Uo7/>WtQ]4??rEܹr2C>RpޜD:Bݦfrs2J5ϭJ^%7j.nja$8QF; +fh #>cLEޅ6鸍a ?螧JeIHM+ٔj`b*tB`gHpZ <²MvE87ԇii7gңY8cb2d+Xß iGv} 53m@Cu~/YhXq;???bjU?NvVyG3N& mVsTJ2u>gѝNNs؈<,U1(*yNDU8NI\.j3 OF&my+oqڑݛ@|5;F6mmχգf=JYcxiP.+,E+vlWE:qU3<>*e.Gac/߻ .^ڥC͌?'藔T"yϜN2m֝Oah6W^"9!Ҫ{2y%L"c/@ s JqnqҪ8.Pi)J.eB%qiҖ2=lXoy!Jvmy4\?O?Lyf_&+i9gn!T ,]$ZXtw 4vN_RU<`q63TT*@ͭ2>=?賘YrKhGC yC;D"ݮ3\4Yt!kEt4D*Ǣmlf!H2ҾOmW3Lh7+;z=L:̱حFf}fplhHW(Rp=L`Oa2vوe(  >|eN9)%Snoq}״"=&]0*ovK;Lg̳rwmԽ[]]"hT[Sa|%{bDlıF!)vy/>6t32j#+LmZ=c&F.[9vR&p`U [x8579;s1Q)q803cb.7n|SfmyBHIv?0PusNjq8bQC/2Q<\7=sn[+5ۙrQ:tvE2}c=೩Z_/IkYQ0ޥ⧺tY,c5<J4jzjp0į< 6oLx,E\;\y0#U3Xf2:/?ybs"@W5 -BPȘ$;P)pep+-@E ۱h ]616_,*s0}MojDbKTP/RlN!8rNEXS }}u,u7^w/>#a-x0z DG&_hX`AaP+؋|G!gh;AWAn&sKPҍq*%q:ypzvs@Ydv b\S?8Gt,ra g#TBcؼkOc˯HrIGf#LLJd,N<gTBsWCA0ےqX_?y1|\3PZg?f9\yGtCL6q:',& -ܼ{pmy{\ARk<^Sľ(HK-qRg107R> IЋ^[+\שjHkIot@-⧴7V,*9 R\UyTVuaBbƿ1 dQ`v= @WX3RSٙAkXZ~IZ*4a.iC6T,wlJnGDKu.j+FRlg3r;.mSyx{WgQ*Yt{1op0/b3M>YMj:-ffA3 p#_qa!-kCSLxyByn#Ek~. ⏴ ֈ RnOyxM w͌0ΰ[ܭB%ׂ__B&x..lDJDZh[l +5F:x;K+ʲ\yW_@?ISo[i<ՀܤWvFe? جck-وj([ݿf06]I/]dUT&8/4/_3+2GRj*AM O9bur*),% o}L20~5L}(~xِ!8l܋ßxfʍbt5ůKK=ԼjnmdDϢT3ݧDmLΑlMu2W*0Dqc%a" iEi¸Ut0A/vcEyx@} *ΪQ^ﳬWsbdNC=EFf5xmoȔCWib(4ЧZ[փ0)FA+t>ʽyN1lƯ>0x8gAl+lA2FI=C(3G1S48?쳵 1!g-j7wnT,rlN u㙀#_ beĪ`e-?t;xoq^[7y*srnR13kOeG ەKsDG0TgIm&"*.|x]y{҅즾"&X :F Z;/ZVrusx/%zwV[{5^E0PTo]{]|LLM  חS  6(~g1*ҹpmE _SpZ{և-5 ¿ܯ; d9Έk$i TR&~ȣCL;1u ݹv`! \c~%{u}1^2͐5VƢKuMJ)PցcE bX_%3^<ŠeQniGyJW_ΊbcgCg'Ɇ8\UJpz,C/ ;ڊe*p}eԷ s̭l0*Rw (J2V7dSY,\^D,YcS^^cP@n@ l?(ljivþ[RTcm,x C qz^ h5)okѴL@lk^pi03sfR!W{ruE0޽P7WF*pFS' qa#kɰ }u'T 2rxѾV /5i7Ra"ԦkSn DS @ߺjNf >=̅vfeԍƱ(]g/i\Ӌz@}ӦXiP&N 3Gr7v-h;AA08Het;@TldM7Ѯ8Vhׇ"t-_.#e PglqAvR~IV|_JU)wmF"rU*B(ڷ v `j)?Kf:+R˗P*>ڞX & iӨyzAn?L*vʺq:g!ZbF+Jx9eLьF``R\',xҜ% k^ 4,j8L3r8(b(d̥]UgT/E.AX7X< L¹8z] >&Өyc/u?h5SD#x.+y`tvB4 %{,”fǥC} ܣG ԏo X$wgYeCPa)D!pP=H !P_(w:x״Vf%F_/(J> 0 X0Aw {17drn򍣱8N58or=C\eTs\F*.iZ/""NCƠ6b=Y fFp'_1Fa,~^ѹvL%NZ;Ҹj6^Y]NKQ9jZy .[;i euʸf0S߿IN{KCg\(-~ fA`9ka.`:]bRl~nMw^ n)*,/@rA^rGJ"SwXT&Qi?X9d\zeDzE@濾 ™)uɴ=_i\;]>k#~7,b75tStpyMn%OG3⢪y~hLcah3veÈnV꾦0/y}fwf h{`r{ΜRq&HMɸ?|i4(, saky4 =LX`10̂^|GBM;(3O,Mnd,C|)4VD3l@v!`(')il"++(cXcJۓ,z@ Mw!RَCh9J- ϓlP|[.lxD(XcP!Z A* -;G1*̳Es+̶yS=:"ʻLjoe(#b[ ϼn:Nx(cWeK^)=9v 阱YU{͙;R=it%İw1&˃L+e nkcɺ5W_{]X73E)cNO[?׬~y3]pP9v')@P܍+y:L}v_ R9iCغ1l&MP9 3.ؖO0ipo;u12ͣ-l4W4Dte_aX#>ޱQ!YW{JþiN7eȩ2 \P]XI\ y[Z#s{KmM)%<ܬumf:Fߙ7uh&(RPlr8͜)rۆe&<`Ru6cY@]piX`!RH֎ =P;n=\3" AH\5'M GP*pF"_Dv8ʑ 59w6Cv uc`N,eBݢlS>`-zSG0+US_`INk(O#[X5n58|T[2蘜)> Rl*FI>:]zinwOMu_-R՘8 LZ4S(̞|J^eC8ԶMj80 kb:#䍥t?x Mijz"y4)RKHŦ\T2X4#ڱ݄۳n,'vY"mx5 ;+(}k ˿lJpo#/(lUQp8S5NC/-76j%+Lps0iS i:<@h}UӞbĠ~߃aQ7#*v}}7GGGQ$X֥reۥuqC1ʧk/X7pJuVo3Ji%FפZ:VR 8Vdeu-o=k@-.E~D~9j 2ccT2+fR-*myqnLeTEn9˔,5b] N(Q 2ݞXmxNqf/-WT3,LQa|.:-Ws&1BJKUUQu/ Xqc̰ K(uf UkHL~ "YYƷaSa3jÒn3908#_X4@5α}pt; q *m\AZ8r2ƫEهR> pza=75{Ģ2W*:@Z KoX%k~􉉧P@5RI 6UET_vN{7dfk_!N9%PY7ۤu!PSTBUroA%-H"2XVGIsIQP|п rB5.p}`!riC&n tKzL˘DF55+;O러DYElA -grl= 91Q+.%JSS2ӯՌ/^1!􂥀ٞ+hc^Ng>pdu9:M37+5g,EEkAy*lFQnZ~C}!WpwU7*k f6fQQjȝ扺:ڟȧ l kmAp@fC#ˡLqs/$;uu; ^S7+M:Cдۊ ;&ic.CgMU}'\!{%z5X:55\CQTW8fk>0  w,yB9<v1{ܪb6ǴW\thP c|z |6|qqgM9V:B ?FT9ה(7G N8 iw rx! jϡ(ݙxX2ʶcK5}i=u5Cs+Eqhj׈EU_)!_mK xb珙@r̍l2{9C,T(r\rCb\S'J[';K#؊!*UŠ jcuD5)ia*c̷muoAd`@k Kb1קoN`DR¸ , zb%骻AϤtλ4Q,|acEyuYֹ-|A9/iV^jTF/LOvpOc9C=?빉Pfrzw Ɋ<Z.˗>ފfM;Fh3V% P',-+a] KgDZZVq9D+f=f RK|7 1eQGwt9T'i˓ԏ ^O9Qt0 Jĕkkc:>[~q Dh<=&`{dJΠ BxWPt%T~{i(vnPF 5KC"H2\Wi'ٹ|8P 3 ĸ1$t} P3g.%{n{ -*k$OA<@k^{VF rŴo=wίhT 2v)itsrK)0RQ&̩[tg\k YmZ "gdIPљiӣ=}fgl8l}c.ǘ4*`fj\/ъAg̤pO35B_2G7QsʨhRၥI{푱at\{h![LJhcu>D׵L@3([|Fq<ĺq4#L^jA8}9#ѷA% 1K_V`菊j`JsZ, n5|DX t.}%ٜK^ p7+2TPjffn&b..`HԏJ6kI*hVSe+j>ePɖW (prCo8W"8"eXΐYMaˮE5\0QX#q1A/!~ϛ"BfAyc]!N ^ٍve(YA3CvьMҔ\4 i8HNPeLk^1,a. =!'V2r^XK֫`!*AǼ!u# -:@׹ Z_׏JE%1S-naNefdK;ezd 4')6;Wr˅)Sw٨ w {JuxB/fL=Jd2ڰဿq*1ײj0lX:jMޥ59=a_~%x/`=vf]VWY`VHhkTy?ݽo +,D6Kޘ:Bڕ ‚S$93k7WU|fiD5|2ߔGRӻvw !2'<WEo{lK!hvz[e1}fo~¥3 _1S_蓏*mR:Vp8qvo@ 8CVK@;$ eBs̛\AC #K䎉fU>&p*X/tpD\"X Jd,2e h-W0ֹW0-!;%.D!uԱo Geѭӓ MRPe/*%̴u89Umt'U*avC]ʼn6}JB GKU+xtu_tE,Wԕ_N1Tq1T}?NOw0  WlPKtK!*]He|ͽGl)cMÓ")KŐzEas>*O:h4Bg+/ DhlٰykY̺f.HJAH9rDWq2tb6Se۪K*,gC@%U0tWȻAD|Gg˒0qbY VZ0:V]Z")}Y4ltNyqQM13D 9ZΦbG&nҠÉg\$p3&%UI}J8ޓ( N}ҙfIc26w@z+S2sc,?Xg^c$FIqUl {_&Tͷ̺z۪_އ+D1b^gCpA\m>]+1`R_T ;sa IZ|[9s1"n 5CJ(auqݭP:O4.s+=rgЎ2v< HPn/O8sbA|0<%8^FtA9԰Zw4*s,r8@ hpgBU+o7NEq @w ѾoNd\W2oa0sI"'Ml)S30f5o,nQLBk84:i)LxodPkg\ L/wK./t k}BV%7kC7eÇ$h! xGZ6 &z?+LcgrԩUӴLgC1sӦmdh:N'W~-LLsT|z%-X9h[|%%Ur)Q:l+͗|CQC qRx; tJ 4_Y͘|fep;@ KcL '|z852w& 0&QڽY(EI#7ˈ[K Կ|c?R4ωe3)G Y2q) ?XVx|L`QMavMҀ YQX%mpg3M`x J+oˊ-0_k?_n:1-# n*vjs/jxO[U :_R{C|S5IpV E[ tDxALZ22FAܹ§KPv,B FNVp?E}f$;L; K_\w۲sS>n:^W3DsMʲ/OB$Zt=|L"~Yrkl@BO p1R¾H0M ڨA+mħ荆qJPz8PJ@59" C QIT6yn.v#\φmmQj ^s/Ky#*"!6C |J&֘2%ӈeK%0y[x5ᙅK"!^b2Z2i˙˟ e)Eݯeqȥ5j,3]lVN2ƫ5bo11u)~gz~"r_I{( =sɃT{˼u~)9g%.XE)Lf |XI~R3/WI!=#)):J^.Zqܑ[@aGfiޏUYvNndE䧳LTTJ^qPY%'A8b㼩WM?1%V^.Q%{mpSC;#0$ƈWmTˑ5.ԡ7QU~R,\6R!:]A V @x=}fơLy#=pSW A&h>LQ,VY"u9[v)̣j2xf'a'Zy23#v} ݡs"l=m9(&%q*VT N%x[~&%~Sx%x X[^#ӹz̫S4dv:CGPy마NYНۀ.VVD`r }%z~ ahr1b ~GRSvJݵs2>t2q՗z9ˎ𣈬ll_(JzrӨ>!R2z03gY>Ъ]61.5Xeݩ(/ӈZΦK7q,Ctu3 hQn}2)eJTfp)Й+x#3_fMƷeU{%T;·B!$3bkhr{w+'94d]NAx=j S+* m9-`u8Ҿ%Er|D5 O̓){]0T8@<[F?5@r⯷]"-X!O \`b5Y6lZc`&rbanK?@FrE a[D6_M4 JLq3+yQy=">龡+'%G70j*eHs=X2Y]~ muYH}‹28^Sb8QT՟hL#x9Zxk~!¨|\6$p }%7tK' ̎YU3rzO6Jۙ3P ү@f YY޸] a2uC,hs ;,wcmcP}cNC A0[qK?(bYiy 8G8{xѴ+ߙ*@k6^N\cqEB0 ![4švKWvPR\b-WB}Eܶu<ǰO*O7ַ+fvؖ0 FkSb^ }n͋ףFN5dPNY(/-VT#oըfImn6Ʀd@B_ISў+:Ni]Z2^.`Kex?)J2uM%b yFXf%D g)k1!jwGO*usn7S2wI@ʢ1fs 2RJ6R 7)?Mbfzn'L"&E 1'z!R^FK!}Iӊ ]bZ@Jt$6^.v3+ PIJ)2FsYi+OUVMyDK_dǰˡq9Кa va-de`ģ|Z:=yo`Xw1緘δJqYr^4|t乱G,X)X,S \PGb_I`cv&H9%V `&6ʼ so꒩ 1CAo,))>,Ez-OA g0] aK֞GШ1yd[T6&؏q ,t`G_&s19Pg!'7 9 fwg3I.eDc>F)ΉLWY Y4xL &,m2weN!ĽgTh仪LqZMd&Xad\1243+q 0H)}QJE* Fk"G>m>ӓa!"{ѝ}i  pLXQ\< /o߿݈L 1 !y 6`dӻ2nX)^,ZU2F'2q}rTAaVUNжYje\=]fFYږ\/3$}.Yr.kCNh ;Z~k%-.;!Ny"}W&#y.s(=/wؘ>VO#jiЇ 6kTE ETu4,}AGXo/uܕ9+r8+ a!eM1$p0>"Pe F\g_5QQN;C [m3 ee_pg‡SZ|[?dfVl8-mJ{2G\־g'mSM;1M%ҹz' Omkn`/ӆ?D:ԡCyͺd -f/Qx_aZuJi.WlDR=/*EcnTL}!=WXoC|-v3u9k5"y&|X,\;8`<,GCkDܨ)v@XfEL-"\*s([߫}gC2E%:*3iPX9ݶf27ԗ6@4*c!ŘOɣa _s+8秆i<6|\(y_)a{,'78;[ O{\4.(BQ<!/oio.ml)2b!q d˿MbfT0j[he]hyD`/3w~I\^(&,ےÇ'C QLЩӿĻe<& `1/ PwQp=YY|ŝa1nZa~sh혠IkԻW YYu\J.uqa).A+sS)Z"7u.!ʤ y{Lyt˭}Z>k)2:21yu7K;83Ծrena4zz1s/G$̻Q#AX%te}"Ef51s3[T/j n6n*g5*+iNP.Q" ʳGf;Fgf6Z7*ӯM!y6d37K0JƠVvgkk kgK5\R]w2_s,}Ha5V(ԀYyS%_ MIB=XP,p'#l%QuSN}FTZ`A\p}s*TɆdٿYf<ޮekΆ$AϼQa)_R`~ܰ5gl0 0=_V(^@|Po3,s}HfWf}|Yf!~ l#"eHg蜲AkѦ98B2D+R et3p2IbSal˼04#7qljۼ@}%,m^rÔ5 ^&pkњFJ0@ -_/_#Gt=ߥ&*z&~Wa\תvj[3|g'̹,a73ƜC:u3b[q&P;3n٤~f4l#h0;eN$.\׭Ds_nI[L($S#jG1:e[[a :Mߍ9sKG1fJ"T/H?TJeB:lqIbgpWl>pd9VѿD 0FZ\8L_iOyuTՇٍ̽`c6#ӬxhT~s-Yؕq3n2J`M3*?b }#GeVyFof<.0˩YcÈ0[N"Id9뼭urK.˲[)A=Rs9Mq2#|ͿO7~ʂh1_mk#~o05 D6Q+_8h0TA8~!In߹+sWJt:mg}љ=xVfe{9N_Z0a!b2uO/CzMl+c*Xbά>@ĥ ÝJ+ҏ~p[s 5pY͉OɹQƊ 1fj|Ҍ bz<.;:Cp8<ǡQCmjqL ܱKG J/B+W2x3e uw5'ZY@;ObGAylz˸oG,_+MarV|E f\}e}B,tg>'򏥱{YC@;OCs&q(Yٹlʬ@]IONv2^j , ^tF%/JO-6F]JD!n5۔5̱ooS,_ghe;ןJN zX%t/֏T$+g/zCP5V xP/5wpVj㯩o512]-#\6u0BqWXegEom|EuS"J-Z&k/gLp2ZMYNs,F: r/w}6l[xunU6D62#]R-n+"bS۷SϠg៊4CJl_ Ŷ*=SV#|33"9pva7U&,-JjncG\5GflXruE 0[ \<^~YSe-qybX5/\?tI*o\X*efؿB[]#dOywNgR#Srňs%0?(, M3W_i|c]H;} PsSj$|ɣm+ y>ψSs(:z;j=G5yFTZ^ 0WxYL@PTIR3#oWaGy'b.\\cޅMS\0S1pŸ~?lLfX/?eMw ߂ozhu#g`z.,0ј NB 0f<0S~+@nj癖J'i+9en1:fjcё0Sƥ-L{Dys. vzJW̪Z2M2hG&68F,hseCGej]89V]X[2E~_> 2؜dr]B ۼx)܃Ŷ:ܲ ecĢi{\G<aIȕ/+q.q*N!O"V$1}Y7>VܚeGcuocBV0jwcmh,By16Uvv< 5BXLy)ĽVx' Fr0XwaN.20vgcQc4H>&ǙytKH/_2,]#-ݼ̉g)Ei71w4Gn)_ $J޻MGNf-SamڋE3NqU>鈀PƥF-i=EvVF9-U<7*~Ia-ku21O ڐs#Z5qc1B+9n1@=tӆqʩAyw,ZÍ`+os)"/%@ Jή%^L6/yA,iT&rSrwܥGgICבLOgfg&0E d4;Sg2@5fSAD:Wyn zX#y~U7f̸X1G\U{k7=$vCATDw7q1ü;1 T'RX0;S>^.' %pZ>aH:i*q?}=Q J.:J(OCܔf#I 0TKHY۩-e; ިL՗A7.We20tv:ٝ~g0*ߖZ1!LqiC.gb+70p`UjGh9G[ q:yr,d|Զ 㙞 ?=sq&Y=>O-n0JaADj~uMIk.vDsS&Еq閇0Ic^f)q^f9B\fu q 1+܉TvJ{&r`擴^U~s0Bf nTEEwԵ2;/~p:]~Rƻ"#:OKq^%s Tc{ʰ~띟SЇiv G9obvche\3;@-n=#_g̹Ri)}=ƍP_!uNn{[KW|JHXv1)fC]MGw ޠ]ǫ|1С\,xX]cu /9Q; v2qb^%Sat);3Me|Cr9*a`3^nwz.g̥qԮӡLG? LRcge8άܢVxR/y%<}Q==Z6c楛8L<@ k=c9 %)%Ǧ'3~ g>˧4coO9"s7ـvXwgt#ɡFJҳF0І2C9d¬wjAϏI2B.(+)Ybbnc+3G0X;ͣN"`]7ܞݝLЅ(>DžʶQ{NjS]S]B:4x%,U0:Ϣ>=KVF}陔X+ai3a7yvZ4 )ڃ[NDwO^~#ѣP2:Kc-k.Ȋ^k%2\˄szjLL1&qqʻ3dt;MADP+c{jh ^&Z^ +%{âh-bծe~#5[PFcB2-0%n2{LѼb,~aSqȫU6ī6ʋQϻO>'N% '1y}<¼L!bopsb\?hz4WzQb{ b'<]#kUQz9+-+Q! :1MOE\el_&wYMGImOV?812zƹ'ȷgg3LՓ` 5R"7틤.`NS%ʆ=9 wrzns9ۜ_[/iVcF-oXm0MR <>Dqǎ=Ftxϙ]5s^~gs4+7RmPV~jq (P+<1#0Q vD܎J`ZV{5I5h/9$sQ6(;kSdeivJQbssmi~c8Ks~==" L32cxr-LLgN`2Ѭ0)JDt}*d.Rs0q]%T-hJ tjLtOi nvo:6A/Z8Snڥ=xiޱ7,F 8gZICلmKY܍PܳWȈM!ꌚ/1q^;9h= SQ̒R,NW~T6޾ed1~]x: +]438=Ǡw cQTucqb2[zIk˰B.D37(wvy*3g/K9xeJ; zY{J[*.)^%~Ck8ZW%刱m3!0CX/LOĻ,>J}(J%Td~H}ѱCf/a'S vm GInw bs-ǟS!d۝RG>5Īnc!vaX xYUJu\o-;17K{x/ea}NJLf lN*4#| ]f_uwswĿ 3Vlpg΃!B4%,Of}7 Ny%ܓwR/ѯ^/5 jsң屪˞ۨ$+2f]RN/9~+PN*@sLĠ~Bʀʥ ݾƥYUoZ5,6TgUBW jU[۟H*Ú0 &.1p#ڠWAiB]~0}+db ]vEgh):͈I(`|?tcQXDLwgNɨzae>S718w h}Y3T W:?Ƭ{況}0lpao$t6C6}fW_9DIg^LgP S!h?skZ `{|5P{ m!g:"MLC= Y}q+SF22R(Ļōyo"&Ne|+-7a5 48ӿ_ގe GH˸+q`'Bgz6{~=XY 'tD=1OdN7AeTR'3^YdNDy&} z-S Д3{eG {"[.o;QxkT#fMf+g0|*KXās^Cܖ3~bm { ]=u8z)ܠh+1]]ݏGc(ؚ51@iLx0f\RseHɁ͞bX㈻"Z;$X(:89O;b/>}'髠> $-%TuY#MXbep pFpƝb")<^.eq ON\)}!3p {:VX Por=4Q؎=As X(^3<̪L,}ĵ% `勸<@z^2Ma(-lG׫X'BVu/XqT[IQ0&UNV#/f$kxzzkIe724yzo2Wa4@ vt c$hY9ε7%w% Aٞ`H8'()%u3.~4py;.Sxk՗fSobѤ̡ 萯 (gs˙YzC lp@daLYnk{f1vnK\YL:K) *K&,ρ~gmR/KdQ=WчCT\Ks238vp#Ib-c>P@a&h%Fos~"s"Am1L#,D𘾒`do&fqQA4I{ē$*@Uڍ8qK[-j z mtq+0flHg9pL )݄v`c>#<х}@p%ncџi"T1+u.+d^*P1n}y#SL9G_eu=݋1}aD>X6:[;YzOОIwl;AHxرQ-r ӇYi3Lt^20E3znsL0i[rvwNc}7\Tj|ٽ ǥ?2S!2ƙ]#fK^BO1,KJ}-W傰xJ1 秴/i 0Y-}3-(h-N_.W!ZU0UҎ(s,50u3(ryl k? ; {,AؤyW_>wij .k hNҜe8/ܞDvҷ/ C6 e1h/T2n!0_ޑj"*ܗ*-Tfx+`-7D.w*_Ϫߤ12ޥ9ͶOy 89yv &&q4b"wR%Lm_7U(2  2F̺AvT8ZT`)92ֶ̠w 7r].XPm79A3"~浘 }(<#['M X-̙U3Q^дP Sh9>m|])5JhTuIPY-& s49ΦLŲ yi/c%C}fZzz2_K=cu7Eyֽ1Ms;&,[UQl-o%T؆ ;J`'0pԾ:Jke{TJ;s`5(<] J ߈^wV%<{\ʧG \2|Jo%f(c~&r,W5Oiǘ`M Ûo%h !pA(`1 (CEch,_i5Lc8O*Q`M B 2BW(Uz$0&w˳` 8 k3EH\u1̫W FڎT蟑ƫ76Bf~4kq5ƿmX4jR`)rU&Kd5_hMlalLD&~pr$zj8g%z Me\eFFiӏFopz03]3ǥF %PJi/^s|"4Q.bb.1V1j1rCzЍ5g)}a'I1ݧ[L0 `T,};jf#; *uC*5`ORܑuҥ, 5X(s(1}Kg@QPSL/\Muxj%fbtnKჸktXQaW `)!_ 'e@%#6pfd,'w-"* uIpaMv*rf.9"eB5ǰu! _J+9^&FS̽4>/?J>Y|jjcY(pMosuj˴"<: }BzMO1ǯ2#؏} <z&Sy(VJ踄N%WWĪg!JMs9y#pplЕ¿4dǙm۴\Kc)xU@(F>C\xZjc2AcW-t3aќG.9EW 5 3bi2rܻTeeЗJXBU60"Kݱ`?/7>҈"1sZu{nb wipt{ʑ1Nc*Ѡ/rfE=b^950 ]Y* pq@}e${"QI4cw#~ص~7rfQeoDNefjԫ~!cN.fp_F%W9fs*gʢ@:Mn Ö9>!Zi_AwsQJ3̊Cz0Lquwc$ aԮ '%̹ ֮Q` XLjJ?;1)rzX_2^&&aRu#hwacΌD,%Ĭ-aB;-L Ś#E"d^gmeu"Ըe[M'0uVBb& (Oe|&ԮRW-~7%M,;8`:pJgH:8\ L5u Hu5ƒGJ+vk{RbQK{eCw@^(Ꙧ9D}e&ۇg9ɂ:!G0)f}.{C tq.4@0g zF0f;.C`Qn7:FRʲl ` ΄]Mb--2Hx"NyaUѬq3pwCi~!fsG63E{ť="[~AzNO5D 8*ݟ cKs=<Hi L_ڗ >`Z\ 90Q.C<AN8 4@rcw_lu,>{?M"򹒍x ͬJ2a3̵,Û,S"YmJQ+3SҽN`9ԋ1hz*i2fxf(&ebpT?V X>f/<]j1^#JFGBmn1`.U #k *?Pmx<gH7C ʷj26_#3D_KF8f#cҨ'hdI^TJb1 v&&Z͕ ip5;tPTXqslFi#9jg̨w&& d{N!sewk~_$ Ji#YrPUb;f"/Kc9i z5Z=<55ʢl @|,3*>铄,9L$! ϡkѿ]>5˛Hϣ~YjQ1T?uۊdڹ2^͗bc"S''dNMxA͹ _hP|qEcPlT jϰe!.-;yҭi ]r\Acۜ 5g&Suh>uc3}jvBb`.zi G702gP̪bY/=QLh0 d:KkY|3cI!\j+2,GXvc$!8^J:A^3:B5DrK<2=BPXJC8RрIoG'P eep]y _#9=C߬2F 6i J.g/Emr>:=(ff,tZ,C3ZoE)I@~4@fHĢOX8|DBɾrDe_X9 7! CC}>"TR@ Yߢ\KĪ7d73}bw>YFRS*(=sa}a8lje}XR`7M9/Ҩ_yd..k d֘ W^0,%J :_XY`C/A+Sp34#Bimeqś*醥0yk:53b* ǘ=Jv2GieR$\QS}c(ێ,`)ŸH<@nJV(.sA4u7٣B\oLs*THzWTR\3} Lښh|]hk,4*k>hNg EKzi~Ie@W@/Ó@`gcѴJL ~\lZmm[򩢧91;s;u(-re^]18"WA,oi%·lF+zv\T,J0I>JNJc9%WZ=22*ehZE=AUxQɿi}Y\E!$վT'DycmpVly3( FKt40bS<:* 535 ]~[:8=>2Lec^ԩY|76^)1u/ "WT*.UI% o3 rɛ9f~Rnl6̪-;N{6&-Q>%t59 =SD} =35aAy5*Bh+mX7q/.:S8 n[9oPw+טp)`]_3c7)'9әUܰWŌh!f&G"^L2ZhДMZD:y!:?4Z[{gK~9 v>O%7|K_XGYYLCO}X=e*!1AQaq 0@P?/TR ҿ}B.\ ./B.\r.\HAGqcYKH:8Z- ^ r˗/+.\peƢtr/~\aU@˗t(0Eȸ.C \(HL :?rѹr˗`˗\ yʉ 8?ĹqK?}<=1[3=P@:J333a!/_\r_K.__a6 uG=02/aHtFTRt*S:$r˗*ErTJ+ tW򨒥tN+RJRQb躃^eAX._*T}oYR W&[`UiQCfU|dNL#Ѓܹ}.\}_}/*J\qbqܸ *U[]ZS]@5T*TAper˃.?Q%tu ~@6&B1s_n\Yrˋ/(zBTI_en-q!c ҄_*T\/*TR}n\}n\Yrآ+miHrM%JTQ%u*.qt#lhtCۡab=0ƒK#$u\2չrοJ+/qa O)D8f՟~~%uRu.\} _CsGAp /u]rUꌾ K.\Zҥtj$a0^J`Dfaf#% ;ch[$]#|oM*!P%J/lJJ+e1_ٔʘrԯ}.\r\}*T+ + l|B涞 2#ԓ.5io_edͶYr˗JRq ˗._\QǢ e"G>BQ(*$p`˗.}C*;.\r\}jWa:nΠbi{x* +u/K -(,wF* ވLڮMsX[n\r.\rR'f/6XvkLJ< n㺪eOF A* M\a2X\uV+d4U!"`w2yFXZLPr .CGt%n;]*B `[ekIܴ(䟿撿rӹ0z;T0Q8HTAi,splXUo+L`Bj|ےpӎ^vAUTUr@ ݩxF- ҉6X`JVt*422tr˗_r J*_NB|gh6 fc ANs%$5& 2]FE\`awt$K.`=//]W{rXIPHͰ]/l h4גBlHƱ#߿ig~0)#Q^B}~/1~isX(ϘK? O)׼ǩœ| F":_*W0e9꧜ف!NsSUKB:0 !JoRv00B#< dSxKY/d`y Ц^‹c^/ e88)Ef!PӃ{0I9!npHW .,y贝0rǟ5r` P{ioPw8B4)ōj*k+LvS2riʆT =o3#Vf ;+ODA, B!n=Ņxq9MBϓ=cf%n7-o%[*ϙV Ru}@+E?\rѹUnTJDK1V9F$yQ! ՕbdaR8+V-)9KHHW>yuhw$%5˗._r.\W*\+}vEzd(l0(LZ>_T0A r|m,%Zd:Jk#s{|22%baizIf; R[*r9>0m5V򚔆C~f YuӮ 7 BhrԿ.\rɨof);`S# " X' 1.Xq ]S!qr#?SdHrX^ ̏:5QtuS&Gi/v/99IsXr?/B082Y_Әjg?Ws[O*j]460T/:=e4XX-yJIv쵨<̮r I<1K˿}wLx)P!osAոm;:QPJ)^׵6OD*& $NJ_E4^ ljRҹr$RPG"DzcUn W]JRU[J*7 q!k~JU7.ԫFv)ح@%괡_.;3yY7CՓ.J pS~KCβJݝ[ Bo_*TIRWST\z,<Ϳ;#i}=o+o"L*Y\ޞC-eeuIyW*j& cvsA-koG ZP&>.(14ODVwwwzV>+ @ ү?#P ZDHqV2ŔN!S-_C8xn$_w.\_wy`堨5SP=[]Uhߨ+%Iʺ!^d"-r׹R=Ve}t#TWM} Ǚj'kLPY1P1 #c{j0{B|l2s0e?Tf4ypwFoʵ 65D"_ '%. C@mcǍy~&mR{ 7J/H N_rr%J#/^zmm^4Lny%Q3''=8MAoklJFNפx-e˭+R[Ÿ.Uq 7.\"UmIP`̺֋UAZ&K8! |sr'ĻN**WG~0UxCoAMrR)Q 1ejleϭTZ\ L RԡV( %r7(򐥗FJk&ْ+l;nT "^6Al? QEB7i3NU z Jkk0ч㔙f}X=+E| =Cf⮾kC?5JӰnJM$q 77)RV9Db;g{3jø ^ M!{J&ST8b0|}n\KrkM ᙪg`N%fi)nX\Bh*~An?79^,雔? 2˵`f+ 5Pmu;[26aB[*z2,!MI2p9WBmc,tzWr˗/L[)+ZvsCڊٓ:EXJcjnݔCrw _L܌ؽH)ݧ=@;pU/%QvS98㜭q?wrSO8L]aY#;wѥKiK)c7)c@L"/[1JXP`h[y!EboeZqO{C#\_._/(4rRJ3ʸ"9b5De]l"I))?v2ª#bu+'E`⇵k{ݒ)ǿ`BW9\%V, ݕajk*O*WQ̹D׶:G{D|Lcx| |Z u;yϠQ)2❘V0Z%>' ¥={9 J^rl!,!߸2, JN-`!b`D[QYvcf`2`)q77gSjB6>!ݼk˗/r%]*k#rf7ӈ4$57,`B4#*e)G?,1̺"ިʇSk-tTݨX hEs%%==u<8c;y$Iڢ_O37`|& *u}HTj;?bielU<dHcC,sD$&651ah-`ERJjXh` 2_D}Zu> Ha# ʸƔ6?}ʘG$KKK< r,1 r^2 1â!:^qm̗zi~!ME{=OVHv2Y._}oQ2T66R^}H?~lEk9S }eƕ=O If¢0bXb7sv<.\zy"C5olLD\!6 Kr}o\r+øj&ʣbxZr66.0h#M`킣UNQ,9 7ZEZhri-,QU-_!b_r~LEN% Y8Vˢ\˗/w-eD dv>'iዀ6"C=/SP3Co?ք׾n6'{A*ntf.ܼfg[h4ݧ?n豎!}X4Z!)J7fdg3pc+}ށTN8M{d0n䲂csΧm>"[Sߊ.ϧw r˗._J7/tPiI_ ɦxc d5c0O5Jk$BVe!M[FwׄX1 bCzeӫ@څa pd瑗y.pT>Q9H˓u}ru/jW}.\˗/_[k r ÿc3d0EoUKh0֯f*|b:neоY ʚQMN#ZbJ||b#1!b#IYb8a&£0Uo!J^7zG+"+B1vP/Sʌ_JVRY~S+vy!TiBɸ|[2%Ɏ^ kJ;[C;pTX:ZWbam4ps}53c2g[xh0(bP+g>f~#j3m Y2BT1Wous<L4~Xu *~be˗/\"i&)㝻9_k'e3l-a8/ A._^,&wRHϘ+ z3b@LTz&^G{jig鸎ځM/o+3 ԳRrd4BM :nI++Њ+~(D,)\Gn<:e1w8~f7\2}La_bm5 U8#22OUM=хJ*A.U{\+-ێ5L?.L9mJ0TJw?WՍ]^UM^Xy{6(w," Ev ;QB|`X7 ֘|{JzW|2W.\r ev~ }?|:Vb<8}~*)q Rӽivj $2pTh @}e0w~=Ȥso!SyvCq~? ," v{':#&jr Q}3c6 6'j-MرSJR&tt<^RT>gץ -GDqKƭЪqUqy !pʏ"ݱk{Q5,?f~茗U]߃xbݯ!T U?uL7LI!n$ϯw/u@sɳFpb:(ٜ&v<-!Ji5]ahj|NpqG7īf-gzWXp.FX}TD嵍/\< lwA-R]ц7NX- i>%tB!rls%;.rRJ޵*_|U$WoJ])yM3cOi8BG &)t"J6K* ^pk$%+Px}Pp+ T&IQpς ئ$c+i v0Ơ.܌bW!Zq4y!YrC"b6VHJsv\?BG3v<6b ؀g!|0GXɹ&\  C?8drSl͓fGo$e)cZ5'ҩO M[RRw3ȳ+x7e8EsaqUeFEApJE$Ko }o$z*hsk|gs(J.f!E ὏ `,yC}J"ӤKxNA.=]мɦTӲv34=(+ T1k)L`XR* (+[_c3%UJ][%s+?ځ t(4MɬnMe=̻@a@Ƽt T's>F½ɞ?L.U}FE[%y8T NrDB QH @m$ +xȕZ3@f"3 &11T31&tX#ζ4d 5/C"i UKɖ/0P_{k _o,8y^1nx˲1f,u+VDt5߀)C@<:g`pBVicАѷ]ku._lwe[""mƛȝcpg[}(S`ԯn&N3ɃFgusf#0eqY 0 NnuƬ^Y9 P( lL@N Өy8M<8pJMn 036S4\9u-g CWkU˗._KЮ͙0mCE %ak+>M]1~bs,vCΎ dx=8LZ00ො^o |=تUDϴ^)7V\^Sұm (> bjŽ3 1k'#Y2'FNQP9P'?&zԩ_.=YkіU#s_H&L(szf3ty5`'Qs4$4ld|cU+w F1Qq3ev0KȌn y ZjF>X{ X5EuƠ/wHnͷK7%$U33U|f6cPaM{ƹn)7Vþ+/m`3N tݚs~ N`?*`rU'(Y.Ԥ1#،aYGm) X%,j*v~ێNQ{[W(!| Ǭ Co5W1Q3HsX<]AgT6?K*OOWm!23ow=NB/CR[%L_ aB˖K",Dl4oX}Ec{+NC_ <}#цH22ҊPB EW )X 1b X- XD p3v"adv%j ¹ca-Kޣl;Wg/ F/Y&Wj+8lßlq2N*YN0_7‚lՙ> ͋!&צ,[L R>Pq%I/P'ǤXc0I?셞*/aXA PŶ im>J4PdZŔpOP[,߄Y@+UeeIYbSukQ3`e#Cp4ªddcMU2VKa.Wc Ԧg]7Byxo>=h~6~|j#W%.15Uo_㾗/꿃SWt=_/10)tx ˥e?T ^~FE b ž'.lT<69հVDhoL4nk_-Id`66s^t+ )Z4i 5`[dޠ~Ț[jA1uNTSBhƫs1/&cSMiLpKxqV>+aRp!/?bA6ʬh [1gNDZg?_V5N4jpq;|E=WR F_'/g_1 Vo\Ꮏ&W7J '-UhTAC/"FemyBdMe®3p/Zb?Qn6a@$2Nl8#@9le#.!9pzPXIS +q*z4kjs?8.Rv0.Pa]438gI@8 mfh<4T$݈^/A(/mmyTXmeI{KfnԗUf%aܐ[-N*YK c'x녮gljbqw`wr *C[cpɗ7lӫy X0̉oR^U]2 k8[aOTk./ByBllNDǢ zr힇_ORkǘY,J;MQ*˗6S 4nɥbR0K];*y<b,WQ36 ]=t|v `ÃMxC Ac@J3{UԢStcU 9=gT9S 2I?C2]ciiBhFfnb[bml65rexLULY>8(j.l\1UZDsue ,MkEBƞU_%kbvqQ&j^,y82ZFh"-d N`0;lmr wREE0k`, b8:Z%ĻPklN;c+8e_[wc8ߗe;AsGvW඲_dG¿~12[ǨZudҥ}]+p؏@ 0j_cVgٙc:W}XF6W JSpߎ*[Tp*RKr܊%xnʐ,N3_b| D4 Xp C" \iż FypMd&Yj` q4t%*ʇp_`j9Xv-FQlee~ tlbCXZL)DҳErj-<8\N&9cx`51F\YcWGtn_5ÕL+JB;aB hdI|vJHgjbQDG9ya`pfzFԿt 5Zu,4Ŭ5z׹Ah!Bj)V4h$vn+%L2grI(Ҷcɗ& 9sPagI{@T)(xuZj'8 uݩ{@XBȮh9HX ^E k0,- `J?-R#1FU<{U'h4@ x@̢ͫ>(s {Q= Ѻ qIW`RgRPR эoe4GMQ«k:iݟ0A@!\mB/ٍEaEFy&]}7 fMxaBw/9_-ƗOy mf*gB'Nj#/rղ&e0`*#bz^K Ib}(ow rH y\RІfҲ0m KTFRx‡vfPw}60he Wn IJaE1Klp*6?K@ݷ4'Bt )DNOnvBrPxizI WEoq3_;edx旒3Yj^NVvޛ:Ra34鴅Ijͮ$M[WX{"7堂W;Ɓ`8X@+eBPbOk֪hKuhYWʦX[0X+u)\ؼ*˭AbRႋ78#~ H19r\jk_eheB[4LD9F(KYnuĽC燹Eu`m?ilpt_vT`L_ ewXAXq$ե>X*v=H ,HU5pXRW\]2SD#BXt)صr*3Gyc_/e]z^a_)A>)1MATi&*-ʋdٞ[< ׅѦ0 3^EIP21|\ Mבw2xܸm j~EcwKu8D \&<%.-2z4(F^ ^"ʽT@Rv!!m9~g;n-YQq+ U:ז&ZY#T;1KE3ZOoGیO,C~~;_^o4D'LJ%ƈ@C1EH_^C߃hN܁i}.b_P5DEKϙA8DYMiZ̪ruX@CIZzKds X.Ȳ1峼m1L-n YE(k+P\Ns` g5* whYfa}Rռ9,  o^m|,CTc4B*۫?@ qC^~+pbA1X[ "98z6w u ט[39:{wB<,_>ŦNx8}3F{oL}:φʳ)| .HA/*%)[Q^gWjZ(Ⳙd6^9dEٶx ?8Pq~nYH54n!7Z/,%O J:vZkl]=einRVJ%^V5I, f)I! [  Y.aC"cuF%+ڥUi4NDcsPG{9*YAv9p"` ~3)$L~tB֒kno0缩-Ear =t Ҹgh n g 2:*Urb#IG'r ,b_;JSP"#PET`5ⸯ)2,/cYc/L3h@P _r3z>5b]XXW֢ڣ`g#RF@Xp ԡanԘ!U@b--Ӧ̀A򈣹c0ѷ@p9WBAF!k)"Få۟rK.K$jŀvVJ#] n"3sD*̠74Ns) 蔭SWd$o <Կ vJn{1Er_Ob 2vCcp{V".yb_[`j0ૂ9fOӦ7h+ǔ{Lr dlu˵>l|&<1;Sa2`+54qBn|XmVë;QjsX @LSVDolJ5}a\=x͐x?.ގa.ʍ^fIR-E* gf`uD0B` ."Oz(K֭P#$雄R.+W>e7]-O-*PY)36\(\J:=(sr*"3p."rTw)6`F1r /Oirso=ATډ)wOhP_ aevPN`՛ٍXچÑ S^, i@j ;ucgDV ,3 Qp=Owq`,s$.42? G"vƍRryʖS]$DWQ0ȞA`Bn{z$Xza[Ь), L{q XE/3F2c ^'g_$շL(fjw nf)lD b#~e}U lgdm2W Eh\ybB[VD.KeoK(X ̝)/hb6Xskx_1Uu&4TbXUwtx&\Lr o9 jɄ"zk j2 4AAZr鵹!#'S08mM׉)ŵUǡ!)nO1-n\5(ܟTG>TZ\czTv 0@W1 @0mRvRtdJܼR@F2"54BA6(gLkBiNfRQݼ*0<,805 g>=2R/m/5Ev7;"79n 32 %5Щ3Qƻ ȿ "WյGU QSN-·.{ʞm.WjN'Ԍ,(ڰ`Q%,.@|B O[igOx,(cQs*8!A@!Z\"ɲd vvM(hM#$=Stϑ׷8cZvF#SOLِb!: U|P*VZb֡O+SK/cPx r {/"ٽc30a,KTBf{|Ӵ3Cee\[a0ivQ0PZl3\ҳFc Pg VJ.`Uyq=xu+46\h+ m{]xϰf@>#v㰖)^V.1I퀘 [XٖuZ)`&l{٪9kGA(Uut@]T+y_*\p=fZS*X~p܅m9m* u3oԷ+m!Ig7L_buRXj}Pϸ's7 b ObK#~b-(b\sLD, dҏtfR/;ui%^qb to!PggkXAScNC}ojHb1:fKF qN?.e? qPi|v" &bKq tf"0b]>#V.QpswMcz,@[~b~Q骸U&6 [!+/}azHe2f-K8q1N֪UNSe.,7pDh>z%w497a*Ulmh"sb k75{bP]Q'Ee5wO+uUDwfljE0[w!,чɘY+[؊A}/~]hj?eAgQ<35V' ;o/,lEe. Q6pY2 X 7*S;.+־ɚAՌO.YV\3^<#;(F奱O~Q򧐝vX׳*1v0# M)FhFfDfY~߷NGi'AKq 8Ɛl}V;B&񕯍"D(Ͳ|BEN އ/t$e[CvdCL MYmƎ.TWp =xүew@c%h̨UJ5@Ҧ)pneYJ0P-"aq`7oi,!bZ+q( y9Y̻q8R e8I46eY"[RsHhز4A…o^p^7ܢrg2ݟ\;.ị% l%B`QyG\F[[\% Ơ ^8DUzUl*D5'1EW y;p56b( :7v~(MmSVn`:VB+l"o-;̸ՋL:D, fw~rF#: ߴbΘ%&ILdJq \4m.qv*ٵcDW,3,ᭅFBh@f y7Vr1-qU@!yWn-ET%hY6S2ĭR(Kl.8n4 'l#ԸF.c>cVhj#( 9v>nS(ޮ+ L>B,6:Uj,LS^@fP֕ y,he*5dȇvQbr#SAu0Z{̿ĭLV(6/zeо%r8r4#-%Rsi̳<_/CY2 >ks28{K"\1l^Ɲ&b|i!X\H`j+,B >a* l׻.pJr@-¥Uo-SXR7[d3b5CXams2}43اDF2𜟨KwEƍ/Z1Nd2n'޻NTd|Mx)?fm][~ɗv% Hߒ Ar‡1iмX4X&ʓA~.Z.GI^v|BUɘ_4Kڲ,!A;( (`Sd+GuWtq rZSW5UCMRPXL1m8|@g8̡tlm,]ʪe.4M1}v2rITĭ9~PH߆qdhFܙ7M@yUanEE1lZZ0Pq@uK^W gWA1gx-S%n}Y" "3;'0h[Co-("@}b_mANvNo࿹_wCINvGL슕h?/v|~ S~?a-XҲ+?0TZFfN/E?r1Jⱏ*{ؽ1.U.0s*xw|ᘤ$~I&]C=K0]'ܴ 9}x2i4Xd<&Ƞ6)|d3c .&N{\UԶ6]V aшPA]&ͭZsSg򷃛2*M%^M&u 7bV@/rQMfDQÇV1q8"Emw+_{&$J+;@4gO̲ ˳߁>D/ &Z;GVV^m)|R5H&6=`~ѨnYdzR`M".Eb]R}EBoo`Z&Jx ߙfj5=E" | !Y3&p ι"$g;˪$D4,5M]HJ&X`6,FTE ]+CYV]7Y]a .lͱE 4#1b3 h],tby bpX%%su[f Qnh&Q|D \HoA;`4؇!7AnKa3guBpL(o}K'Wq F \PK$̬qIN},?07)-iܳq5 m2Y (#k|gXR'?#QBo Հ!m}@.˚Arr%-]Y<ȋz&L&{[ÿ2zLc : 3-:4/}iwPch#|Vh|ƹ5rGwmf:w2RL+QqnY4شuD Vv Wj1򺶧kAR+egb*ཏK÷7@4%1M]%T%VX@$l{fvsօ ^W9~V{%S$<"; #6_=΄w}RAbV*bGSR\c4;Lf%DqKswᘀKsMF;BGt뗣 2*qVfh6_lr+$GDwv_'<{)cy9qpV\Uj=ʙlRZA"Φs_;#Hbe&!%> Pԡ!4xҔ”_DTÚnT E]7W%"휆Tv.ͿE€8)e*]{FexĶedTƝFH[Lbv @tTC|Z\pJaAVjZ;cT*)n_!%ў26Θow&f,o `,Z8+ g S78n+$.L/"k}{ Jah*Q~q7|ef+ł1Ʉ2[$c,eAiZhU}$s,G/ TYj"AbweCB̓=ux]Cl&l|4ZrPX45ah  y5`=;9e 4A ئ`yk&XQc C5M0ԳA!o*!0e68GLb` –/(\U˥%b'f bVDI>*)|2C5q!/4=[LH6%i!abEVjU2D~X/UQz>Pܾs6*bes XF ኹJ>lb8鋯A:'"xS!MT%(WB {DAn7oy\GGZ!d}Bh %A*(QG{j:d n -,$f [Bqm)"Lh UP5X[. ~+ 3׮ oaߕhTL,E"c@U]h,נcKpro2t[&y3gzK}F^$mUj1}#wcU1˿1Ífo'VdzIH[tL)ipN9Z*dآiZeQxã'fgyO] jRq To(Xٖ`2)⩑1cx< ٘z0A H򙋖g*C_A,(7!@0@[0I&уTVtNnWN ٘,.'m_*un֠q-%j6f)R ^{K+UF6T/q>6QVeUq3-D%VW[weoUM7ž#f@UZ 5(/$ei+loqi Ւ֮i5J;a3VŸW*[ZUPH8&;$4%a|lq@&oD8@oo/.R.^,?WIBgeЗ< 򦒺NT=2c!ڲ@?XcK xaV]> Lb}SjI4q9LRan}(:Ȍdy1Q0)뙈c7yn*FQ[``mf5{ &hes3=j]RYwjR)4QT*WiVhSl9bʁXj! fkLJ0;wKnT,hyBk>heWK)7=<Wjp*9 eXj7r|aPzv5vq{tS<@*!E2P ^pA6QIGgYvHKxgf!nw+LDVdWzdw ֗=HK0W^ yYww P1߷UKGmoiYNe-=n9sEJxǖ`97'NG EU8ݙƕe C 1J-pDIh%Z x?'a97}O64@tLR|G&theԔ B("d&a.Hee.q~h [0>FV/x -mt@4Z)dEY[fc?2LSASM )6ALU#$sNPZx9qnŸ4#c 4@wt0_JؓU;18#[ SFVXcyNM;*)*̷KGvA "0Kҭ,J"xӏ(jaYŘ7+\ ɔLQa2ih ;TstTD丘 Gf@{Љ?r zVفQ9n_׆v)wڋXaS|۽],q|E0!:RS= Zd/89^+kb uDWl)v8 Af{4(Yyn̰䯍{ {Z6DkRQ@  C^&jD!6q~ _@zH|FZ ՗0Qq{I|"Pz-AC6L-D)4spq`b0^c Kv,(U|EAnp6xށ\.c ^#(1TeLO%bR >JS 0eAH6ʄiWQ4;eTw}xD7 3;'%s~Ҟhܫx};&2}יnnљP+, qe)O0C men6Zl çDRT2|nʎg~ s~`/QUa_{Nߘ3 ADD] 5w `N AV(Ķi5NۻBO'~mt1(\BPM7uF!md ,rlآ^WF'v52PZ܈~'0ZV6P3,K">6[?b/s;vr^QK ZU;xT^LKg,n4/Ҋ(I9 V wC֠*Zp #O5zn@[`)w*K5͙nBK0k9̍5ey.~>b zqZC|Oٖg[3h |3n|Jpnr>ʝ?+*7VPLMb[Qݍ2,e ^ 7:$Di-I:{ڢRS`ʭݞd?]U/ Y8G+}٪Ke ɮPGI"ɭ `4Ɇn8`wlH3% l=V]$0B#N_c/aDĠS4J3z`WeCEwTb9|o1E/bw"v=q7:i.CCheEŰ('sMr֮4"`d{ogԣj7b kR@b/u0(f&ں7ĺ3wQ4y%,4_I[MKS#' K;#;a(:ebJs~WCIxVa]O7o8 Id +/۰'`nMO1I\> 79{Nu7@IPttn,4qW@UZ" v@{>c)u|MȽWI [N"#V>"AKS!vfzK8Cؗɦ[xBRaa5ŹLs`˖!PÝQK'AwCݧs&j)[Y}ٖ2O1jA%:*eKP^}sƎ* + =Rh(}"!>l Zw>e#Er+,Os|^RF A/-=$g,ݡ~1l40Bd)m'Aߚ1J,.kL>E' c~o o0\d{_$טd'\N f@U@{$X 8{'5?hcn5чJ>e/D{6U;fW9XG}[!q13a@,6W3whdy!fQiw*XEx_XfM A`̢\Y,qzc,}{&vndv˫ۂTļbŃ._E˗J2~^b8e203E@EA@.Y'&_]i< qy|a8z#6HRŜc#4/xPJyy7`1t=UCkaϸZpfDpG?A2$h(A5v67 NUocEUy{m%Mȗg0^<%"Ҹķ0UhUe;ZqqF$n}s9J(tV(a9\;I 7p:1vkYFmf:+w)N٥v/V+Lʿ3ATo&>IH&f誶*\sY;k[Yг1"& `Y{pK|EWe;%V4Z*Tۇ1b/"pA AKja q͌.j9U1[BG8#,d- sZLpt`oiU> )&Y9!.,RwRYPXC"FXlxln9۩3Y< je,e+6"v]۸6V>p#q7M|Z& M'i4xѭExq1PѵʉQ6hN&OeӦA"hd0ea1 mҎZk %j=Wt RR? G"b*Km~hW1/e9|sNnOZy UQV8_ʃQ['y$1,A98`rUX~܍@A,AAsYVjߘ7.i\L˩+e"HBhQh*{֫{/Yj*SM!dQzc:0ˌ9`~^ 56e&HVaX*uor;M*, hYi̻*0^a#ܫDފN: 8!x+k Xc*8>S0{\ Zô6.XP. B`sCZ?(ca K0K 1:ZE[.1,^;&:x*2{%; robTohV+g>zCNbmeawU+K0b8|=g*d4=ĨTRG~bj*) oAi0\EK 6"Tþ"l 4- vN>LA*{%ADXPDwao0Y}r0GȏZfw?Mܰ.iw.( S& 9UPo.sٜKo/><$UFx%071>F6KHq Dz\pC厭w% .*%`x8nұ,݌tTBext.rE.1vS UdA z4\k*LA.h{?IGDt)4XnBlĺvg},UܼKn̽^tgLnxJ9^3l"$aȁ LM/h;ʦz+%l5 ۫( ӵ8 z[.1uW8^ZoKswXKLb/ q"D= xD\b^}VWYL+dV̦h /p/,BDwTzo{!՝o苓f#kf࠱>ABl](ɨ' CfjhYi5aB A/1q )E(o2?Ɗ`{>6"Vdlt֮*x:Q`6dіZĿDW3z]#`4q*O䕝:i@Pr4xw-^+\9yA̴Y9&܉t +U_XPhq J dœ[_:Lo|!գ)kO4/2z0?!wO8o [>(jbT7/GcdGEc Md75!v BF<1,!*"3LtJK%NzBu OYG1bH{Kpeo%o a ʼney06&Lp5 hP^cXU{?1qLeÊ#}0[͑XWT<#+%K\SH0_I$UMҷAt#5<߆t >J=%SG#A>Jt*#m/D0O ހ~п ,W_P#H`.%-ۿL _+%7/*dZgYA~N|,H+3ƩO7~na0aWZZӱ>.6w@ TVm }BGDx%)dV4&0 f,4Khulpb\ӱ 3*Q0iUO([FO;NIED BII0_r1%l%o1v"09.!pkj4XRXpѝ|+6prŘn] n^R# U<!#(8Hiem - [N`J.T҉S" ROhk %8 u >)geQx`.q,qZ"i/e0>UqNJ\x @+rհnahG7yB!(D(̳-X'4f9[6;YP̦t35<]*%[ɇ]@nOD6#Nҽ^0yE^oh0`e;&-@yd(tx)1p'`R^E-5{Ga[_-9TեxL+m8ipV\rXqxE.ձwNupܶ9ycu)فR9:ҨMވj1Yj*!u]$\-l /£y\1i撬=Ls>Qiw`fT0dШimiX&e8TUv"( Eȯ#cpN,z0n2ZwYZP*A oga`@ Q/i -@蝪FZDM©_0 :\ΦuM\ E6V8ք GB@l|ah+Ȱ*Q'lE^fop] sYsnarX" CR%8h 䔥neIp3F0?0 hyv!^fji4eNK[x8Gj f%af/?c/ZK mW?K9A]!fvq-{h4(Q @+ZG嗟v\*,fEl^&8q[LP"rlj4 XZ;B"US l>).&. \P{)pfx2>/¬r/xx/F%Q_L|?!Fc|U_ n](N|B#P86Y蛁#agAEA18A)QxJGr.0T3W*̣]땊)*W IV]w"]Y .VS.9yT(հe3[ķc wTQ!|Nڌ\ZKxE :'v"ۊkUE3F\ g'uk,Q{45JᎾp*4J7rCe./5.a–-OlNF#+Ǹ*Ih+4{ 1%zhS<˛--,[9=S1q12˷`Hsc0S\ U}Y>X.Q)(7+5)Jw3eRkAv`s[@8weg\@5 -L@ o /1pV{Vrhde &Z b{.uu(Ec 72ؖdHg&4S-Ūyfd܊hhrܸw !ouZюoU2;`ٕX+rJRZ=Մُh6\@vąMψ:C0qIP"X9 5~.YT#DS_4\ :|8"OEs; Z*ٰ(G6gBPKߐ|{FdP3JI^ OxfeMܮG@oQ?vnR (cH ؐXm@~@Ǡ¶!EL7=VPUwx `/~1ZsL+F^* (t$SDKXwQ!aPM zŗvpj%=!h8%2FfzEiYBzO &bt hUñ#\+*d/+?nPap!N@F.ɖ{ a(%Ǚ B\b&t}gii[lG&6h<D;~.LӔ|dQ,;X4^kQl̉@5sXv0\#c!(mjYJB<76ŵ1U|. ZKffx3HD9) Icc.#H1e) ֡ܬ$]|O!x3r6c.m&88Hܗ+Dj@о&{0EQJiT8E QM/&g[oFO.[(jX$w$ݰ9:XMҸ!n%U-A@ ƒ6@~ |1H06T^ҎKGh ʦx~ݡ(]Z-Vbfod0'ZWlsC*4)8ahP#I%D\m=4 +>VM'ZHUFKwy](BQk - .2ϔT axUvz.YWX9ʨsacnC@385)_#ꇡNB0]Aġ,=&40 `'qolø ~+ R-]Q*OG4)!⢓B`= JwSU}پ,Xbcn\Me7'VL)bōm!>,ߊ_TA99P)Uce\STF=KpA FB,Xk[.qfgҟqݸjj: >b2m& [;z,0a;b D?3xhX7bʗ%p4{;#._15߷.T;oQq"'kaP^HװE#]F]V=t[)l=%e 3qnz/4; Uh JtFĺS[fV*,XF3Z@Rk4!ayr?˨9.ha a Jހb7<_اH ,uQ2y ;֭C\wz!106,F{y!ώP7D*w+bzIrOܵqK}6dDLO %,Sz')a SK2+%Ĥ7 *%5AЇ k EP8hdR[8-5Ƃ#d:dإJ卛{:[s{O":(d+ R]d] %Up7(EGs]#dǁ%7B›Hdp/w_ kjnY N1򂶷xH׶0 ɿy.W^y px{>!֤mmzT.wuM©!r xw^ݒKUT.5V0Ʌ aq w%Fyfj 7mK Kt'3UL.5,ɿ~mbĜMͳNp=1+f5Chbw\LȆ%$RRM#..9R炇?E!Db(;ɛٹ+x~ѱ&f#׉IND40JD[|/ /N V5T>%4k&]YcnR\,2p+QcK2<nW,̡RȬI6ylu$ |DmfK)-МSeA_æi;?iq&Vjterxc]q,YP\K`'Ld"&l~N +9uCaRmލR&oFpTFW١Z`]۟$uen"e~MYu}@{PmB\:B5( SDgR..)soc`"fy)^P!P:f3%`^J1jwHQ@*a _snpl>eCT S+'{.V U(1PXl@2AZ-|9yiW #O %w3k6afjJ;cM0yJ>FlQAPVS-賠86ja@$Z7eKf:?!k * xB cs̽UJ%+d|M,@ 8{scmÈW-65 KufrԸ K"(!^y?FeW@`lf~zYSg[T3f7@nez]C.0@]r,vX*w2 fj+tUC1^x{Ǹb;OB qG,ޣDEzj-`&lwءy ?gAĴ Cp゠#l 7g1{XU0.m7;0U\.RβUU0 [[Fp\"9@QqAlLveL兘-a+X ~O$Kdǖ:n!WhL؈-֏ܻ/\qR͌ÌZ8}I-jƘ`eK07!% /UɚnT",o(p~P-г&.~cJ#"@&_, ̂°ަe FT 9L o2p 2P+d`hg{vo_1-~cO1TOw K,L栂ޕ&D@u.٬ƊU.}ȚV4dU[pJ|ҖU dX¨̧Yȷ-UhDvѳaR0Y%obQT^pGȈ-K(Jdv -` ûw+D^ NϕR!M_IWGRЂ{ M%jNkFR[;G3 PwZ +/aEu>E#dyVf xXov7$%)ڹG)0zj#AjY mxFū D.2#"vp/p6=s(ܮQ悆X`m7XJȇgq`Ӄmka ̶M)RB`[ &-%kTӪ10q6 0z+#+f#A|$o%C?؏TWLXx؎TY6|Ԫa0@HH0Td ZVeArQ|m"4 >me,5; +DB(lR[̯$ P )BCD\`Mwa97b(&3h[tԁeQx(\$b#Bs 095BEуnWS8)|D=tЕPKh]-MXP2f#Rp:V's0vx<&=uiو# mm  s7M16` KdV(`ef<^ҸL[\"]1ޠ|0<^UycG$9?bˈXrbjhx @RsWKޭ6ЃW,pWf|j KU;\Y^a! M(.Xc*86 er+U'F_<]=q\30C Ҍ9.,Bs 2R͌J4 ǵHÃFYPGÌ3^̗`z3n l I4m̃v7Ohdr5* E& 5`_Mie3%hɉp1Z(k@*E'^<+uɉ]pCL[Y%TlDUt ;V']kp0]& \D(:&iu瘿Hmy/%^& ei-V,))> Z?e[4!ݓb)" W{+eӕ5y2,w,W9zK/B{f­|6 F&p+z{P鵗Rl<3{z*Ux+qa|"ysBu@zYD,9^K5K}P-艔K>f躋y/#2Gdqn` ^ )Kaǔ) \WB-90 ys2[Lfc|H*̭@IK95!t'4L2EM!21 kj92 @L̕ͅv0ӄ11|UpQ79En`1,g'yx (K k!0ґ:ROҞ{Ѐ0md=jEdۮ1 B%j Utn]^cmԤL5,‹cl5Z muzZ wK7uX 1mBO+k0q6L7V` sNa=HC:a4BÁCݿeͿ)y-4F@ H9Y,ړ!|  ,ض\j`(Ja}NNQ t,@q]EBPF.ES-ҡFF4{1-GSIjfuQz|3#3j.Fc8z1hGst옩(.UU 3xc v%,&UxC +B6=ܾOd802›KnnAo}XgF͢cϽ/N?1R>-+u^ ȍxUnC`UQvCw;_pT-0|~ 85[r>V7Jm{>(Ÿ%DSfŨ~ Dq$@ ˂pG8eU&6 xcbC6Y%%%0p!mh+G@a-P2-=ڵ+Mvek,H!*gy3ʀ2r% 3^B6(wP= 4)G4TZEH3V<[A@#!@>)vxa'/cmJ(Pa8^c7}" ʯ+9[/P\@@#PV]QmObC)`ܡ'}PŏiQ'6;h4kY%̳1"![18eq`áYi.D>yFg.Z?aIu`}B_yeVA 7UKA9ؠΎuN.a! *iAM-*F LiĬvܵ~vm>NH`KeSFY3B*cN2F#sw˖䬹AeYZ R\KX{׬v$.k ʿ3Ef֦'W=0T_Kck^n-ZB0\RJ ReA%u5P ɫ ߃ kɹ_l m p,edgS{m{s ,hzki͡y-7͑ m+=)xSp{gG`XJcU9fhbL@4. ng\fgţl~&,&6E-n,&B LejQ Fy4TZ6_RR`t^5 4i棣}9鰘"O,f?LYA˜y]) ]+0gQBI FqU/]ыx0zL)%P3Ig̼.) 12W%\j~JٚK mZal٧a @jWC/71 L+eU,<L5dR,69ơ+Zn? rȌ.+)2CAR#;25< @;=1O67:Y"~5I%]8UċBk0ǥv&Wj8PXJ,68HY;U~SGJ~dꩤyEL֒T/yd ŰǴh18\Rf$Ӝ~V 4l֌*6é<}"u(c xavf ۹:%% %؏sf1#1!oZ_8\`T֮Xg&lC`ULP9MLFt}lk\@+Mʈ1x(K3b-&Ҕj6@`!QV.Xl4>Ax啇6URLD6r7KkԲM/u TDA/k6@Q5QY a*6Xg +nԠ">pK3Q.8qMl.*/G0O`jyߋF[W.<\Tqh8MHY#7]ŀi4"a;qG Nw;%F6FXSWuf1_ht+X9IzJNjS`H+meyb7 1^I_ Zns/ ;-2 Jlo.30.w=_5R(Wph袹L:/1_k6h-# &4@s?PF0R(s'"8NLq5TSHMʱV ᴄ Zv%8jxQ*E,(9EjhةH m7x'.L@lpZ`\Qa,Y,65k6g^ˤe`K>'.Vrfq LJ М6isV*}0Q;Eo)W̒jзm wJ-0%w?A tj7S$q^if.LF  Ma!0ۺOИtèXZ$)g v}m? *>e\Ee>3V>PU߂)v0]ַ0#L9WٌTBq[m_|{㢚Jȵ T[e 8VܬjR^ TF4 hdlcdp b[j^^%L7pzfQ>udAc~tPc3TA¯n{Q2q~ 9VK:T@aL/pL0,L:eo!* Jh.jR󽌤n@>%%%TA.23<Ŏ Vn?fHm1dSLüe^Ⱥ+p2(cظ4myaaZh^:CI䀛unr4$6֌;FXuBzc?3FǵyHr=6?*=Vd'## =1 JeB[oJ(Pji&ܥ[dFTyDP!:S:[Hp@t!?drߕJnʌÍ1=Qh'Q>VXj":ˇTXd@HfX Q7+!~:'J1Fd̞` )hӥ%* )fO  ,%j\-~Fzad3/KYEky'.˦4%\/.<U*4)enlX..>hip&Y@ɔ+TUƌqTńܭm xauyn8ͽ՚fƽRiO7cN <N\O*R\W剪6S^;p+G33FEb{YXU|ehG<" v(T¥1J]!w"81FjUN#-;́?U 2Ytw&#)` $I.@4\JhʔpVXN&Ռ!~ C MC}+
Linux 4gvps.4gvps.com 3.10.0-1127.18.2.vz7.163.46 #1 SMP Fri Nov 20 21:47:55 MSK 2020 x86_64
  SOFT : Apache PHP : 7.4.33
/proc/self/root/usr/share/yum-cli/
38.135.39.45

 
[ NAME ] [ SIZE ] [ PERM ] [ DATE ] [ ACT ]
+FILE +DIR
callback.py 9.151 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
callback.pyc 6.717 KB -rw-r--r-- 2020-10-01 17:03 R E G D
cli.py 107.065 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
cli.pyc 75.603 KB -rw-r--r-- 2020-10-01 17:03 R E G D
completion-helper.py 3.4 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
output.py 136.489 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
output.pyc 106.217 KB -rw-r--r-- 2020-10-01 17:03 R E G D
shell.py 17.407 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
shell.pyc 14.407 KB -rw-r--r-- 2020-10-01 17:03 R E G D
utils.py 15.448 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
utils.pyc 14.485 KB -rw-r--r-- 2020-10-01 17:03 R E G D
yumcommands.py 185.063 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
yumcommands.pyc 165.668 KB -rw-r--r-- 2020-10-01 17:03 R E G D
yummain.py 12.334 KB -rwxr-xr-x 2020-10-01 17:03 R E G D
yummain.pyc 10.62 KB -rw-r--r-- 2020-10-01 17:03 R E G D
REQUEST EXIT
#!/usr/bin/python -t # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Copyright 2006 Duke University # Copyright 2013 Red Hat # Written by Seth Vidal """ Classes for subcommands of the yum command line interface. """ import os import sys import cli import rpm from yum import logginglevels from yum import _, P_ from yum import misc import yum.Errors import operator import locale import fnmatch import time from yum.i18n import utf8_width, utf8_width_fill, to_unicode, exception2msg import tempfile import shutil import distutils.spawn import glob import errno import yum.config from yum import updateinfo from yum.packages import parsePackages from yum.fssnapshots import LibLVMError, lvmerr2str def _err_mini_usage(base, basecmd): if basecmd not in base.yum_cli_commands: base.usage() return cmd = base.yum_cli_commands[basecmd] txt = base.yum_cli_commands["help"]._makeOutput(cmd) base.logger.critical(_(' Mini usage:\n')) base.logger.critical(txt) def checkRootUID(base): """Verify that the program is being run by the root user. :param base: a :class:`yum.Yumbase` object. :raises: :class:`cli.CliError` """ if base.conf.uid != 0: base.logger.critical(_('You need to be root to perform this command.')) raise cli.CliError def checkGPGKey(base): """Verify that there are gpg keys for the enabled repositories in the rpm database. :param base: a :class:`yum.Yumbase` object. :raises: :class:`cli.CliError` """ if base._override_sigchecks: return if not base.gpgKeyCheck(): for repo in base.repos.listEnabled(): if (repo.gpgcheck or repo.repo_gpgcheck) and not repo.gpgkey: msg = _(""" You have enabled checking of packages via GPG keys. This is a good thing. However, you do not have any GPG public keys installed. You need to download the keys for packages you wish to install and install them. You can do that by running the command: rpm --import public.gpg.key Alternatively you can specify the url to the key you would like to use for a repository in the 'gpgkey' option in a repository section and yum will install it for you. For more information contact your distribution or package provider. """) base.logger.critical(msg) base.logger.critical(_("Problem repository: %s"), repo) raise cli.CliError def checkPackageArg(base, basecmd, extcmds): """Verify that *extcmds* contains the name of at least one package for *basecmd* to act on. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ if len(extcmds) == 0: base.logger.critical( _('Error: Need to pass a list of pkgs to %s') % basecmd) _err_mini_usage(base, basecmd) raise cli.CliError def checkSwapPackageArg(base, basecmd, extcmds): """Verify that *extcmds* contains the name of at least two packages for *basecmd* to act on. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ min_args = 2 if '--' in extcmds: min_args = 3 if len(extcmds) < min_args: base.logger.critical( _('Error: Need at least two packages to %s') % basecmd) _err_mini_usage(base, basecmd) raise cli.CliError def checkRepoPackageArg(base, basecmd, extcmds): """Verify that *extcmds* contains the name of at least one package for *basecmd* to act on. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ if len(extcmds) < 2: # install|remove [pkgs] base.logger.critical( _('Error: Need to pass a repoid. and command to %s') % basecmd) _err_mini_usage(base, basecmd) raise cli.CliError repos = base.repos.findRepos(extcmds[0], name_match=True, ignore_case=True) if not repos: base.logger.critical( _('Error: Need to pass a single valid repoid. to %s') % basecmd) _err_mini_usage(base, basecmd) raise cli.CliError if len(repos) > 1: repos = [r for r in repos if r.isEnabled()] if len(repos) > 1: repos = ", ".join([r.ui_id for r in repos]) base.logger.critical( _('Error: Need to pass only a single valid repoid. to %s, passed: %s') % (basecmd, repos)) _err_mini_usage(base, basecmd) raise cli.CliError if not repos[0].isEnabled(): # Might as well just fix this... base.repos.enableRepo(repos[0].id) base.verbose_logger.info( _('Repo %s has been automatically enabled.') % repos[0].ui_id) def checkItemArg(base, basecmd, extcmds): """Verify that *extcmds* contains the name of at least one item for *basecmd* to act on. Generally, the items are command-line arguments that are not the name of a package, such as a file name passed to provides. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ if len(extcmds) == 0: base.logger.critical(_('Error: Need an item to match')) _err_mini_usage(base, basecmd) raise cli.CliError def checkGroupArg(base, basecmd, extcmds): """Verify that *extcmds* contains the name of at least one group for *basecmd* to act on. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ if len(extcmds) == 0: base.logger.critical(_('Error: Need a group or list of groups')) _err_mini_usage(base, basecmd) raise cli.CliError def checkCleanArg(base, basecmd, extcmds): """Verify that *extcmds* contains at least one argument, and that all arguments in *extcmds* are valid options for clean. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ VALID_ARGS = ('headers', 'packages', 'metadata', 'dbcache', 'plugins', 'expire-cache', 'rpmdb', 'all') if len(extcmds) == 0: base.logger.critical(_('Error: clean requires an option: %s') % ( ", ".join(VALID_ARGS))) raise cli.CliError for cmd in extcmds: if cmd not in VALID_ARGS: base.logger.critical(_('Error: invalid clean argument: %r') % cmd) _err_mini_usage(base, basecmd) raise cli.CliError def checkShellArg(base, basecmd, extcmds): """Verify that the arguments given to 'yum shell' are valid. yum shell can be given either no argument, or exactly one argument, which is the name of a file. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError` """ if len(extcmds) == 0: base.verbose_logger.debug(_("No argument to shell")) elif len(extcmds) == 1: base.verbose_logger.debug(_("Filename passed to shell: %s"), extcmds[0]) if not os.path.isfile(extcmds[0]): base.logger.critical( _("File %s given as argument to shell does not exist."), extcmds[0]) base.usage() raise cli.CliError else: base.logger.critical( _("Error: more than one file given as argument to shell.")) base.usage() raise cli.CliError def checkEnabledRepo(base, possible_local_files=[]): """Verify that there is at least one enabled repo. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* :raises: :class:`cli.CliError`: """ if base.repos.listEnabled(): return for lfile in possible_local_files: if lfile.endswith(".rpm") and (yum.misc.re_remote_url(lfile) or os.path.exists(lfile)): return # runs prereposetup (which "most" plugins currently use to add repos.) base.pkgSack if base.repos.listEnabled(): return msg = _('There are no enabled repos.\n' ' Run "yum repolist all" to see the repos you have.\n' ' To enable Red Hat Subscription Management repositories:\n' ' subscription-manager repos --enable \n' ' To enable custom repositories:\n' ' yum-config-manager --enable ') base.logger.critical(msg) raise cli.CliError class YumCommand: """An abstract base class that defines the methods needed by the cli to execute a specific command. Subclasses must override at least :func:`getUsage` and :func:`getSummary`. """ def __init__(self): self.done_command_once = False self.hidden = False def doneCommand(self, base, msg, *args): """ Output *msg* the first time that this method is called, and do nothing on subsequent calls. This is to prevent duplicate messages from being printed for the same command. :param base: a :class:`yum.Yumbase` object :param msg: the message to be output :param *args: additional arguments associated with the message """ if not self.done_command_once: base.verbose_logger.info(logginglevels.INFO_2, msg, *args) self.done_command_once = True def getNames(self): """Return a list of strings that are the names of the command. The command can be called from the command line by using any of these names. :return: a list containing the names of the command """ return [] def getUsage(self): """Return a usage string for the command, including arguments. :return: a usage string for the command """ raise NotImplementedError def getSummary(self): """Return a one line summary of what the command does. :return: a one line summary of what the command does """ raise NotImplementedError def doCheck(self, base, basecmd, extcmds): """Verify that various conditions are met so that the command can run. :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being checked for :param extcmds: a list of arguments passed to *basecmd* """ pass def doCommand(self, base, basecmd, extcmds): """Execute the command :param base: a :class:`yum.Yumbase` object. :param basecmd: the name of the command being executed :param extcmds: a list of arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ return 0, [_('Nothing to do')] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before the command can run :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return True # Some of this is subjective, esp. between past/present, but roughly use: # # write = I'm using package data to alter the rpmdb in anyway. # read-only:future = I'm providing data that is likely to result in a # future write, so we might as well do it now. # Eg. yum check-update && yum update -q -y # read-only:present = I'm providing data about the present state of # packages in the repo. # Eg. yum list yum # read-only:past = I'm providing context data about past writes, or just # anything that is available is good enough for me # (speed is much better than quality). # Eg. yum history info # Eg. TAB completion # # ...default is write, which does the same thing we always did (obey # metadata_expire and live with it). def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'write' class InstallCommand(YumCommand): """A class containing methods needed by the cli to execute the install command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['install', 'install-n', 'install-na', 'install-nevra'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return _("PACKAGE...") def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Install a package or packages on your system") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkPackageArg(base, basecmd, extcmds) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Install Process")) return base.installPkgs(extcmds, basecmd=basecmd) class UpdateCommand(YumCommand): """A class containing methods needed by the cli to execute the update command. """ def getNames(self): """Return a list containing the names of this command. This command can by called from the command line by using any of these names. :return: a list containing the names of this command """ return ['update', 'update-to'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return _("[PACKAGE...]") def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Update a package or packages on your system") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that there are enabled repositories with gpg keys, and that this command is being run by the root user. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Update Process")) ret = base.updatePkgs(extcmds, update_to=(basecmd == 'update-to')) updateinfo.remove_txmbrs(base) return ret class DistroSyncCommand(YumCommand): """A class containing methods needed by the cli to execute the distro-synch command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['distribution-synchronization', 'distro-sync', 'distupgrade'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return _("[PACKAGE...]") def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Synchronize installed packages to the latest available versions") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, and that there are enabled repositories with gpg keys. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Distribution Synchronization Process")) base.conf.obsoletes = 1 ret = base.distroSyncPkgs(extcmds) updateinfo.remove_txmbrs(base) return ret def _add_pkg_simple_list_lens(data, pkg, indent=''): """ Get the length of each pkg's column. Add that to data. This "knows" about simpleList and printVer. """ na = len(pkg.name) + 1 + len(pkg.arch) + len(indent) ver = len(pkg.version) + 1 + len(pkg.release) rid = len(pkg.ui_from_repo) if pkg.epoch != '0': ver += len(pkg.epoch) + 1 for (d, v) in (('na', na), ('ver', ver), ('rid', rid)): data[d].setdefault(v, 0) data[d][v] += 1 def _list_cmd_calc_columns(base, ypl): """ Work out the dynamic size of the columns to pass to fmtColumns. """ data = {'na' : {}, 'ver' : {}, 'rid' : {}} for lst in (ypl.installed, ypl.available, ypl.extras, ypl.updates, ypl.recent): for pkg in lst: _add_pkg_simple_list_lens(data, pkg) if len(ypl.obsoletes) > 0: for (npkg, opkg) in ypl.obsoletesTuples: _add_pkg_simple_list_lens(data, npkg) _add_pkg_simple_list_lens(data, opkg, indent=" " * 4) data = [data['na'], data['ver'], data['rid']] columns = base.calcColumns(data, remainder_column=1) return (-columns[0], -columns[1], -columns[2]) def _cmdline_exclude(pkgs, cmdline_excludes): """ Do an extra exclude for installed packages that match the cmd line. """ if not cmdline_excludes: return pkgs e,m,u = parsePackages(pkgs, cmdline_excludes) excluded = set(e + m) return [po for po in pkgs if po not in excluded] class InfoCommand(YumCommand): """A class containing methods needed by the cli to execute the info command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['info'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[PACKAGE|all|available|installed|updates|distro-extras|extras|obsoletes|recent]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Display details about a package or group of packages") def doCommand(self, base, basecmd, extcmds, repoid=None): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ if extcmds and extcmds[0] in ('updates', 'obsoletes'): updateinfo.exclude_updates(base) else: updateinfo.exclude_all(base) if True: # Try, YumBase... highlight = base.term.MODE['bold'] # If we are doing: "yum info installed blah" don't do the highlight # because the usability of not accessing the repos. is still higher # than providing colour for a single line. Usable updatesd/etc. FTW. if basecmd == 'info' and extcmds and extcmds[0] == 'installed': highlight = False ypl = base.returnPkgLists(extcmds, installed_available=highlight, repoid=repoid) update_pkgs = {} inst_pkgs = {} local_pkgs = {} columns = None if basecmd == 'list': # Dynamically size the columns columns = _list_cmd_calc_columns(base, ypl) if highlight and ypl.installed: # If we have installed and available lists, then do the # highlighting for the installed packages so you can see what's # available to update, an extra, or newer than what we have. for pkg in (ypl.hidden_available + ypl.reinstall_available + ypl.old_available): key = (pkg.name, pkg.arch) if key not in update_pkgs or pkg.verGT(update_pkgs[key]): update_pkgs[key] = pkg if highlight and ypl.available: # If we have installed and available lists, then do the # highlighting for the available packages so you can see what's # available to install vs. update vs. old. for pkg in ypl.hidden_installed: key = (pkg.name, pkg.arch) if key not in inst_pkgs or pkg.verGT(inst_pkgs[key]): inst_pkgs[key] = pkg if highlight and ypl.updates: # Do the local/remote split we get in "yum updates" for po in sorted(ypl.updates): if po.repo.id != 'installed' and po.verifyLocalPkg(): local_pkgs[(po.name, po.arch)] = po # Output the packages: kern = base.conf.color_list_installed_running_kernel clio = base.conf.color_list_installed_older clin = base.conf.color_list_installed_newer clir = base.conf.color_list_installed_reinstall clie = base.conf.color_list_installed_extra if base.conf.query_install_excludes: ypl.installed = _cmdline_exclude(ypl.installed, base.cmdline_excludes) rip = base.listPkgs(ypl.installed, _('Installed Packages'), basecmd, highlight_na=update_pkgs, columns=columns, highlight_modes={'>' : clio, '<' : clin, 'kern' : kern, '=' : clir, 'not in' : clie}) kern = base.conf.color_list_available_running_kernel clau = base.conf.color_list_available_upgrade clad = base.conf.color_list_available_downgrade clar = base.conf.color_list_available_reinstall clai = base.conf.color_list_available_install rap = base.listPkgs(ypl.available, _('Available Packages'), basecmd, highlight_na=inst_pkgs, columns=columns, highlight_modes={'<' : clau, '>' : clad, 'kern' : kern, '=' : clar, 'not in' : clai}) rep = base.listPkgs(ypl.extras, _('Extra Packages'), basecmd, columns=columns) cul = base.conf.color_update_local cur = base.conf.color_update_remote rup = base.listPkgs(ypl.updates, _('Updated Packages'), basecmd, highlight_na=local_pkgs, columns=columns, highlight_modes={'=' : cul, 'not in' : cur}) # XXX put this into the ListCommand at some point if len(ypl.obsoletes) > 0 and basecmd == 'list': # if we've looked up obsolete lists and it's a list request rop = [0, ''] print _('Obsoleting Packages') # The tuple is (newPkg, oldPkg) ... so sort by new for obtup in sorted(ypl.obsoletesTuples, key=operator.itemgetter(0)): base.updatesObsoletesList(obtup, 'obsoletes', columns=columns, repoid=repoid) else: rop = base.listPkgs(ypl.obsoletes, _('Obsoleting Packages'), basecmd, columns=columns) rrap = base.listPkgs(ypl.recent, _('Recently Added Packages'), basecmd, columns=columns) # extcmds is pop(0)'d if they pass a "special" param like "updates" # in returnPkgLists(). This allows us to always return "ok" for # things like "yum list updates". if len(extcmds) and \ rrap[0] and rop[0] and rup[0] and rep[0] and rap[0] and rip[0]: return 1, [_('No matching Packages to list')] return 0, [] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ if len(extcmds) and extcmds[0] == 'installed': return False return True def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ if len(extcmds) and extcmds[0] in ('updates', 'obsoletes'): return 'read-only:future' if len(extcmds) and extcmds[0] in ('installed', 'distro-extras', 'extras', 'recent'): return 'read-only:past' # available/all return 'read-only:present' class ListCommand(InfoCommand): """A class containing methods needed by the cli to execute the list command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['list'] def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("List a package or groups of packages") class EraseCommand(YumCommand): """A class containing methods needed by the cli to execute the erase command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['erase', 'remove', 'autoremove', 'erase-n', 'erase-na', 'erase-nevra', 'autoremove-n', 'autoremove-na', 'autoremove-nevra', 'remove-n', 'remove-na', 'remove-nevra'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "PACKAGE..." def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Remove a package or packages from your system") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) if basecmd == 'autoremove': return checkPackageArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ pos = False if basecmd.startswith('autoremove'): # We have to alter this, as it's used in resolving stage. Which # sucks. Just be careful in "yum shell". base.conf.clean_requirements_on_remove = True basecmd = basecmd[len('auto'):] # pretend it's just remove... if not extcmds: pos = True extcmds = [] for pkg in sorted(base.rpmdb.returnLeafNodes()): if 'reason' not in pkg.yumdb_info: continue if pkg.yumdb_info.reason != 'dep': continue extcmds.append(pkg) self.doneCommand(base, _("Setting up Remove Process")) ret = base.erasePkgs(extcmds, pos=pos, basecmd=basecmd) return ret def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False def needTsRemove(self, base, basecmd, extcmds): """Return whether a transaction set for removal only must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a remove-only transaction set is needed, False otherwise """ return True class GroupsCommand(YumCommand): """ Single sub-command interface for most groups interaction. """ direct_commands = {'grouplist' : 'list', 'groupinstall' : 'install', 'groupupdate' : 'update', 'groupremove' : 'remove', 'grouperase' : 'remove', 'groupinfo' : 'info'} def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['groups', 'group'] + self.direct_commands.keys() def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[list|info|summary|install|upgrade|remove|mark] [GROUP]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Display, or use, the groups information") def _grp_setup_doCommand(self, base): self.doneCommand(base, _("Setting up Group Process")) base.doRepoSetup(dosack=0) try: base.doGroupSetup() except yum.Errors.GroupsError: return 1, [_('No Groups on which to run command')] except yum.Errors.YumBaseError, e: raise def _grp_cmd(self, basecmd, extcmds): if basecmd in self.direct_commands: cmd = self.direct_commands[basecmd] elif extcmds: cmd = extcmds[0] extcmds = extcmds[1:] else: cmd = 'summary' if cmd in ('mark', 'unmark') and extcmds: cmd = "%s-%s" % (cmd, extcmds[0]) extcmds = extcmds[1:] remap = {'update' : 'upgrade', 'erase' : 'remove', 'mark-erase' : 'mark-remove', } cmd = remap.get(cmd, cmd) return cmd, extcmds def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. The exact conditions checked will vary depending on the subcommand that is being called. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ cmd, extcmds = self._grp_cmd(basecmd, extcmds) checkEnabledRepo(base) ocmds_all = [] ocmds_arg = [] if base.conf.group_command == 'objects': ocmds_arg = ('mark-install', 'mark-remove', 'mark-blacklist', 'mark-packages', 'mark-packages-force', 'unmark-packages', 'mark-packages-sync', 'mark-packages-sync-force', 'mark-groups', 'mark-groups-force', 'unmark-groups', 'mark-groups-sync', 'mark-groups-sync-force') ocmds_all = ('mark-install', 'mark-remove', 'mark-convert', 'mark-convert-whitelist', 'mark-convert-blacklist', 'mark-blacklist', 'mark-packages', 'mark-packages-force', 'unmark-packages', 'mark-packages-sync', 'mark-packages-sync-force', 'mark-groups', 'mark-groups-force', 'unmark-groups', 'mark-groups-sync', 'mark-groups-sync-force') if cmd in ('install', 'remove', 'info') or cmd in ocmds_arg: checkGroupArg(base, cmd, extcmds) if cmd in ('install', 'remove', 'upgrade') or cmd in ocmds_all: checkRootUID(base) if cmd in ('install', 'upgrade'): checkGPGKey(base) cmds = set(('list', 'info', 'remove', 'install', 'upgrade', 'summary')) if base.conf.group_command == 'objects': cmds.update(ocmds_all) if cmd not in cmds: base.logger.critical(_('Invalid groups sub-command, use: %s.'), ", ".join(cmds)) raise cli.CliError if base.conf.group_command != 'objects': pass elif not os.path.exists(os.path.dirname(base.igroups.filename)): base.logger.critical(_("There is no installed groups file.")) base.logger.critical(_("Maybe run: yum groups mark convert (see man yum)")) elif not os.access(os.path.dirname(base.igroups.filename), os.R_OK): base.logger.critical(_("You don't have access to the groups DBs.")) raise cli.CliError elif not os.path.exists(base.igroups.filename): base.logger.critical(_("There is no installed groups file.")) base.logger.critical(_("Maybe run: yum groups mark convert (see man yum)")) elif not os.access(base.igroups.filename, os.R_OK): base.logger.critical(_("You don't have access to the groups DB.")) raise cli.CliError def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ cmd, extcmds = self._grp_cmd(basecmd, extcmds) self._grp_setup_doCommand(base) if cmd == 'summary': return base.returnGroupSummary(extcmds) if cmd == 'list': return base.returnGroupLists(extcmds) if True: # Try, YumBase... if cmd == 'info': return base.returnGroupInfo(extcmds) if cmd == 'install': return base.installGroups(extcmds) if cmd == 'upgrade': ret = base.installGroups(extcmds, upgrade=True) updateinfo.remove_txmbrs(base) return ret if cmd == 'remove': return base.removeGroups(extcmds) if cmd == 'mark-install': gRG = base._groupReturnGroups(extcmds,ignore_case=False) igrps, grps, ievgrps, evgrps = gRG for evgrp in evgrps: base.igroups.add_environment(evgrp.environmentid, evgrp.allgroups) for grp in grps: pkg_names = set() # Only see names that are installed. for pkg in base.rpmdb.searchNames(grp.packages): pkg_names.add(pkg.name) base.igroups.add_group(grp.groupid, pkg_names) base.igroups.save() return 0, ['Marked install: ' + ','.join(extcmds)] if cmd == 'mark-blacklist': gRG = base._groupReturnGroups(extcmds,ignore_case=False) igrps, grps, ievgrps, evgrps = gRG for ievgrp in ievgrps: evgrp = base.comps.return_environment(igrp.evgid) if not evgrp: continue base.igroups.changed = True ievgrp.grp_names.update(grp.groups) for igrp in igrps: grp = base.comps.return_group(igrp.gid) if not grp: continue base.igroups.changed = True igrp.pkg_names.update(grp.packages) base.igroups.save() return 0, ['Marked upgrade blacklist: ' + ','.join(extcmds)] if cmd in ('mark-packages', 'mark-packages-force'): if len(extcmds) < 2: return 1, ['No group or package given'] gRG = base._groupReturnGroups([extcmds[0]], ignore_case=False) igrps, grps, ievgrps, evgrps = gRG if igrps is None or len(igrps) != 1: return 1, ['No group matched'] grp = igrps[0] force = cmd == 'mark-packages-force' for pkg in base.rpmdb.returnPackages(patterns=extcmds[1:]): if not force and 'group_member' in pkg.yumdb_info: continue pkg.yumdb_info.group_member = grp.gid grp.pkg_names.add(pkg.name) base.igroups.changed = True base.igroups.save() return 0, ['Marked packages: ' + ','.join(extcmds[1:])] if cmd == 'unmark-packages': for pkg in base.rpmdb.returnPackages(patterns=extcmds): if 'group_member' in pkg.yumdb_info: del pkg.yumdb_info.group_member return 0, ['UnMarked packages: ' + ','.join(extcmds)] if cmd in ('mark-packages-sync', 'mark-packages-sync-force'): gRG = base._groupReturnGroups(extcmds,ignore_case=False) igrps, grps, ievgrps, evgrps = gRG if not igrps: return 1, ['No group matched'] force = cmd == 'mark-packages-sync-force' for grp in igrps: for pkg in base.rpmdb.searchNames(grp.pkg_names): if not force and 'group_member' in pkg.yumdb_info: continue pkg.yumdb_info.group_member = grp.gid if force: return 0, ['Marked packages-sync-force: '+','.join(extcmds)] else: return 0, ['Marked packages-sync: ' + ','.join(extcmds)] if cmd in ('mark-groups', 'mark-groups-force'): if len(extcmds) < 2: return 1, ['No environment or group given'] gRG = base._groupReturnGroups([extcmds[0]], ignore_case=False) igrps, grps, ievgrps, evgrps = gRG if ievgrps is None or len(ievgrps) != 1: return 1, ['No environment matched'] evgrp = ievgrps[0] force = cmd == 'mark-groups-force' gRG = base._groupReturnGroups(extcmds[1:], ignore_case=False) for grp in gRG[1]: # Packages full or empty? self.igroups.add_group(grp.groupid, grp.packages, ievgrp) if force: for grp in gRG[0]: grp.environment = evgrp.evgid base.igroups.changed = True base.igroups.save() return 0, ['Marked groups: ' + ','.join(extcmds[1:])] if cmd == 'unmark-groups': gRG = base._groupReturnGroups([extcmds[0]], ignore_case=False) igrps, grps, ievgrps, evgrps = gRG if igrps is None: return 1, ['No groups matched'] for grp in igrps: grp.environment = None base.igroups.changed = True base.igroups.save() return 0, ['UnMarked groups: ' + ','.join(extcmds)] if cmd in ('mark-groups-sync', 'mark-groups-sync-force'): gRG = base._groupReturnGroups(extcmds,ignore_case=False) igrps, grps, ievgrps, evgrps = gRG if not ievgrps: return 1, ['No environment matched'] force = cmd == 'mark-groups-sync-force' for evgrp in ievgrps: grp_names = ",".join(sorted(evgrp.grp_names)) for grp in base.igroups.return_groups(grp_names): if not force and grp.environment is not None: continue grp.environment = evgrp.evgid base.igroups.changed = True base.igroups.save() if force: return 0, ['Marked groups-sync-force: '+','.join(extcmds)] else: return 0, ['Marked groups-sync: ' + ','.join(extcmds)] # FIXME: This doesn't do environment groups atm. if cmd in ('mark-convert', 'mark-convert-whitelist', 'mark-convert-blacklist'): # Convert old style info. into groups as objects. def _convert_grp(grp): if not grp.installed: return pkg_names = [] for pkg in base.rpmdb.searchNames(grp.packages): if 'group_member' in pkg.yumdb_info: continue pkg.yumdb_info.group_member = grp.groupid pkg_names.append(pkg.name) # We only mark the packages installed as a known part of # the group. This way "group update" will work and install # any remaining packages, as it would before the conversion. if cmd == 'mark-convert-whitelist': base.igroups.add_group(grp.groupid, pkg_names) else: base.igroups.add_group(grp.groupid, grp.packages) # Blank everything. for gid in base.igroups.groups.keys(): base.igroups.del_group(gid) for pkg in base.rpmdb: if 'group_member' in pkg.yumdb_info: del pkg.yumdb_info.group_member # Need to do this by hand, when using objects, to setup the # .installed attribute in comps. base.comps.compile(base.rpmdb.simplePkgList()) # This is kind of a hack, to work around the biggest problem # with having pkgs in more than one group. Treat Fedora/EL/etc. # base/core special. Maybe other groups? # Not 100% we want to force install "core", as that's then # "different", but it is better ... so, meh. special_gids = (('core', True), ('base', False)) for gid, force_installed in special_gids: grp = base.comps.return_group(gid) if grp is None: continue if force_installed: grp.installed = True _convert_grp(grp) for grp in base.comps.get_groups(): if grp.groupid in special_gids: continue _convert_grp(grp) base.igroups.save() return 0, ['Converted old style groups to objects.'] if cmd == 'mark-remove': gRG = base._groupReturnGroups(extcmds,ignore_case=False) igrps, grps, ievgrps, evgrps = gRG for evgrp in ievgrps: base.igroups.del_environment(evgrp.evgid) for grp in igrps: base.igroups.del_group(grp.gid) base.igroups.save() return 0, ['Marked remove: ' + ','.join(extcmds)] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ cmd, extcmds = self._grp_cmd(basecmd, extcmds) if cmd in ('list', 'info', 'remove', 'summary'): return False if cmd.startswith('mark') or cmd.startswith('unmark'): return False return True def needTsRemove(self, base, basecmd, extcmds): """Return whether a transaction set for removal only must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a remove-only transaction set is needed, False otherwise """ cmd, extcmds = self._grp_cmd(basecmd, extcmds) if cmd in ('remove',): return True return False def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ cmd, extcmds = self._grp_cmd(basecmd, extcmds) if cmd in ('list', 'info', 'summary'): return 'read-only:past' if cmd.startswith('mark') or cmd.startswith('unmark'): return 'read-only:past' return 'write' class MakeCacheCommand(YumCommand): """A class containing methods needed by the cli to execute the makecache command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['makecache'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Generate the metadata cache") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that there is an enabled repository. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkEnabledRepo(base) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.logger.debug(_("Making cache files for all metadata files.")) base.logger.debug(_("This may take a while depending on the speed of this computer")) # Fast == don't download any extra MD fast = False if extcmds and extcmds[0] == 'fast': fast = True if True: # Try, YumBase... for repo in base.repos.sort(): repo.metadata_expire = 0 if not fast: repo.mdpolicy = "group:all" base.doRepoSetup(dosack=0) base.repos.doSetup() # These convert the downloaded data into usable data, # we can't remove them until *LoadRepo() can do: # 1. Download a .sqlite.bz2 and convert to .sqlite # 2. Download a .xml.gz and convert to .xml.gz.sqlite if fast: # Can't easily tell which other metadata each repo. has, so # just do primary. base.repos.populateSack(mdtype='metadata', cacheonly=1) else: base.repos.populateSack(mdtype='all', cacheonly=1) # Now decompress stuff, so that -C works, sigh. fname_map = {'group_gz' : 'groups.xml', 'pkgtags' : 'pkgtags.sqlite', 'updateinfo' : 'updateinfo.xml', 'prestodelta': 'prestodelta.xml', } for repo in base.repos.listEnabled(): for MD in repo.repoXML.fileTypes(): if MD not in fname_map: continue if MD not in repo.retrieved or not repo.retrieved[MD]: continue # For fast mode. misc.repo_gen_decompress(repo.retrieveMD(MD), fname_map[MD], cached=repo.cache) return 0, [_('Metadata Cache Created')] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class CleanCommand(YumCommand): """A class containing methods needed by the cli to execute the clean command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['clean'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[headers|packages|metadata|dbcache|plugins|expire-cache|all]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Remove cached data") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that there is at least one enabled repository, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkCleanArg(base, basecmd, extcmds) checkEnabledRepo(base) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.conf.cache = 1 return base.cleanCli(extcmds) def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' class ProvidesCommand(YumCommand): """A class containing methods needed by the cli to execute the provides command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['provides', 'whatprovides'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "SOME_STRING" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Find what package provides the given value") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkItemArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.logger.debug("Searching Packages: ") updateinfo.exclude_updates(base) return base.provides(extcmds) def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' class CheckUpdateCommand(YumCommand): """A class containing methods needed by the cli to execute the check-update command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['check-update', 'check-updates', 'check-upgrade', 'check-upgrades'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[PACKAGE...]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Check for available package updates") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that there is at least one enabled repository. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkEnabledRepo(base) def doCommand(self, base, basecmd, extcmds, repoid=None): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ updateinfo.exclude_updates(base) obscmds = ['obsoletes'] + extcmds base.extcmds.insert(0, 'updates') result = 0 if True: ypl = base.returnPkgLists(extcmds, repoid=repoid) if (base.conf.obsoletes or base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3)): typl = base.returnPkgLists(obscmds, repoid=repoid) ypl.obsoletes = typl.obsoletes ypl.obsoletesTuples = typl.obsoletesTuples columns = _list_cmd_calc_columns(base, ypl) if len(ypl.updates) > 0: local_pkgs = {} highlight = base.term.MODE['bold'] if highlight: # Do the local/remote split we get in "yum updates" for po in sorted(ypl.updates): if po.repo.id != 'installed' and po.verifyLocalPkg(): local_pkgs[(po.name, po.arch)] = po cul = base.conf.color_update_local cur = base.conf.color_update_remote base.listPkgs(ypl.updates, '', outputType='list', highlight_na=local_pkgs, columns=columns, highlight_modes={'=' : cul, 'not in' : cur}) result = 100 if len(ypl.obsoletes) > 0: # This only happens in verbose mode print _('Obsoleting Packages') # The tuple is (newPkg, oldPkg) ... so sort by new for obtup in sorted(ypl.obsoletesTuples, key=operator.itemgetter(0)): base.updatesObsoletesList(obtup, 'obsoletes', columns=columns, repoid=repoid) result = 100 # Add check_running_kernel call, if updateinfo is available. if (base.conf.autocheck_running_kernel and updateinfo._repos_downloaded(base.repos.listEnabled())): def _msg(x): base.verbose_logger.info("%s", x) updateinfo._check_running_kernel(base, base.upinfo, _msg) return result, [] def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:future' class SearchCommand(YumCommand): """A class containing methods needed by the cli to execute the search command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['search'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "SOME_STRING" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Search package details for the given string") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkItemArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.logger.debug(_("Searching Packages: ")) updateinfo.exclude_updates(base) return base.search(extcmds) def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:present' class UpgradeCommand(YumCommand): """A class containing methods needed by the cli to execute the upgrade command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['upgrade', 'upgrade-to'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return 'PACKAGE...' def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Update packages taking obsoletes into account") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, and that there are enabled repositories with gpg. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.conf.obsoletes = 1 self.doneCommand(base, _("Setting up Upgrade Process")) ret = base.updatePkgs(extcmds, update_to=(basecmd == 'upgrade-to')) updateinfo.remove_txmbrs(base) return ret class LocalInstallCommand(YumCommand): """A class containing methods needed by the cli to execute the localinstall command. """ def __init__(self): YumCommand.__init__(self) self.hidden = True def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['localinstall', 'localupdate'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "FILE" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Install a local RPM") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkPackageArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Local Package Process")) updateonly = basecmd == 'localupdate' return base.localInstall(filelist=extcmds, updateonly=updateonly) def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class ResolveDepCommand(YumCommand): """A class containing methods needed by the cli to execute the resolvedep command. """ def __init__(self): YumCommand.__init__(self) self.hidden = True def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['resolvedep'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "DEPENDENCY" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return "repoquery --pkgnarrow=all --whatprovides --qf '%{envra} %{ui_from_repo}'" def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ base.logger.debug(_("Searching Packages for Dependency:")) updateinfo.exclude_updates(base) return base.resolveDepCli(extcmds) def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' class ShellCommand(YumCommand): """A class containing methods needed by the cli to execute the shell command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['shell'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[FILENAME]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Run an interactive yum shell") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkShellArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _('Setting up Yum Shell')) return base.doShell() def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class DepListCommand(YumCommand): """A class containing methods needed by the cli to execute the deplist command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['deplist'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return 'PACKAGE...' def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("List a package's dependencies") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkPackageArg(base, basecmd, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Finding dependencies: ")) updateinfo.exclude_updates(base) return base.deplist(extcmds) def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' # read-only ? class RepoListCommand(YumCommand): """A class containing methods needed by the cli to execute the repolist command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ('repolist', 'repoinfo') def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return '[all|enabled|disabled]' def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _('Display the configured software repositories') def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ def _repo_size(repo): ret = 0 for pkg in repo.sack.returnPackages(): ret += pkg.packagesize return base.format_number(ret) def _repo_match(repo, patterns): for pat in patterns: if repo in base.repos.findRepos(pat, name_match=True, ignore_case=True): return True return False def _num2ui_num(num): return to_unicode(locale.format("%d", num, True)) if len(extcmds) >= 1 and extcmds[0] in ('all', 'disabled', 'enabled'): arg = extcmds[0] extcmds = extcmds[1:] else: arg = 'enabled' extcmds = map(lambda x: x.lower(), extcmds) if basecmd == 'repoinfo': verbose = True else: verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) if arg != 'disabled' or extcmds: try: # Setup so len(repo.sack) is correct base.repos.populateSack() base.pkgSack # Need to setup the pkgSack, so excludes work except yum.Errors.RepoError: if verbose: raise # populate them by hand, so one failure doesn't kill everything # after it. for repo in base.repos.listEnabled(): try: base.repos.populateSack(repo.id) except yum.Errors.RepoError: pass repos = base.repos.repos.values() repos.sort() enabled_repos = base.repos.listEnabled() on_ehibeg = base.term.FG_COLOR['green'] + base.term.MODE['bold'] on_dhibeg = base.term.FG_COLOR['red'] on_hiend = base.term.MODE['normal'] tot_num = 0 cols = [] for repo in repos: if len(extcmds) and not _repo_match(repo, extcmds): continue (ehibeg, dhibeg, hiend) = '', '', '' ui_enabled = '' ui_endis_wid = 0 ui_num = "" ui_excludes_num = '' force_show = False if arg == 'all' or repo.id in extcmds or repo.name in extcmds: force_show = True (ehibeg, dhibeg, hiend) = (on_ehibeg, on_dhibeg, on_hiend) if repo in enabled_repos: enabled = True if arg == 'enabled': force_show = False elif arg == 'disabled' and not force_show: continue if force_show or verbose: ui_enabled = ehibeg + _('enabled') + hiend ui_endis_wid = utf8_width(_('enabled')) if not verbose: ui_enabled += ": " ui_endis_wid += 2 if verbose: ui_size = _repo_size(repo) # We don't show status for list disabled if arg != 'disabled' or verbose: if verbose or base.conf.exclude or repo.exclude: num = len(repo.sack.simplePkgList()) else: num = len(repo.sack) ui_num = _num2ui_num(num) excludes = repo.sack._excludes excludes = len([pid for r,pid in excludes if r == repo]) if excludes: ui_excludes_num = _num2ui_num(excludes) if not verbose: ui_num += "+%s" % ui_excludes_num tot_num += num else: enabled = False if arg == 'disabled': force_show = False elif arg == 'enabled' and not force_show: continue ui_enabled = dhibeg + _('disabled') + hiend ui_endis_wid = utf8_width(_('disabled')) if not verbose: rid = repo.ui_id # can't use str() if repo.metadata_expire >= 0: if os.path.exists(repo.metadata_cookie): last = os.stat(repo.metadata_cookie).st_mtime if last + repo.metadata_expire < time.time(): rid = '!' + rid if enabled and repo.metalink: mdts = repo.metalink_data.repomd.timestamp if mdts > repo.repoXML.timestamp: rid = '*' + rid cols.append((rid, repo.name, (ui_enabled, ui_endis_wid), ui_num)) else: if enabled: md = repo.repoXML else: md = None out = [base.fmtKeyValFill(_("Repo-id : "), repo.ui_id), base.fmtKeyValFill(_("Repo-name : "), repo.name)] if force_show or extcmds: out += [base.fmtKeyValFill(_("Repo-status : "), ui_enabled)] if md and md.revision is not None: out += [base.fmtKeyValFill(_("Repo-revision: "), md.revision)] if md and md.tags['content']: tags = md.tags['content'] out += [base.fmtKeyValFill(_("Repo-tags : "), ", ".join(sorted(tags)))] if md and md.tags['distro']: for distro in sorted(md.tags['distro']): tags = md.tags['distro'][distro] out += [base.fmtKeyValFill(_("Repo-distro-tags: "), "[%s]: %s" % (distro, ", ".join(sorted(tags))))] if md: out += [base.fmtKeyValFill(_("Repo-updated : "), time.ctime(md.timestamp)), base.fmtKeyValFill(_("Repo-pkgs : "),ui_num), base.fmtKeyValFill(_("Repo-size : "),ui_size)] if hasattr(repo, '_orig_baseurl'): baseurls = repo._orig_baseurl else: baseurls = repo.baseurl if baseurls: out += [base.fmtKeyValFill(_("Repo-baseurl : "), ", ".join(baseurls))] if enabled: # This needs to be here due to the mirrorlists are # metalinks hack. repo.urls if repo.metalink: out += [base.fmtKeyValFill(_("Repo-metalink: "), repo.metalink)] if enabled: ts = repo.metalink_data.repomd.timestamp out += [base.fmtKeyValFill(_(" Updated : "), time.ctime(ts))] elif repo.mirrorlist: out += [base.fmtKeyValFill(_("Repo-mirrors : "), repo.mirrorlist)] if enabled and repo.urls and not baseurls: url = repo.urls[0] if len(repo.urls) > 1: url += ' (%d more)' % (len(repo.urls) - 1) out += [base.fmtKeyValFill(_("Repo-baseurl : "), url)] if not os.path.exists(repo.metadata_cookie): last = _("Unknown") else: last = os.stat(repo.metadata_cookie).st_mtime last = time.ctime(last) if repo.metadata_expire <= -1: num = _("Never (last: %s)") % last elif not repo.metadata_expire: num = _("Instant (last: %s)") % last else: num = _num2ui_num(repo.metadata_expire) num = _("%s second(s) (last: %s)") % (num, last) out += [base.fmtKeyValFill(_("Repo-expire : "), num), base.fmtKeyValFill(_(" Filter : "), repo.metadata_expire_filter), ] if repo.exclude: out += [base.fmtKeyValFill(_("Repo-exclude : "), ", ".join(repo.exclude))] if repo.includepkgs: out += [base.fmtKeyValFill(_("Repo-include : "), ", ".join(repo.includepkgs))] if ui_excludes_num: out += [base.fmtKeyValFill(_("Repo-excluded: "), ui_excludes_num)] if repo.repofile: out += [base.fmtKeyValFill(_("Repo-filename: "), repo.repofile)] base.verbose_logger.info("%s\n", "\n".join(map(misc.to_unicode, out))) if not verbose and cols: # Work out the first (id) and last (enabled/disalbed/count), # then chop the middle (name)... id_len = utf8_width(_('repo id')) nm_len = 0 st_len = 0 ui_len = 0 for (rid, rname, (ui_enabled, ui_endis_wid), ui_num) in cols: if id_len < utf8_width(rid): id_len = utf8_width(rid) if nm_len < utf8_width(rname): nm_len = utf8_width(rname) if st_len < (ui_endis_wid + len(ui_num)): st_len = (ui_endis_wid + len(ui_num)) # Need this as well as above for: utf8_width_fill() if ui_len < len(ui_num): ui_len = len(ui_num) if arg == 'disabled': # Don't output a status column. left = base.term.columns - (id_len + 1) elif utf8_width(_('status')) > st_len: left = base.term.columns - (id_len + utf8_width(_('status')) +2) else: left = base.term.columns - (id_len + st_len + 2) if left < nm_len: # Name gets chopped nm_len = left else: # Share the extra... left -= nm_len id_len += left / 2 nm_len += left - (left / 2) txt_rid = utf8_width_fill(_('repo id'), id_len) txt_rnam = utf8_width_fill(_('repo name'), nm_len, nm_len) if arg == 'disabled': # Don't output a status column. base.verbose_logger.info("%s %s", txt_rid, txt_rnam) else: base.verbose_logger.info("%s %s %s", txt_rid, txt_rnam, _('status')) for (rid, rname, (ui_enabled, ui_endis_wid), ui_num) in cols: if arg == 'disabled': # Don't output a status column. base.verbose_logger.info("%s %s", utf8_width_fill(rid, id_len), utf8_width_fill(rname, nm_len, nm_len)) continue if ui_num: ui_num = utf8_width_fill(ui_num, ui_len, left=False) base.verbose_logger.info("%s %s %s%s", utf8_width_fill(rid, id_len), utf8_width_fill(rname, nm_len, nm_len), ui_enabled, ui_num) return 0, ['repolist: ' +to_unicode(locale.format("%d", tot_num, True))] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' class HelpCommand(YumCommand): """A class containing methods needed by the cli to execute the help command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['help'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "COMMAND" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Display a helpful usage message") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run; namely that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ if len(extcmds) == 0: base.usage() raise cli.CliError elif len(extcmds) > 1 or extcmds[0] not in base.yum_cli_commands: base.usage() raise cli.CliError @staticmethod def _makeOutput(command): canonical_name = command.getNames()[0] # Check for the methods in case we have plugins that don't # implement these. # XXX Remove this once usage/summary are common enough try: usage = command.getUsage() except (AttributeError, NotImplementedError): usage = None try: summary = command.getSummary() except (AttributeError, NotImplementedError): summary = None # XXX need detailed help here, too help_output = "" if usage is not None: help_output += "%s %s" % (canonical_name, usage) if summary is not None: help_output += "\n\n%s" % summary if usage is None and summary is None: help_output = _("No help available for %s") % canonical_name command_names = command.getNames() if len(command_names) > 1: if len(command_names) > 2: help_output += _("\n\naliases: ") else: help_output += _("\n\nalias: ") help_output += ', '.join(command.getNames()[1:]) return help_output def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ if extcmds[0] in base.yum_cli_commands: command = base.yum_cli_commands[extcmds[0]] base.verbose_logger.info(self._makeOutput(command)) return 0, [] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class ReInstallCommand(YumCommand): """A class containing methods needed by the cli to execute the reinstall command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['reinstall'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "PACKAGE..." def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkPackageArg(base, basecmd, extcmds) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Reinstall Process")) return base.reinstallPkgs(extcmds) def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("reinstall a package") def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class DowngradeCommand(YumCommand): """A class containing methods needed by the cli to execute the downgrade command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['downgrade'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "PACKAGE..." def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkPackageArg(base, basecmd, extcmds) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ self.doneCommand(base, _("Setting up Downgrade Process")) return base.downgradePkgs(extcmds) def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("downgrade a package") def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False class VersionCommand(YumCommand): """A class containing methods needed by the cli to execute the version command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['version'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[all|installed|available]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Display a version for the machine and/or available repos.") def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ vcmd = 'installed' if extcmds: vcmd = extcmds[0] if vcmd in ('grouplist', 'groupinfo', 'nogroups', 'nogroups-installed', 'nogroups-available', 'nogroups-all', 'installed', 'all', 'group-installed', 'group-all', 'available', 'all', 'group-available', 'group-all'): extcmds = extcmds[1:] else: vcmd = 'installed' def _append_repos(cols, repo_data): for repoid in sorted(repo_data): cur = repo_data[repoid] ncols = [] last_rev = None for rev in sorted(cur): if rev is None: continue last_rev = cur[rev] ncols.append((" %s/%s" % (repoid, rev), str(cur[rev]))) if None in cur and (not last_rev or cur[None] != last_rev): cols.append((" %s" % repoid, str(cur[None]))) cols.extend(ncols) verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) groups = {} if vcmd in ('nogroups', 'nogroups-installed', 'nogroups-available', 'nogroups-all'): gconf = [] if vcmd == 'nogroups': vcmd = 'installed' else: vcmd = vcmd[len('nogroups-'):] else: gconf = yum.config.readVersionGroupsConfig() for group in gconf: groups[group] = set(gconf[group].pkglist) if gconf[group].run_with_packages: groups[group].update(base.run_with_package_names) if vcmd == 'grouplist': print _(" Yum version groups:") for group in sorted(groups): print " ", group return 0, ['version grouplist'] if vcmd == 'groupinfo': for group in groups: if group not in extcmds: continue print _(" Group :"), group print _(" Packages:") if not verbose: for pkgname in sorted(groups[group]): print " ", pkgname else: data = {'envra' : {}, 'rid' : {}} pkg_names = groups[group] pkg_names2pkgs = base._group_names2aipkgs(pkg_names) base._calcDataPkgColumns(data, pkg_names, pkg_names2pkgs) data = [data['envra'], data['rid']] columns = base.calcColumns(data) columns = (-columns[0], -columns[1]) base._displayPkgsFromNames(pkg_names, True, pkg_names2pkgs, columns=columns) return 0, ['version groupinfo'] # Have a way to manually specify a dynamic group of packages, whee. if not vcmd.startswith("group-") and extcmds: for dgrp in extcmds: if '/' not in dgrp: # It's a package name, add it to the cmd line group... if '' not in groups: groups[''] = set() groups[''].add(dgrp) else: # It's a file containing a list of packages... if not os.path.exists(dgrp): base.logger.warn(_(" File doesn't exist: %s"), dgrp) else: pkg_names = open(dgrp).readlines() pkg_names = set(n.strip() for n in pkg_names) dgrp = os.path.basename(dgrp) if dgrp in groups: for num in range(1, 100): ndgrp = dgrp + str(num) if ndgrp in groups: continue dgrp = ndgrp break groups[dgrp] = pkg_names rel = base.conf.yumvar['releasever'] ba = base.conf.yumvar['basearch'] cols = [] if vcmd in ('installed', 'all', 'group-installed', 'group-all'): if True: # Try, YumBase... data = base.rpmdb.simpleVersion(not verbose, groups=groups) lastdbv = base.history.last() if lastdbv is not None: lastdbv = lastdbv.end_rpmdbversion if lastdbv is not None and data[0] != lastdbv: base._rpmdb_warn_checks(warn=lastdbv is not None) if vcmd not in ('group-installed', 'group-all'): cols.append(("%s %s/%s" % (_("Installed:"), rel, ba), str(data[0]))) _append_repos(cols, data[1]) if groups: for grp in sorted(data[2]): if (vcmd.startswith("group-") and extcmds and grp not in extcmds): continue cols.append(("%s %s" % (_("Group-Installed:"), grp), str(data[2][grp]))) _append_repos(cols, data[3][grp]) if vcmd in ('available', 'all', 'group-available', 'group-all'): if True: # Try, YumBase... data = base.pkgSack.simpleVersion(not verbose, groups=groups) if vcmd not in ('group-available', 'group-all'): cols.append(("%s %s/%s" % (_("Available:"), rel, ba), str(data[0]))) if verbose: _append_repos(cols, data[1]) if groups: for grp in sorted(data[2]): if (vcmd.startswith("group-") and extcmds and grp not in extcmds): continue cols.append(("%s %s" % (_("Group-Available:"), grp), str(data[2][grp]))) if verbose: _append_repos(cols, data[3][grp]) data = {'rid' : {}, 'ver' : {}} for (rid, ver) in cols: for (d, v) in (('rid', len(rid)), ('ver', len(ver))): data[d].setdefault(v, 0) data[d][v] += 1 data = [data['rid'], data['ver']] columns = base.calcColumns(data) columns = (-columns[0], columns[1]) for line in cols: print base.fmtColumns(zip(line, columns)) return 0, ['version'] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ vcmd = 'installed' if extcmds: vcmd = extcmds[0] verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) if vcmd == 'groupinfo' and verbose: return True return vcmd in ('available', 'all', 'group-available', 'group-all') def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:present' class HistoryCommand(YumCommand): """A class containing methods needed by the cli to execute the history command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['history'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[info|list|packages-list|summary|addon-info|redo|undo|rollback|new]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Display, or use, the transaction history") def _hcmd_redo(self, base, extcmds): kwargs = {'force_reinstall' : False, 'force_changed_removal' : False, } kwargs_map = {'reinstall' : 'force_reinstall', 'force-reinstall' : 'force_reinstall', 'remove' : 'force_changed_removal', 'force-remove' : 'force_changed_removal', } while len(extcmds) > 1: done = False for arg in extcmds[1].replace(' ', ',').split(','): if arg not in kwargs_map: continue done = True key = kwargs_map[extcmds[1]] kwargs[key] = not kwargs[key] if not done: break extcmds = [extcmds[0]] + extcmds[2:] old = base._history_get_transaction(extcmds) if old is None: return 1, ['Failed history redo'] tm = time.ctime(old.beg_timestamp) print "Repeating transaction %u, from %s" % (old.tid, tm) base.historyInfoCmdPkgsAltered(old) if base.history_redo(old, **kwargs): return 2, ["Repeating transaction %u" % (old.tid,)] def _hcmd_undo(self, base, extcmds): old = base._history_get_transaction(extcmds) if old is None: return 1, ['Failed history undo'] tm = time.ctime(old.beg_timestamp) print "Undoing transaction %u, from %s" % (old.tid, tm) base.historyInfoCmdPkgsAltered(old) if base.history_undo(old): return 2, ["Undoing transaction %u" % (old.tid,)] def _hcmd_rollback(self, base, extcmds): force = False if len(extcmds) > 1 and extcmds[1] == 'force': force = True extcmds = extcmds[:] extcmds.pop(0) old = base._history_get_transaction(extcmds) if old is None: return 1, ['Failed history rollback, no transaction'] last = base.history.last() if last is None: return 1, ['Failed history rollback, no last?'] if old.tid == last.tid: return 0, ['Rollback to current, nothing to do'] mobj = None for tid in base.history.old(range(old.tid + 1, last.tid + 1)): if not force and (tid.altered_lt_rpmdb or tid.altered_gt_rpmdb): if tid.altered_lt_rpmdb: msg = "Transaction history is incomplete, before %u." else: msg = "Transaction history is incomplete, after %u." print msg % tid.tid print " You can use 'history rollback force', to try anyway." return 1, ['Failed history rollback, incomplete'] if mobj is None: mobj = yum.history.YumMergedHistoryTransaction(tid) else: mobj.merge(tid) tm = time.ctime(old.beg_timestamp) print "Rollback to transaction %u, from %s" % (old.tid, tm) print base.fmtKeyValFill(" Undoing the following transactions: ", ", ".join((str(x) for x in mobj.tid))) base.historyInfoCmdPkgsAltered(mobj) if base.history_undo(mobj): return 2, ["Rollback to transaction %u" % (old.tid,)] def _hcmd_new(self, base, extcmds): base.history._create_db_file() def _hcmd_stats(self, base, extcmds): print "File :", base.history._db_file num = os.stat(base.history._db_file).st_size print "Size :", locale.format("%d", num, True) trans_N = base.history.last() if trans_N is None: print _("Transactions:"), 0 return counts = base.history._pkg_stats() trans_1 = base.history.old("1")[0] print _("Transactions:"), trans_N.tid print _("Begin time :"), time.ctime(trans_1.beg_timestamp) print _("End time :"), time.ctime(trans_N.end_timestamp) print _("Counts :") print _(" NEVRAC :"), locale.format("%6d", counts['nevrac'], True) print _(" NEVRA :"), locale.format("%6d", counts['nevra'], True) print _(" NA :"), locale.format("%6d", counts['na'], True) print _(" NEVR :"), locale.format("%6d", counts['nevr'], True) print _(" rpm DB :"), locale.format("%6d", counts['rpmdb'], True) print _(" yum DB :"), locale.format("%6d", counts['yumdb'], True) def _hcmd_sync(self, base, extcmds): extcmds = extcmds[1:] if not extcmds: extcmds = None for ipkg in sorted(base.rpmdb.returnPackages(patterns=extcmds)): if base.history.pkg2pid(ipkg, create=False) is None: continue print "Syncing rpm/yum DB data for:", ipkg, "...", if base.history.sync_alldb(ipkg): print "Done." else: print "FAILED." def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. The exact conditions checked will vary depending on the subcommand that is being called. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ cmds = ('list', 'info', 'summary', 'repeat', 'redo', 'undo', 'new', 'rollback', 'addon', 'addon-info', 'stats', 'statistics', 'sync', 'synchronize' 'pkg', 'pkgs', 'pkg-list', 'pkgs-list', 'package', 'package-list', 'packages', 'packages-list', 'pkg-info', 'pkgs-info', 'package-info', 'packages-info') if extcmds and extcmds[0] not in cmds: base.logger.critical(_('Invalid history sub-command, use: %s.'), ", ".join(cmds)) raise cli.CliError if extcmds and extcmds[0] in ('repeat', 'redo', 'undo', 'rollback', 'new'): checkRootUID(base) checkGPGKey(base) elif not (base.history._db_file and os.access(base.history._db_file, os.R_OK)): base.logger.critical(_("You don't have access to the history DB.")) raise cli.CliError def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ vcmd = 'list' if extcmds: vcmd = extcmds[0] if False: pass elif vcmd == 'list': ret = base.historyListCmd(extcmds) elif vcmd == 'info': ret = base.historyInfoCmd(extcmds) elif vcmd == 'summary': ret = base.historySummaryCmd(extcmds) elif vcmd in ('addon', 'addon-info'): ret = base.historyAddonInfoCmd(extcmds) elif vcmd in ('pkg', 'pkgs', 'pkg-list', 'pkgs-list', 'package', 'package-list', 'packages', 'packages-list'): ret = base.historyPackageListCmd(extcmds) elif vcmd == 'undo': ret = self._hcmd_undo(base, extcmds) elif vcmd in ('redo', 'repeat'): ret = self._hcmd_redo(base, extcmds) elif vcmd == 'rollback': ret = self._hcmd_rollback(base, extcmds) elif vcmd == 'new': ret = self._hcmd_new(base, extcmds) elif vcmd in ('stats', 'statistics'): ret = self._hcmd_stats(base, extcmds) elif vcmd in ('sync', 'synchronize'): ret = self._hcmd_sync(base, extcmds) elif vcmd in ('pkg-info', 'pkgs-info', 'package-info', 'packages-info'): ret = base.historyPackageInfoCmd(extcmds) if ret is None: return 0, ['history %s' % (vcmd,)] return ret def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ vcmd = 'list' if extcmds: vcmd = extcmds[0] return vcmd in ('repeat', 'redo', 'undo', 'rollback') def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ vcmd = 'list' if extcmds: vcmd = extcmds[0] if vcmd in ('repeat', 'redo', 'undo', 'rollback'): return 'write' return 'read-only:past' class CheckRpmdbCommand(YumCommand): """A class containing methods needed by the cli to execute the check-rpmdb command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['check', 'check-rpmdb'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[dependencies|duplicates|all]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Check for problems in the rpmdb") def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ chkcmd = 'all' if extcmds: chkcmd = extcmds def _out(x): print to_unicode(x.__str__()) rc = 0 if base._rpmdb_warn_checks(out=_out, warn=False, chkcmd=chkcmd, header=lambda x: None): rc = 1 return rc, ['%s %s' % (basecmd, chkcmd)] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ return False def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ return 'read-only:past' class LoadTransactionCommand(YumCommand): """A class containing methods needed by the cli to execute the load-transaction command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['load-transaction', 'load-ts', 'ts-load'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "filename" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("load a saved transaction from filename") def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ def _pkg_avail(l): if not l.startswith('mbr:'): return True # Kind of ... try: pkgtup, current_state = l.split(':')[1].strip().split(' ') current_state = int(current_state.strip()) pkgtup = tuple(pkgtup.strip().split(',')) if current_state == yum.TS_INSTALL: po = base.getInstalledPackageObject(pkgtup) elif current_state == yum.TS_AVAILABLE: po = base.getPackageObject(pkgtup) else: return False # Bad... except: return False # Bad... return True def _pkg_counts(l, counts): if not l.startswith(' ts_state: '): return state = l[len(' ts_state: '):] if state in ('e', 'od', 'ud'): counts['remove'] += 1 elif state in ('i', 'u'): counts['install'] += 1 if not extcmds: extcmds = [tempfile.gettempdir()] load_file = extcmds[0] if os.path.isdir(load_file): self.doneCommand(base, _("showing transaction files from %s") % load_file) yumtxs = sorted(glob.glob("%s/*.yumtx" % load_file)) currpmv = None done = False for yumtx in yumtxs: data = base._load_ts_data(yumtx) if data[0] is not None: continue # Bad file... data = data[1] rpmv = data[0].strip() if currpmv is None: currpmv = str(base.rpmdb.simpleVersion(main_only=True)[0]) if rpmv == currpmv: current = _('y') else: current = ' ' # Not usable is the most common # See load_ts() for data ... try: numrepos = int(data[2].strip()) pkgstart = 3+numrepos numpkgs = int(data[pkgstart].strip()) pkgstart += 1 except: continue counts = {'install' : 0, 'remove' : 0} for l in data[pkgstart:]: l = l.rstrip() _pkg_counts(l, counts) # Check to see if all the packages are available.. bad = ' ' for l in data[pkgstart:]: l = l.rstrip() if _pkg_avail(l): continue bad = '*' break # assert (counts['install'] + counts['remove']) == numpkgs current = '%s%s' % (bad, current) if not done: pkgititle = _("Install") pkgilen = utf8_width(pkgititle) if pkgilen < 6: pkgilen = 6 pkgititle = utf8_width_fill(pkgititle, pkgilen) pkgetitle = _("Remove") pkgelen = utf8_width(pkgetitle) if pkgelen < 6: pkgelen = 6 pkgetitle = utf8_width_fill(pkgetitle, pkgelen) print "?? |", pkgititle, "|", pkgetitle, "|", _("Filename") done = True numipkgs = locale.format("%d", counts['install'], True) numipkgs = "%*s" % (pkgilen, numipkgs) numepkgs = locale.format("%d", counts['remove'], True) numepkgs = "%*s" % (pkgelen, numepkgs) print "%s | %s | %s | %s" % (current, numipkgs, numepkgs, os.path.basename(yumtx)) return 0, [_('Saved transactions from %s; looked at %u files') % (load_file, len(yumtxs))] self.doneCommand(base, _("loading transaction from %s") % load_file) base.load_ts(load_file) return 2, [_('Transaction loaded from %s with %s members') % (load_file, len(base.tsInfo.getMembers()))] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ if not extcmds or os.path.isdir(extcmds[0]): return False return True def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ if not extcmds or os.path.isdir(extcmds[0]): return 'read-only:past' return 'write' class SwapCommand(YumCommand): """A class containing methods needed by the cli to execute the swap command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['swap'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return "[remove|cmd] [-- install|cmd] " def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Simple way to swap packages, instead of using shell") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkSwapPackageArg(base, basecmd, extcmds) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ if '--' in extcmds: off = extcmds.index('--') rextcmds = extcmds[:off] iextcmds = extcmds[off+1:] else: rextcmds = extcmds[:1] iextcmds = extcmds[1:] if not (rextcmds and iextcmds): return 1, ['swap'] # impossible if rextcmds[0] not in base.yum_cli_commands: rextcmds = ['remove'] + rextcmds if iextcmds[0] not in base.yum_cli_commands: iextcmds = ['install'] + iextcmds # Very similar to what the shell command does... ocmds = base.cmds oline = base.cmdstring for cmds in (rextcmds, iextcmds): base.cmdstring = " ".join(cmds) base.cmds = cmds # Don't call this atm. as the line has gone through it already, # also makes it hard to do the "is ?extcmds[0] a cmd" check. # base.plugins.run('args', args=base.cmds) # We don't catch exceptions, just pass them up and fail... base.parseCommands() cmdret = base.doCommands() if cmdret[0] != 2: return cmdret[0], ['%s %s' % (basecmd, " ".join(cmds))] base.cmds = ocmds base.cmdstring = oline return 2, ['%s %s' % (basecmd, " ".join(extcmds))] class RepoPkgsCommand(YumCommand): """A class containing methods needed by the cli to execute the repo command. """ def getNames(self): """Return a list containing the names of this command. This command can be called from the command line by using any of these names. :return: a list containing the names of this command """ return ['repo-pkgs', 'repo-packages', 'repository-pkgs', 'repository-packages'] def getUsage(self): """Return a usage string for this command. :return: a usage string for this command """ return " [pkg(s)]" def getSummary(self): """Return a one line summary of this command. :return: a one line summary of this command """ return _("Treat a repo. as a group of packages, so we can install/remove all of them") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) checkRepoPackageArg(base, basecmd, extcmds) checkEnabledRepo(base, extcmds) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ def _add_repopkg2txmbrs(txmbrs, repoid): for txmbr in txmbrs: txmbr.repopkg = repoid repoid = extcmds[0] cmd = extcmds[1] args = extcmds[2:] noargs = False if not args: noargs = True args = ['*'] num = 0 remap = {'erase' : 'remove', 'erase-or-reinstall' : 'remove-or-reinstall', 'erase-or-sync' : 'remove-or-sync', 'erase-or-distro-sync' : 'remove-or-sync', 'remove-or-distro-sync' : 'remove-or-sync', 'erase-or-distribution-synchronization' : 'remove-or-sync', 'remove-or-distribution-synchronization' : 'remove-or-sync', 'upgrade' : 'update', # Hack, but meh. 'upgrade-to' : 'update-to', # Hack, but meh. 'check-upgrade' : 'check-update', # Hack, but meh. 'check-upgrades' : 'check-update', # Hack, but meh. 'check-updates' : 'check-update', # Hack, but meh. } cmd = remap.get(cmd, cmd) if False: pass elif cmd == 'list': # list/info is easiest... return ListCommand().doCommand(base, cmd, args, repoid=repoid) elif cmd == 'info': return InfoCommand().doCommand(base, cmd, args, repoid=repoid) elif cmd == 'check-update': return CheckUpdateCommand().doCommand(base, cmd, args,repoid=repoid) elif cmd == 'install': # install is simpler version of installPkgs... for arg in args: txmbrs = base.install(pattern=arg, repoid=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: return 2, P_('%d package to install', '%d packages to install', num) elif cmd == 'update': # update is basically the same as install... for arg in args: txmbrs = base.update(pattern=arg, repoid=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: updateinfo.remove_txmbrs(base) return 2, P_('%d package to update', '%d packages to update', num) elif cmd == 'update-to': # update is basically the same as install... for arg in args: txmbrs = base.update(pattern=arg, update_to=True, repoid=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: updateinfo.remove_txmbrs(base) return 2, P_('%d package to update', '%d packages to update', num) elif cmd in ('reinstall-old', 'reinstall-installed'): # We have to choose for reinstall, for "reinstall foo" do we mean: # 1. reinstall the packages that are currently installed from "foo". # 2. reinstall the packages specified to the ones from "foo" # This is for installed.from_repo=foo if noargs: onot_found_a = base._not_found_a.copy() for arg in args: txmbrs = base.reinstall(pattern=arg, repoid=repoid, repoid_install=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if noargs: base._not_found_a = onot_found_a if num: return 2, P_('%d package to reinstall', '%d packages to reinstall', num) elif cmd in ('reinstall-new', 'reinstall-available', 'move-to'): # This is for move-to the packages from this repo. if noargs: onot_found_a = base._not_found_a.copy() for arg in args: txmbrs = base.reinstall(pattern=arg, repoid_install=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if noargs: base._not_found_a = onot_found_a if num: return 2, P_('%d package to move to', '%d packages to move to', num) elif cmd == 'reinstall': # This means "guess", so doing the old version unless it produces # nothing, in which case try switching. if noargs: onot_found_a = base._not_found_a.copy() for arg in args: try: txmbrs = base.reinstall(pattern=arg, repoid=repoid,repoid_install=repoid) except yum.Errors.ReinstallRemoveError: continue _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if noargs: base._not_found_a = onot_found_a.copy() if num: return 2, P_('%d package to reinstall', '%d packages to reinstall', num) for arg in args: txmbrs = base.reinstall(pattern=arg, repoid_install=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if noargs: base._not_found_a = onot_found_a if num: return 2, P_('%d package to move to', '%d packages to move to', num) elif cmd == 'remove': # Also mostly the same... for arg in args: txmbrs = base.remove(pattern=arg, repoid=repoid) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: return 2, P_('%d package to remove', '%d packages to remove', num) elif cmd == 'remove-or-reinstall': # More complicated... for arg in args: txmbrs = base.remove(pattern=arg, repoid=repoid) # Add an install() if it's in another repo. for txmbr in txmbrs[:]: pkgs = base.pkgSack.searchPkgTuple(txmbr.pkgtup) for pkg in sorted(pkgs): if pkg.repoid == repoid: continue txmbrs += base.install(po=pkg) break _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: return 2, P_('%d package to remove/reinstall', '%d packages to remove/reinstall', num) elif cmd == 'remove-or-sync': # Even more complicated... for arg in args: txmbrs = base.remove(pattern=arg, repoid=repoid) # Add an install/upgrade/downgrade if a version is in another # repo. for txmbr in txmbrs[:]: pkgs = base.pkgSack.searchNames([txmbr.name]) apkgs = None for pkg in sorted(pkgs): if pkg.repoid == repoid: # Backwards filter_pkgs_repoid continue if apkgs and pkg.verEQ(apkgs[0]): apkgs.append(pkg) else: apkgs = [pkg] if apkgs: for pkg in apkgs: if pkg.arch != txmbr.arch: continue apkgs = [pkg] break if len(apkgs) != 1: apkgs = base.bestPackagesFromList(apkgs) for toinst in apkgs: n,a,e,v,r = toinst.pkgtup if toinst.verEQ(txmbr.po): txmbrs += base.install(po=toinst) elif toinst.verGT(txmbr.po): txmbrs += base.update(po=toinst) else: base.tsInfo.remove(txmbr.pkgtup) txmbrs.remove(txmbr) txmbrs += base.downgrade(po=toinst) _add_repopkg2txmbrs(txmbrs, repoid) num += len(txmbrs) if num: return 2, P_('%d package to remove/sync', '%d packages to remove/sync', num) else: return 1, [_('Not a valid sub-command of %s') % basecmd] return 0, [_('Nothing to do')] def needTs(self, base, basecmd, extcmds): """Return whether a transaction set must be set up before this command can run. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: True if a transaction set is needed, False otherwise """ cmd = 'install' if len(extcmds) > 1: cmd = extcmds[1] if cmd in ('info', 'list'): return InfoCommand().needTs(base, cmd, extcmds[2:]) return True def cacheRequirement(self, base, basecmd, extcmds): """Return the cache requirements for the remote repos. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: a list of arguments passed to *basecmd* :return: Type of requirement: read-only:past, read-only:present, read-only:future, write """ cmd = 'install' if len(extcmds) > 1: cmd = extcmds[1] if cmd in ('info', 'list'): return InfoCommand().cacheRequirement(base, cmd, extcmds[2:]) if cmd in ('check-update', 'check-upgrade', 'check-updates', 'check-upgrades'): return CheckUpdateCommand().cacheRequirement(base, cmd, extcmds[2:]) return 'write' # Using this a lot, so make it easier... _upi = updateinfo class UpdateinfoCommand(YumCommand): # Old command names... direct_cmds = {'list-updateinfo' : 'list', 'list-security' : 'list', 'list-sec' : 'list', 'info-updateinfo' : 'info', 'info-security' : 'info', 'info-sec' : 'info', 'summary-updateinfo' : 'summary'} # Note that this code (instead of using inheritance and multiple # cmd classes) means that "yum help" only displays the updateinfo command. # Which is what we want, because the other commands are just backwards # compatible gunk we don't want the user using). def getNames(self): return ['updateinfo'] + sorted(self.direct_cmds.keys()) def getUsage(self): return "[info|list|...] [security|...] [installed|available|all] [pkgs|id]" def getSummary(self): return "Acts on repository update information" def doCheck(self, base, basecmd, extcmds): pass def list_show_pkgs(self, base, md_info, list_type, show_type, iname2tup, data, msg): n_maxsize = 0 r_maxsize = 0 t_maxsize = 0 for (notice, pkgtup, pkg) in data: n_maxsize = max(len(notice['update_id']), n_maxsize) tn = notice['type'] if tn == 'security' and notice['severity']: tn = notice['severity'] + '/Sec.' t_maxsize = max(len(tn), t_maxsize) if show_type: for ref in _upi._ysp_safe_refs(notice['references']): if ref['type'] != show_type: continue r_maxsize = max(len(str(ref['id'])), r_maxsize) for (notice, pkgtup, pkg) in data: mark = '' if list_type == 'all': mark = ' ' if pkgtup[0] in iname2tup and _upi._rpm_tup_vercmp(iname2tup[pkgtup[0]], pkgtup) >= 0: mark = 'i ' tn = notice['type'] if tn == 'security' and notice['severity']: tn = notice['severity'] + '/Sec.' if show_type and _upi._ysp_has_info_md(show_type, notice): for ref in _upi._ysp_safe_refs(notice['references']): if ref['type'] != show_type: continue msg("%s %-*s %-*s %s" % (mark, r_maxsize, str(ref['id']), t_maxsize, tn, pkg)) elif hasattr(pkg, 'name'): print base.fmtKeyValFill("%s: " % pkg.name, base._enc(pkg.summary)) else: msg("%s%-*s %-*s %s" % (mark, n_maxsize, notice['update_id'], t_maxsize, tn, pkg)) def info_show_pkgs(self, base, md_info, list_type, show_type, iname2tup, data, msg): show_pkg_info_done = {} for (notice, pkgtup, pkg) in data: if notice['update_id'] in show_pkg_info_done: continue show_pkg_info_done[notice['update_id']] = notice if hasattr(notice, 'text'): debug_log_lvl = yum.logginglevels.DEBUG_3 vlog = base.verbose_logger if vlog.isEnabledFor(debug_log_lvl): obj = notice.text(skip_data=[]) else: obj = notice.text() else: # Python-2.4.* doesn't understand str(x) returning unicode obj = notice.__str__() if list_type == 'all': if pkgtup[0] in iname2tup and _upi._rpm_tup_vercmp(iname2tup[pkgtup[0]], pkgtup) >= 0: obj = obj + "\n Installed : true" else: obj = obj + "\n Installed : false" msg(obj) def summary_show_pkgs(self, base, md_info, list_type, show_type, iname2tup, data, msg): def _msg(x): base.verbose_logger.info("%s", x) counts = {} sev_counts = {} show_pkg_info_done = {} for (notice, pkgtup, pkg) in data: if notice['update_id'] in show_pkg_info_done: continue show_pkg_info_done[notice['update_id']] = notice counts[notice['type']] = counts.get(notice['type'], 0) + 1 if notice['type'] == 'security': sev = notice['severity'] if sev is None: sev = '' sev_counts[sev] = sev_counts.get(sev, 0) + 1 maxsize = 0 for T in ('newpackage', 'security', 'bugfix', 'enhancement'): if T not in counts: continue size = len(str(counts[T])) if maxsize < size: maxsize = size if not maxsize: if base.conf.autocheck_running_kernel: _upi._check_running_kernel(base, md_info, _msg) return outT = {'newpackage' : 'New Package', 'security' : 'Security', 'bugfix' : 'Bugfix', 'enhancement' : 'Enhancement'} print "Updates Information Summary:", list_type for T in ('newpackage', 'security', 'bugfix', 'enhancement'): if T not in counts: continue n = outT[T] if T == 'security' and len(sev_counts) == 1: sn = sev_counts.keys()[0] if sn != '': n = sn + " " + n print " %*u %s notice(s)" % (maxsize, counts[T], n) if T == 'security' and len(sev_counts) != 1: def _sev_sort_key(key): # We want these in order, from "highest" to "lowest". # Anything unknown is "higher". meh. return {'Critical' : "zz1", 'Important': "zz2", 'Moderate' : "zz3", 'Low' : "zz4", }.get(key, key) for sn in sorted(sev_counts, key=_sev_sort_key): args = (maxsize, sev_counts[sn],sn or '?', outT['security']) print " %*u %s %s notice(s)" % args if base.conf.autocheck_running_kernel: _upi._check_running_kernel(base, md_info, _msg) self.show_pkg_info_done = {} def _get_new_pkgs(self, md_info): for notice in md_info.notices: if notice['type'] != "newpackage": continue for upkg in notice['pkglist']: for pkg in upkg['packages']: pkgtup = (pkg['name'], pkg['arch'], pkg['epoch'] or '0', pkg['version'], pkg['release']) yield (notice, pkgtup) _cmd2filt = {"bugzillas" : "bugzilla", "bugzilla" : "bugzilla", "bzs" : "bugzilla", "bz" : "bugzilla", "sec" : "security", "cves" : "cve", "cve" : "cve", "newpackages" : "newpackage", "new-packages" : "newpackage", "newpackage" : "newpackage", "new-package" : "newpackage", "new" : "newpackage"} for filt_type in _upi._update_info_types_: _cmd2filt[filt_type] = filt_type def doCommand(self, base, basecmd, extcmds): if basecmd in self.direct_cmds: subcommand = self.direct_cmds[basecmd] elif extcmds and extcmds[0] in ('list', 'info', 'summary', 'remove-pkgs-ts', 'exclude-updates', 'exclude-all', 'check-running-kernel'): subcommand = extcmds[0] extcmds = extcmds[1:] elif extcmds and extcmds[0] in self._cmd2filt: subcommand = 'list' elif extcmds: subcommand = 'info' else: subcommand = 'summary' if subcommand == 'list': return self.doCommand_li(base, 'updateinfo list', extcmds, self.list_show_pkgs) if subcommand == 'info': return self.doCommand_li(base, 'updateinfo info', extcmds, self.info_show_pkgs) if subcommand == 'summary': return self.doCommand_li(base, 'updateinfo summary', extcmds, self.summary_show_pkgs) if subcommand == 'remove-pkgs-ts': filters = None if extcmds: filters = updateinfo._args2filters(extcmds) updateinfo.remove_txmbrs(base, filters) return 0, [basecmd + ' ' + subcommand + ' done'] if subcommand == 'exclude-all': filters = None if extcmds: filters = updateinfo._args2filters(extcmds) updateinfo.exclude_all(base, filters) return 0, [basecmd + ' ' + subcommand + ' done'] if subcommand == 'exclude-updates': filters = None if extcmds: filters = updateinfo._args2filters(extcmds) updateinfo.exclude_updates(base, filters) return 0, [basecmd + ' ' + subcommand + ' done'] if subcommand == 'check-running-kernel': def _msg(x): base.verbose_logger.info("%s", x) updateinfo._check_running_kernel(base, base.upinfo, _msg) return 0, [basecmd + ' ' + subcommand + ' done'] def doCommand_li_new(self, base, list_type, extcmds, md_info, msg, show_pkgs, iname2tup): done_pkgs = set() data = [] for (notice, pkgtup) in sorted(self._get_new_pkgs(md_info), key=lambda x: x[1][0]): if extcmds and not _upi._match_sec_cmd(extcmds, pkgtup[0], notice): continue n = pkgtup[0] if n in done_pkgs: continue ipkgs = list(reversed(sorted(base.rpmdb.searchNames([n])))) if list_type in ('installed', 'updates') and not ipkgs: done_pkgs.add(n) continue if list_type == 'available' and ipkgs: done_pkgs.add(n) continue pkgs = base.pkgSack.searchPkgTuple(pkgtup) if not pkgs: continue if list_type == "updates" and pkgs[0].verLE(ipkgs[0]): done_pkgs.add(n) continue done_pkgs.add(n) data.append((notice, pkgtup, pkgs[0])) show_pkgs(base, md_info, list_type, None, iname2tup, data, msg) def _parse_extcmds(self, extcmds): filt_type = None show_type = None if len(extcmds) >= 1: filt_type = None if extcmds[0] in self._cmd2filt: filt_type = self._cmd2filt[extcmds.pop(0)] show_type = filt_type if filt_type and filt_type in _upi._update_info_types_: show_type = None return extcmds, show_type, filt_type def doCommand_li(self, base, basecmd, extcmds, show_pkgs): md_info = base.upinfo def msg(x): # Don't use: logger.log(logginglevels.INFO_2, x) # or -q deletes everything. print x extcmds, show_type, filt_type = self._parse_extcmds(extcmds) list_type = "available" list_type = "updates" if extcmds and extcmds[0] in ("updates","available","installed", "all"): list_type = extcmds.pop(0) if filt_type is None: extcmds, show_type, filt_type = self._parse_extcmds(extcmds) opts = _upi._ysp_gen_opts(base.updateinfo_filters, extcmds) used_map = _upi._ysp_gen_used_map(base.updateinfo_filters) iname2tup = {} if False: pass elif list_type == 'installed': name2tup = _upi._get_name2allpkgtup(base) iname2tup = _upi._get_name2instpkgtup(base) elif list_type == 'updates': name2tup = _upi._get_name2oldpkgtup(base) elif list_type in ('available', 'all'): name2tup = _upi._get_name2aallpkgtup(base) iname2tup = _upi._get_name2instpkgtup(base) if filt_type == "newpackage": self.doCommand_li_new(base, list_type, extcmds, md_info, msg, show_pkgs, iname2tup) return 0, [basecmd + ' new done'] def _show_pkgtup(pkgtup): name = pkgtup[0] notices = reversed(md_info.get_applicable_notices(pkgtup)) for (pkgtup, notice) in notices: if filt_type and not _upi._ysp_has_info_md(filt_type, notice): continue if list_type == 'installed': # Remove any that are newer than what we have installed if _upi._rpm_tup_vercmp(iname2tup[name], pkgtup) < 0: continue if list_type == 'available': # Remove any that are installed if name in iname2tup and _upi._rpm_tup_vercmp(iname2tup[name], pkgtup) >= 0: continue if _upi._ysp_should_filter_pkg(opts, name, notice, used_map): yield (pkgtup, notice) data = [] for pkgname in sorted(name2tup): for (pkgtup, notice) in _show_pkgtup(name2tup[pkgname]): d = {} (d['n'], d['a'], d['e'], d['v'], d['r']) = pkgtup if d['e'] == '0': d['epoch'] = '' else: d['epoch'] = "%s:" % d['e'] data.append((notice, pkgtup, "%(n)s-%(epoch)s%(v)s-%(r)s.%(a)s" % d)) show_pkgs(base, md_info, list_type, show_type, iname2tup, data, msg) _upi._ysp_chk_used_map(used_map, msg) return 0, [basecmd + ' done'] class UpdateMinimalCommand(YumCommand): def getNames(self): return ['update-minimal', 'upgrade-minimal'] def getUsage(self): return "[PACKAGE-wildcard]" def getSummary(self): return _("Works like upgrade, but goes to the 'newest' package match which fixes a problem that affects your system") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) checkGPGKey(base) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ num = len(base.tsInfo) _upi.update_minimal(base, extcmds) num = len(base.tsInfo) - num if num > 0: msg = '%d packages marked for minimal Update' % num return 2, [msg] else: return 0, ['No Packages marked for minimal Update'] class FSSnapshotCommand(YumCommand): def getNames(self): return ['fssnapshot', 'fssnap'] def getUsage(self): return "[]" def getSummary(self): return _("Creates filesystem snapshots, or lists/deletes current snapshots.") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ checkRootUID(base) @staticmethod def _li_snaps(base, snaps): snaps = sorted(snaps, key=lambda x: x['dev']) max_dev = utf8_width(_('Snapshot')) max_ori = utf8_width(_('Origin')) for data in snaps: max_dev = max(max_dev, len(data['dev'])) max_ori = max(max_ori, len(data['origin'])) done = False for data in snaps: if not done: print ("%s %s %s %s %s %s" % (utf8_width_fill(_('Snapshot'), max_dev), utf8_width_fill(_('Size'), 6, left=False), utf8_width_fill(_('Used'), 6, left=False), utf8_width_fill(_('Free'), 6, left=False), utf8_width_fill(_('Origin'), max_ori), _('Tags'))) done = True print ("%*s %6s %5.1f%% %6s %*s %s" % (max_dev, data['dev'], base.format_number(data['size']), data['used'], base.format_number(data['free']), max_ori, data['origin'], ",".join(data['tags']))) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ if extcmds and extcmds[0] in ('list', 'delete', 'create', 'summary', 'have-space', 'has-space'): subcommand = extcmds[0] extcmds = extcmds[1:] else: subcommand = 'summary' if not base.fssnap.available: msg = _("Snapshot support not available, please check your lvm installation.") if not base.rpmdb.searchNames(['lvm2']): msg += " " + _("No lvm2 package installed.") if not base.rpmdb.searchNames(['lvm2-python-libs']): msg += " " + _("No lvm2-python-libs package installed.") print msg return 1, [basecmd + ' ' + subcommand + ' done'] if subcommand == 'list': try: snaps = base.fssnap.old_snapshots() except LibLVMError as e: return 1, [_("Failed to list snapshots: ") + lvmerr2str(e)] print _("List of %u snapshosts:") % len(snaps) self._li_snaps(base, snaps) if subcommand == 'delete': msg = _("Failed to delete snapshots: ") try: snaps = base.fssnap.old_snapshots() except LibLVMError as e: return 1, [msg + lvmerr2str(e)] devs = [x['dev'] for x in snaps] snaps = set() for dev in devs: if dev in snaps: continue for extcmd in extcmds: if dev == extcmd or fnmatch.fnmatch(dev, extcmd): snaps.add(dev) break try: snaps = base.fssnap.del_snapshots(devices=snaps) except LibLVMError as e: return 1, [msg + lvmerr2str(e)] print _("Deleted %u snapshosts:") % len(snaps) self._li_snaps(base, snaps) if subcommand in ('have-space', 'has-space'): pc = base.conf.fssnap_percentage try: has_space = base.fssnap.has_space(pc) except LibLVMError as e: return 1, [_("Could not determine free space on logical volumes: ") + lvmerr2str(e)] if has_space: print _("Space available to take a snapshot.") else: print _("Not enough space available on logical volumes to take a snapshot.") if subcommand == 'create': tags = {'*': ['reason=manual']} pc = base.conf.fssnap_percentage msg = _("Failed to create snapshots") try: snaps = base.fssnap.snapshot(pc, tags=tags) except LibLVMError as e: msg += ": " + lvmerr2str(e) snaps = [] if not snaps: print msg for (odev, ndev) in snaps: print _("Created snapshot from %s, results is: %s") %(odev,ndev) if subcommand == 'summary': try: snaps = base.fssnap.old_snapshots() except LibLVMError as e: return 1, [_("Failed to list snapshots: ") + lvmerr2str(e)] if not snaps: print _("No snapshots, LVM version:"), base.fssnap.version return 0, [basecmd + ' ' + subcommand + ' done'] used = 0 dev_oris = set() for snap in snaps: used += snap['used'] dev_oris.add(snap['origin_dev']) msg = _("Have %u snapshots, using %s space, from %u origins.") print msg % (len(snaps), base.format_number(used), len(dev_oris)) return 0, [basecmd + ' ' + subcommand + ' done'] class FSCommand(YumCommand): def getNames(self): return ['fs'] def getUsage(self): return "[]" def getSummary(self): return _("Acts on the filesystem data of the host, mainly for removing docs/lanuages for minimal hosts.") def doCheck(self, base, basecmd, extcmds): """Verify that conditions are met so that this command can run. These include that the program is being run by the root user, that there are enabled repositories with gpg keys, and that this command is called with appropriate arguments. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* """ if extcmds and extcmds[0] in ('du', 'status', 'diff'): # Anyone can go for it... return if len(extcmds) == 1 and extcmds[0] in ('filters', 'filter'): # Can look, but not touch. return checkRootUID(base) def _fs_pkg_walk(self, pkgs, prefix, modified=False, verbose=False): pfr = {'norm' : {}, 'mod' : {}, 'ghost' : {}, 'miss' : {}, 'not' : {} } def quick_match(pkgs): for pkg in pkgs: for fname in pkg.filelist + pkg.dirlist: if not fname.startswith(prefix): continue pfr['norm'][fname] = pkg for fname in pkg.ghostlist: if not fname.startswith(prefix): continue pfr['ghost'][fname] = pkg return pfr def _quick_match_iter(pkgs): # Walking the fi information is much slower than filelist/dirlist for pkg in pkgs: found = False for fname in pkg.dirlist: if fname.startswith(prefix): yield pkg found = True break if found: continue for fname in pkg.filelist: if fname.startswith(prefix): yield pkg found = True break if found: continue for fname in pkg.ghostlist: if fname.startswith(prefix): yield pkg break def verify_match(pkgs): _pfs = [] def scoop_pfs(pfs): _pfs.append(pfs) if not modified: return [] return pfs if prefix != '/': pkgs = _quick_match_iter(pkgs) for pkg in pkgs: _pfs = [] probs = pkg.verify(patterns=[prefix+'*'], fake_problems=False, callback=scoop_pfs, failfast=True) for pf in _pfs[0]: if pf.filename in probs: pfr['mod'][pf.filename] = pkg elif pf.rpmfile_state == 'not installed': pfr['not'][pf.filename] = pkg elif 'ghost' in pf.rpmfile_types: pfr['ghost'][pf.filename] = pkg elif 'missing ok' in pf.rpmfile_types: pfr['miss'][pf.filename] = pkg else: pfr['norm'][pf.filename] = pkg return pfr # return quick_match(pkgs) return verify_match(pkgs) def _fs_du(self, base, extcmds): def _dir_prefixes(path): while path != '/': path = os.path.dirname(path) yield path def loc_num(x): """ String of a number in the readable "locale" format. """ return locale.format("%d", int(x), True) data = {'pkgs_size' : {}, 'pkgs_not_size' : {}, 'pkgs_ghost_size' : {}, 'pkgs_miss_size' : {}, 'pkgs_mod_size' : {}, 'pres_size' : {}, 'data_size' : {}, 'data_not_size' : {}, 'pkgs_count' : 0, 'pkgs_not_count' : 0, 'pkgs_ghost_count' : 0, 'pkgs_miss_count' : 0, 'pkgs_mod_count' : 0, 'data_count' : 0} # data_not_count == pkgs_not_count def _add_size(d, v, size): if v not in d: d[v] = 0 d[v] += size def deal_with_file(fpath, need_prefix=True): size = os.path.getsize(fpath) if fpath in pfr['norm']: data['pkgs_count'] += size _add_size(data['pkgs_size'], pfr['norm'][fpath], size) elif fpath in pfr['ghost']: data['pkgs_ghost_count'] += size _add_size(data['pkgs_ghost_size'], pfr['ghost'][fpath], size) elif fpath in pfr['not']: data['pkgs_not_count'] += size _add_size(data['pkgs_not_size'], pfr['not'][fpath], size) data['data_not_size'][fpath] = size elif fpath in pfr['miss']: data['pkgs_miss_count'] += size _add_size(data['pkgs_miss_size'], pfr['miss'][fpath], size) elif fpath in pfr['mod']: data['pkgs_mod_count'] += size _add_size(data['pkgs_mod_size'], pfr['mod'][fpath], size) elif need_prefix and False: for fpre_path in _dir_prefixes(fpath): if fpre_path not in pkg_files: continue _add_size(data['pres_size'], pkg_files[fpre_path], size) break data['data_count'] += size data['data_size'][fpath] = size else: data['data_count'] += size data['data_size'][fpath] = size prefix = "." if extcmds: prefix = extcmds[0] extcmds = extcmds[1:] if not os.path.exists(prefix): return 1, [_('No such file or directory: ' + prefix)] max_show_len = 4 if extcmds: try: max_show_len = int(extcmds[0]) except: pass verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) pfr = self._fs_pkg_walk(base.rpmdb, prefix, verbose=verbose) base.closeRpmDB() # C-c ftw. num = 0 if os.path.isfile(prefix): num += 1 deal_with_file(prefix) for root, dirs, files in os.walk(prefix): for fname in files: num += 1 fpath = os.path.normpath(root + '/' + fname) if os.path.islink(fpath): continue deal_with_file(fpath, need_prefix=verbose) # output print "Files :", loc_num(num) tot = 0 tot += data['pkgs_count'] tot += data['pkgs_ghost_count'] tot += data['pkgs_not_count'] tot += data['pkgs_miss_count'] tot += data['pkgs_mod_count'] tot += data['data_count'] print "Total size :", base.format_number(tot) if not tot: return num = data['pkgs_count'] if not verbose: num += data['pkgs_ghost_count'] num += data['pkgs_miss_count'] num += data['pkgs_mod_count'] print " Pkgs size :", "%-5s" % base.format_number(num), print "(%3.0f%%)" % ((num * 100.0) / tot) if verbose: for (title, num) in ((_(" Ghost pkgs size :"), data['pkgs_ghost_count']), (_(" Not pkgs size :"), data['pkgs_not_count']), (_(" Miss pkgs size :"), data['pkgs_miss_count']), (_(" Mod. pkgs size :"), data['pkgs_mod_count'])): if not num: continue print title, "%-5s" % base.format_number(num), print "(%3.0f%%)" % ((num * 100.0) / tot) num = data['data_count'] if not verbose: num += data['pkgs_not_count'] print _(" Data size :"), "%-5s" % base.format_number(num), print "(%3.0f%%)" % ((num * 100.0) / tot) if verbose: print '' print _("Pkgs :"), loc_num(len(data['pkgs_size'])) print _("Ghost Pkgs :"), loc_num(len(data['pkgs_ghost_size'])) print _("Not Pkgs :"), loc_num(len(data['pkgs_not_size'])) print _("Miss. Pkgs :"), loc_num(len(data['pkgs_miss_size'])) print _("Mod. Pkgs :"), loc_num(len(data['pkgs_mod_size'])) def _pkgs(p_size, msg): tot = min(max_show_len, len(p_size)) if tot: print '' print msg % tot num = 0 for pkg in sorted(p_size, key=lambda x: p_size[x], reverse=True): num += 1 print _("%*d. %60s %-5s") % (len(str(tot)), num, pkg, base.format_number(p_size[pkg])) if num >= tot: break if verbose: _pkgs(data['pkgs_size'], _('Top %d packages:')) _pkgs(data['pkgs_ghost_size'], _('Top %d ghost packages:')) _pkgs(data['pkgs_not_size'], _('Top %d not. packages:')) _pkgs(data['pkgs_miss_size'], _('Top %d miss packages:')) _pkgs(data['pkgs_mod_size'], _('Top %d mod. packages:')) _pkgs(data['pres_size'], _('Top %d prefix packages:')) else: tmp = {} tmp.update(data['pkgs_size']) for d in data['pkgs_ghost_size']: _add_size(tmp, d, data['pkgs_ghost_size'][d]) for d in data['pkgs_miss_size']: _add_size(tmp, d, data['pkgs_miss_size'][d]) for d in data['pkgs_mod_size']: _add_size(tmp, d, data['pkgs_mod_size'][d]) _pkgs(tmp, _('Top %d packages:')) print '' if verbose: data_size = data['data_size'] else: data_size = {} data_size.update(data['data_size']) data_size.update(data['data_not_size']) tot = min(max_show_len, len(data_size)) if tot: print _('Top %d non-package files:') % tot num = 0 for fname in sorted(data_size, key=lambda x: data_size[x], reverse=True): num += 1 dsznum = data_size[fname] print _("%*d. %60s %-5s") % (len(str(tot)), num, fname, base.format_number(dsznum)) if num >= tot: break def _fs_filters(self, base, extcmds): def _save(confkey): writeRawConfigFile = yum.config._writeRawConfigFile # Always create installroot, so we can change it. if not os.path.exists(base.conf.installroot + '/etc/yum'): os.makedirs(base.conf.installroot + '/etc/yum') fn = base.conf.installroot+'/etc/yum/yum.conf' if not os.path.exists(fn): # Try the old default nfn = base.conf.installroot+'/etc/yum.conf' if os.path.exists(nfn): fn = nfn else: shutil.copy2(base.conf.config_file_path, fn) ybc = base.conf writeRawConfigFile(fn, 'main', ybc.yumvar, ybc.cfg.options, ybc.iteritems, ybc.optionobj, only=[confkey]) if not extcmds: oil = base.conf.override_install_langs if not oil: oil = "rpm: " + rpm.expandMacro("%_install_langs") print _("File system filters:") print _(" Nodocs:"), 'nodocs' in base.conf.tsflags print _(" Languages:"), oil elif extcmds[0] in ('docs', 'nodocs', 'documentation', 'nodocumentation'): c_f = 'nodocs' in base.conf.tsflags n_f = not extcmds[0].startswith('no') if n_f == c_f: if n_f: print _("Already enabled documentation filter.") else: print _("Already disabled documentation filter.") return if n_f: print _("Enabling documentation filter.") else: print _("Disabling documentation filter.") nts = base.conf.tsflags if n_f: nts = nts + ['nodocs'] else: nts = [x for x in nts if x != 'nodocs'] base.conf.tsflags = " ".join(nts) _save('tsflags') elif extcmds[0] in ('langs', 'nolangs', 'lang', 'nolang', 'languages', 'nolanguages', 'language', 'nolanguage'): if extcmds[0].startswith('no') or len(extcmds) < 2 or 'all' in extcmds: val = 'all' else: val = ":".join(extcmds[1:]) if val == base.conf.override_install_langs: if val: print _("Already filtering languages to: %s") % val else: print _("Already disabled language filter.") return if val: print _("Setting language filter to: %s") % val else: print _("Disabling language filter.") base.conf.override_install_langs = val _save('override_install_langs') else: return 1, [_('Not a valid sub-command of fs filter')] def _fs_refilter(self, base, extcmds): c_f = 'nodocs' in base.conf.tsflags # FIXME: C&P from init. oil = base.conf.override_install_langs if not oil: oil = rpm.expandMacro("%_install_langs") if oil == 'all': oil = '' elif oil: oil = ":".join(sorted(oil.split(':'))) found = False num = 0 for pkg in base.rpmdb.returnPackages(patterns=extcmds): if False: pass elif oil != pkg.yumdb_info.get('ts_install_langs', ''): txmbrs = base.reinstall(po=pkg) num += len(txmbrs) elif c_f != ('true' == pkg.yumdb_info.get('tsflag_nodocs')): txmbrs = base.reinstall(po=pkg) num += len(txmbrs) else: found = True if num: return 2,P_('%d package to reinstall','%d packages to reinstall', num) if not found: return 1, [_('No valid packages: %s') % " ".join(extcmds)] def _fs_refilter_cleanup(self, base, extcmds): pkgs = base.rpmdb.returnPackages(patterns=extcmds) verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) pfr = self._fs_pkg_walk(pkgs, "/", verbose=verbose, modified=True) base.closeRpmDB() # C-c ftw. for fname in sorted(pfr['not']): print _('Removing:'), fname try: # Ignore everything, unlink_f() doesn't. os.unlink(fname) except OSError, e: if e.errno == errno.EISDIR: try: os.rmdir(fname) except: pass except: pass def _fs_diff(self, base, extcmds): def deal_with_file(fpath): if fpath in pfr['norm']: pass elif fpath in pfr['ghost']: pass elif fpath in pfr['not']: print >>sys.stderr, _('Not installed:'), fpath elif fpath in pfr['miss']: pass elif fpath in pfr['mod']: pkg = apkgs[pfr['mod'][fpath].pkgtup] # Hacky ... but works. sys.stdout.flush() extract_cmd = "cd %s; rpm2cpio %s | cpio --quiet -id .%s" extract_cmd = extract_cmd % (tmpdir, pkg.localPkg(), fpath) os.system(extract_cmd) diff_cmd = "diff -ru %s %s" % (tmpdir + fpath, fpath) print diff_cmd sys.stdout.flush() os.system(diff_cmd) else: print >>sys.stderr, _('Not packaged?:'), fpath if not distutils.spawn.find_executable("diff"): raise yum.Errors.YumBaseError, _("Can't find diff command") # These just shouldn't happen... if not distutils.spawn.find_executable("cpio"): raise yum.Errors.YumBaseError, _("Can't find cpio command") if not distutils.spawn.find_executable("rpm2cpio"): raise yum.Errors.YumBaseError, _("Can't find rpm2cpio command") prefix = "." if extcmds: prefix = extcmds[0] extcmds = extcmds[1:] pkgs = base.rpmdb.returnPackages(patterns=extcmds) verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) pfr = self._fs_pkg_walk(pkgs, prefix, verbose=verbose, modified=True) base.closeRpmDB() # C-c ftw. apkgs = {} downloadpkgs = [] for ipkg in set(pfr['mod'].values()): iyi = ipkg.yumdb_info if 'from_repo' in iyi: # Updates-testing etc. if iyi.from_repo in base.repos.repos: repo = base.repos.getRepo(iyi.from_repo) if not repo.isEnabled(): base.repos.enableRepo(repo.id) for apkg in base.pkgSack.searchPkgTuple(ipkg.pkgtup): if ('checksum_type' in iyi and 'checksum_data' in iyi and iyi.checksum_type == apkg.checksum_type and iyi.checksum_data == apkg.pkgId): apkgs[ipkg.pkgtup] = apkg downloadpkgs.append(apkg) break if ipkg.pkgtup not in apkgs: raise yum.Errors.YumBaseError, _("Can't find package: %s") %ipkg if downloadpkgs: tmpdir = tempfile.mkdtemp() problems = base.downloadPkgs(downloadpkgs, callback_total=base.download_callback_total_cb) if len(problems) > 0: errstring = '' errstring += _('Error downloading packages:\n') for key in problems: errors = yum.misc.unique(problems[key]) for error in errors: errstring += ' %s: %s\n' % (key, error) raise yum.Errors.YumBaseError, errstring for root, dirs, files in os.walk(prefix): for fname in files: fpath = os.path.normpath(root + '/' + fname) if os.path.islink(fpath): continue deal_with_file(fpath) if downloadpkgs: shutil.rmtree(tmpdir) def _fs_status(self, base, extcmds): def deal_with_file(fpath): if fpath in pfr['norm']: pass elif fpath in pfr['ghost']: pass elif fpath in pfr['not']: print _('Not installed:'), fpath elif fpath in pfr['miss']: pass elif fpath in pfr['mod']: print _('Modified:'), fpath else: print _('Not packaged?:'), fpath prefix = "." if extcmds: prefix = extcmds[0] extcmds = extcmds[1:] pkgs = base.rpmdb.returnPackages(patterns=extcmds) verbose = base.verbose_logger.isEnabledFor(logginglevels.DEBUG_3) pfr = self._fs_pkg_walk(pkgs, prefix, verbose=verbose, modified=True) base.closeRpmDB() # C-c ftw. for root, dirs, files in os.walk(prefix): for fname in files: fpath = os.path.normpath(root + '/' + fname) if os.path.islink(fpath): continue deal_with_file(fpath) def doCommand(self, base, basecmd, extcmds): """Execute this command. :param base: a :class:`yum.Yumbase` object :param basecmd: the name of the command :param extcmds: the command line arguments passed to *basecmd* :return: (exit_code, [ errors ]) exit_code is:: 0 = we're done, exit 1 = we've errored, exit with error string 2 = we've got work yet to do, onto the next stage """ if extcmds and extcmds[0] in ('filters', 'filter', 'refilter', 'refilter-cleanup', 'du', 'status', 'diff', 'snap'): subcommand = extcmds[0] extcmds = extcmds[1:] else: subcommand = 'filters' if False: pass elif subcommand == 'du': ret = self._fs_du(base, extcmds) elif subcommand in ('filter', 'filters'): ret = self._fs_filters(base, extcmds) elif subcommand == 'refilter': ret = self._fs_refilter(base, extcmds) elif subcommand == 'refilter-cleanup': ret = self._fs_refilter_cleanup(base, extcmds) elif subcommand == 'diff': ret = self._fs_diff(base, extcmds) elif subcommand == 'status': ret = self._fs_status(base, extcmds) elif subcommand == 'snap': ret = FSSnapshotCommand().doCommand(base, 'fs snap', args) else: return 1, [_('Not a valid sub-command of %s') % basecmd] if ret is not None: return ret return 0, [basecmd + ' ' + subcommand + ' done']