PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /usr/lib/python3.6/site-packages/dnf-plugins
Viewing File: /usr/lib/python3.6/site-packages/dnf-plugins/copr.py
# supplies the 'copr' command. # # Copyright (C) 2014-2015 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 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., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # from __future__ import print_function import glob import itertools import json import os import re import shutil import stat import sys import base64 import json from dnfpluginscore import _, logger import dnf from dnf.pycomp import PY3 from dnf.i18n import ucd import rpm # Attempt importing the linux_distribution function from distro # If that fails, attempt to import the deprecated implementation # from the platform module. try: from distro import name, version, codename, os_release_attr # Re-implement distro.linux_distribution() to avoid a deprecation warning def linux_distribution(): return (name(), version(), codename()) except ImportError: def os_release_attr(_): return "" try: from platform import linux_distribution except ImportError: # Simple fallback for distributions that lack an implementation def linux_distribution(): with open('/etc/os-release') as os_release_file: os_release_data = {} for line in os_release_file: try: os_release_key, os_release_value = line.rstrip().split('=') os_release_data[os_release_key] = os_release_value.strip('"') except ValueError: # Skip empty lines and everything that is not a simple # variable assignment pass return (os_release_data['NAME'], os_release_data['VERSION_ID'], None) PLUGIN_CONF = 'copr' YES = set([_('yes'), _('y')]) NO = set([_('no'), _('n'), '']) if PY3: from configparser import ConfigParser, NoOptionError, NoSectionError from urllib.request import urlopen, HTTPError, URLError else: from ConfigParser import ConfigParser, NoOptionError, NoSectionError from urllib2 import urlopen, HTTPError, URLError @dnf.plugin.register_command class CoprCommand(dnf.cli.Command): """ Copr plugin for DNF """ chroot_config = None default_hostname = "copr.fedorainfracloud.org" default_hub = "fedora" default_protocol = "https" default_port = 443 default_url = default_protocol + "://" + default_hostname aliases = ("copr",) summary = _("Interact with Copr repositories.") first_warning = True usage = _(""" enable name/project [chroot] disable name/project remove name/project list --installed/enabled/disabled list --available-by-user=NAME search project Examples: copr enable rhscl/perl516 epel-6-x86_64 copr enable ignatenkobrain/ocltoys copr disable rhscl/perl516 copr remove rhscl/perl516 copr list --enabled copr list --available-by-user=ignatenkobrain copr search tests """) @staticmethod def set_argparser(parser): parser.add_argument('subcommand', nargs=1, choices=['help', 'enable', 'disable', 'remove', 'list', 'search']) list_option = parser.add_mutually_exclusive_group() list_option.add_argument('--installed', action='store_true', help=_('List all installed Copr repositories (default)')) list_option.add_argument('--enabled', action='store_true', help=_('List enabled Copr repositories')) list_option.add_argument('--disabled', action='store_true', help=_('List disabled Copr repositories')) list_option.add_argument('--available-by-user', metavar='NAME', help=_('List available Copr repositories by user NAME')) parser.add_argument('--hub', help=_('Specify an instance of Copr to work with')) parser.add_argument('arg', nargs='*') def configure(self): if self.cli.command.opts.command != "copr": return copr_hub = None copr_plugin_config = ConfigParser() config_files = [] config_path = self.base.conf.pluginconfpath[0] default_config_file = os.path.join(config_path, PLUGIN_CONF + ".conf") if os.path.isfile(default_config_file): config_files.append(default_config_file) copr_plugin_config.read(default_config_file) if copr_plugin_config.has_option('main', 'distribution') and\ copr_plugin_config.has_option('main', 'releasever'): distribution = copr_plugin_config.get('main', 'distribution') releasever = copr_plugin_config.get('main', 'releasever') self.chroot_config = [distribution, releasever] else: self.chroot_config = [False, False] for filename in os.listdir(os.path.join(config_path, PLUGIN_CONF + ".d")): if filename.endswith('.conf'): config_file = os.path.join(config_path, PLUGIN_CONF + ".d", filename) config_files.append(config_file) project = [] if len(self.opts.arg): project = self.opts.arg[0].split("/") if len(project) == 3 and self.opts.hub: logger.critical( _('Error: ') + _('specify Copr hub either with `--hub` or using ' '`copr_hub/copr_username/copr_projectname` format') ) raise dnf.cli.CliError(_('multiple hubs specified')) # Copr hub was not specified, using default hub `fedora` elif not self.opts.hub and len(project) != 3: self.copr_hostname = self.default_hostname self.copr_url = self.default_url # Copr hub specified with hub/user/project format elif len(project) == 3: copr_hub = project[0] else: copr_hub = self.opts.hub # Try to find hub in a config file if config_files and copr_hub: self.copr_url = None copr_plugin_config.read(sorted(config_files, reverse=True)) hostname = self._read_config_item(copr_plugin_config, copr_hub, 'hostname', None) if hostname: protocol = self._read_config_item(copr_plugin_config, copr_hub, 'protocol', self.default_protocol) port = self._read_config_item(copr_plugin_config, copr_hub, 'port', self.default_port) self.copr_hostname = hostname self.copr_url = protocol + "://" + hostname if int(port) != self.default_port: self.copr_url += ":" + port self.copr_hostname += ":" + port if not self.copr_url: if '://' not in copr_hub: self.copr_hostname = copr_hub self.copr_url = self.default_protocol + "://" + copr_hub else: self.copr_hostname = copr_hub.split('://', 1)[1] self.copr_url = copr_hub def _read_config_item(self, config, hub, section, default): try: return config.get(hub, section) except (NoOptionError, NoSectionError): return default def _user_warning_before_prompt(self, text): sys.stderr.write("{0}\n".format(text.strip())) def run(self): subcommand = self.opts.subcommand[0] if subcommand == "help": self.cli.optparser.print_help(self) return 0 if subcommand == "list": if self.opts.available_by_user: self._list_user_projects(self.opts.available_by_user) return else: self._list_installed_repositories(self.base.conf.reposdir[0], self.opts.enabled, self.opts.disabled) return try: project_name = self.opts.arg[0] except (ValueError, IndexError): logger.critical( _('Error: ') + _('exactly two additional parameters to ' 'copr command are required')) self.cli.optparser.print_help(self) raise dnf.cli.CliError( _('exactly two additional parameters to ' 'copr command are required')) try: chroot = self.opts.arg[1] if len(self.opts.arg) > 2: raise dnf.exceptions.Error(_('Too many arguments.')) self.chroot_parts = chroot.split("-") if len(self.chroot_parts) < 3: raise dnf.exceptions.Error(_('Bad format of optional chroot. The format is ' 'distribution-version-architecture.')) except IndexError: chroot = self._guess_chroot() self.chroot_parts = chroot.split("-") # commands without defined copr_username/copr_projectname if subcommand == "search": self._search(project_name) return project = project_name.split("/") if len(project) not in [2, 3]: logger.critical( _('Error: ') + _('use format `copr_username/copr_projectname` ' 'to reference copr project')) raise dnf.cli.CliError(_('bad copr project format')) elif len(project) == 2: copr_username = project[0] copr_projectname = project[1] else: copr_username = project[1] copr_projectname = project[2] project_name = copr_username + "/" + copr_projectname repo_filename = "{0}/_copr:{1}:{2}:{3}.repo".format( self.base.conf.get_reposdir, self.copr_hostname, self._sanitize_username(copr_username), copr_projectname) if subcommand == "enable": self._need_root() info = _(""" Enabling a Copr repository. Please note that this repository is not part of the main distribution, and quality may vary. The Fedora Project does not exercise any power over the contents of this repository beyond the rules outlined in the Copr FAQ at <https://docs.pagure.org/copr.copr/user_documentation.html#what-i-can-build-in-copr>, and packages are not held to any quality or security level. Please do not file bug reports about these packages in Fedora Bugzilla. In case of problems, contact the owner of this repository. """) project = '/'.join([self.copr_hostname, copr_username, copr_projectname]) msg = "Do you really want to enable {0}?".format(project) self._ask_user(info, msg) self._download_repo(project_name, repo_filename) logger.info(_("Repository successfully enabled.")) self._runtime_deps_warning(copr_username, copr_projectname) elif subcommand == "disable": self._need_root() self._disable_repo(copr_username, copr_projectname) logger.info(_("Repository successfully disabled.")) elif subcommand == "remove": self._need_root() self._remove_repo(copr_username, copr_projectname) logger.info(_("Repository successfully removed.")) else: raise dnf.exceptions.Error( _('Unknown subcommand {}.').format(subcommand)) def _list_repo_file(self, repo_id, repo, enabled_only, disabled_only): file_name = repo.repofile.split('/')[-1] match_new = re.match("_copr:" + self.copr_hostname, file_name) match_old = self.copr_url == self.default_url and re.match("_copr_", file_name) match_any = re.match("_copr:|^_copr_", file_name) if self.opts.hub: if not match_new and not match_old: return elif not match_any: return if re.match('copr:.*:.*:.*:ml', repo_id): # We skip multilib repositories return if re.match('coprdep:.*', repo_id): # Runtime dependencies are not listed. return enabled = repo.enabled if (enabled and disabled_only) or (not enabled and enabled_only): return old_repo = False # repo ID has copr:<hostname>:<user>:<copr_dir> format, while <copr_dir> # can contain more colons if re.match("copr:", repo_id): _, copr_hostname, copr_owner, copr_dir = repo_id.split(':', 3) msg = copr_hostname + '/' + copr_owner + "/" + copr_dir # repo ID has <user>-<project> format, try to get hub from file name elif re.match("_copr:", file_name): copr_name = repo_id.split('-', 1) copr_hostname = file_name.rsplit(':', 2)[0].split(':', 1)[1] msg = copr_hostname + '/' + copr_name[0] + '/' + copr_name[1] # no information about hub, assume the default one else: copr_name = repo_id.split('-', 1) msg = self.default_hostname + '/' + copr_name[0] + '/' + copr_name[1] old_repo = True if not enabled: msg += " (disabled)" if old_repo: msg += " *" print(msg) return old_repo def _list_installed_repositories(self, directory, enabled_only, disabled_only): old_repo = False for repo_id, repo in self.base.repos.items(): if self._list_repo_file(repo_id, repo, enabled_only, disabled_only): old_repo = True if old_repo: print(_("* These coprs have repo file with an old format that contains " "no information about Copr hub - the default one was assumed. " "Re-enable the project to fix this.")) def _list_user_projects(self, user_name): # https://copr.fedorainfracloud.org/api_3/project/list?ownername=ignatenkobrain api_path = "/api_3/project/list?ownername={0}".format(user_name) url = self.copr_url + api_path res = self.base.urlopen(url, mode='w+') try: json_parse = json.loads(res.read()) except ValueError: raise dnf.exceptions.Error( _("Can't parse repositories for username '{}'.") .format(user_name)) self._check_json_output(json_parse) section_text = _("List of {} coprs").format(user_name) self._print_match_section(section_text) for item in json_parse["items"]: msg = "{0}/{1} : ".format(user_name, item["name"]) desc = item["description"] or _("No description given") msg = self.base.output.fmtKeyValFill(ucd(msg), desc) print(msg) def _search(self, query): # https://copr.fedorainfracloud.org/api_3/project/search?query=tests api_path = "/api_3/project/search?query={}".format(query) url = self.copr_url + api_path res = self.base.urlopen(url, mode='w+') try: json_parse = json.loads(res.read()) except ValueError: raise dnf.exceptions.Error(_("Can't parse search for '{}'." ).format(query)) self._check_json_output(json_parse) section_text = _("Matched: {}").format(query) self._print_match_section(section_text) for item in json_parse["items"]: msg = "{0} : ".format(item["full_name"]) desc = item["description"] or _("No description given.") msg = self.base.output.fmtKeyValFill(ucd(msg), desc) print(msg) def _print_match_section(self, text): formatted = self.base.output.fmtSection(text) print(formatted) def _ask_user_no_raise(self, info, msg): if not self.first_warning: sys.stderr.write("\n") self.first_warning = False sys.stderr.write("{0}\n".format(info.strip())) if self.base._promptWanted(): if self.base.conf.assumeno or not self.base.output.userconfirm( msg='\n{} [y/N]: '.format(msg), defaultyes_msg='\n{} [Y/n]: '.format(msg)): return False return True def _ask_user(self, info, msg): if not self._ask_user_no_raise(info, msg): raise dnf.exceptions.Error(_('Safe and good answer. Exiting.')) @classmethod def _need_root(cls): # FIXME this should do dnf itself (BZ#1062889) if os.geteuid() != 0: raise dnf.exceptions.Error( _('This command has to be run under the root user.')) def _guess_chroot(self): """ Guess which chroot is equivalent to this machine """ # FIXME Copr should generate non-specific arch repo dist = self.chroot_config if dist is None or (dist[0] is False) or (dist[1] is False): dist = linux_distribution() # Get distribution architecture distarch = self.base.conf.substitutions['basearch'] if any([name in dist for name in ["Fedora", "Fedora Linux"]]): if "Rawhide" in dist: chroot = ("fedora-rawhide-" + distarch) # workaround for enabling repos in Rawhide when VERSION in os-release # contains a name other than Rawhide elif "rawhide" in os_release_attr("redhat_support_product_version"): chroot = ("fedora-rawhide-" + distarch) else: chroot = ("fedora-{0}-{1}".format(dist[1], distarch)) elif "Mageia" in dist: # Get distribution architecture (Mageia does not use $basearch) distarch = rpm.expandMacro("%{distro_arch}") # Set the chroot if "Cauldron" in dist: chroot = ("mageia-cauldron-{}".format(distarch)) else: chroot = ("mageia-{0}-{1}".format(dist[1], distarch)) elif "openSUSE" in dist: # Get distribution architecture (openSUSE does not use $basearch) distarch = rpm.expandMacro("%{_target_cpu}") # Set the chroot if "Tumbleweed" in dist: chroot = ("opensuse-tumbleweed-{}".format(distarch)) else: chroot = ("opensuse-leap-{0}-{1}".format(dist[1], distarch)) else: chroot = ("epel-%s-x86_64" % dist[1].split(".", 1)[0]) return chroot def _download_repo(self, project_name, repo_filename): short_chroot = '-'.join(self.chroot_parts[:-1]) arch = self.chroot_parts[-1] api_path = "/coprs/{0}/repo/{1}/dnf.repo?arch={2}".format(project_name, short_chroot, arch) try: response = urlopen(self.copr_url + api_path) if os.path.exists(repo_filename): os.remove(repo_filename) except HTTPError as e: if e.code != 404: error_msg = _("Request to {0} failed: {1} - {2}").format(self.copr_url + api_path, e.code, str(e)) raise dnf.exceptions.Error(error_msg) error_msg = _("It wasn't possible to enable this project.\n") error_data = e.headers.get("Copr-Error-Data") if error_data: error_data_decoded = base64.b64decode(error_data).decode('utf-8') error_data_decoded = json.loads(error_data_decoded) error_msg += _("Repository '{0}' does not exist in project '{1}'.").format( '-'.join(self.chroot_parts), project_name) if error_data_decoded.get("available chroots"): error_msg += _("\nAvailable repositories: ") + ', '.join( "'{}'".format(x) for x in error_data_decoded["available chroots"]) error_msg += _("\n\nIf you want to enable a non-default repository, use the following command:\n" " 'dnf copr enable {0} <repository>'\n" "But note that the installed repo file will likely need a manual " "modification.").format(project_name) raise dnf.exceptions.Error(error_msg) else: error_msg += _("Project {0} does not exist.").format(project_name) raise dnf.exceptions.Error(error_msg) except URLError as e: error_msg = _("Failed to connect to {0}: {1}").format(self.copr_url + api_path, e.reason.strerror) raise dnf.exceptions.Error(error_msg) # Try to read the first line, and detect the repo_filename from that (override the repo_filename value). first_line = response.readline() line = first_line.decode("utf-8") if re.match(r"\[copr:", line): repo_filename = os.path.join(self.base.conf.get_reposdir, "_" + line[1:-2] + ".repo") # if using default hub, remove possible old repofile if self.copr_url == self.default_url: # copr:hub:user:project.repo => _copr_user_project.repo old_repo_filename = repo_filename.replace("_copr:", "_copr", 1)\ .replace(self.copr_hostname, "").replace(":", "_", 1).replace(":", "-")\ .replace("group_", "@") if os.path.exists(old_repo_filename): os.remove(old_repo_filename) with open(repo_filename, 'wb') as f: f.write(first_line) for line in response.readlines(): f.write(line) os.chmod(repo_filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) def _runtime_deps_warning(self, copr_username, copr_projectname): """ In addition to the main copr repo (that has repo ID prefixed with `copr:`), the repofile might contain additional repositories that serve as runtime dependencies. This method informs the user about the additional repos and provides an option to disable them. """ self.base.reset(repos=True) self.base.read_all_repos() repo = self._get_copr_repo(self._sanitize_username(copr_username), copr_projectname) runtime_deps = [] for repo_id in repo.cfg.sections(): if repo_id.startswith("copr:"): continue runtime_deps.append(repo_id) if not runtime_deps: return info = _( "Maintainer of the enabled Copr repository decided to make\n" "it dependent on other repositories. Such repositories are\n" "usually necessary for successful installation of RPMs from\n" "the main Copr repository (they provide runtime dependencies).\n\n" "Be aware that the note about quality and bug-reporting\n" "above applies here too, Fedora Project doesn't control the\n" "content. Please review the list:\n\n" "{0}\n\n" "These repositories have been enabled automatically." ) counter = itertools.count(1) info = info.format("\n\n".join([ "{num:2}. [{repoid}]\n baseurl={baseurl}".format( num=next(counter), repoid=repoid, baseurl=repo.cfg.getValue(repoid, "baseurl")) for repoid in runtime_deps ])) if not self._ask_user_no_raise(info, _("Do you want to keep them enabled?")): for dep in runtime_deps: self.base.conf.write_raw_configfile(repo.repofile, dep, self.base.conf.substitutions, {"enabled": "0"}) def _get_copr_repo(self, copr_username, copr_projectname): repo_id = "copr:{0}:{1}:{2}".format(self.copr_hostname.rsplit(':', 1)[0], self._sanitize_username(copr_username), copr_projectname) if repo_id not in self.base.repos: # check if there is a repo with old ID format repo_id = repo_id = "{0}-{1}".format(self._sanitize_username(copr_username), copr_projectname) if repo_id in self.base.repos and "_copr" in self.base.repos[repo_id].repofile: file_name = self.base.repos[repo_id].repofile.split('/')[-1] try: copr_hostname = file_name.rsplit(':', 2)[0].split(':', 1)[1] if copr_hostname != self.copr_hostname: return None except IndexError: # old filename format without hostname pass else: return None return self.base.repos[repo_id] def _remove_repo(self, copr_username, copr_projectname): # FIXME is it Copr repo ? repo = self._get_copr_repo(copr_username, copr_projectname) if not repo: raise dnf.exceptions.Error( _("Failed to remove copr repo {0}/{1}/{2}" .format(self.copr_hostname, copr_username, copr_projectname))) try: os.remove(repo.repofile) except OSError as e: raise dnf.exceptions.Error(str(e)) def _disable_repo(self, copr_username, copr_projectname): repo = self._get_copr_repo(copr_username, copr_projectname) if repo is None: raise dnf.exceptions.Error( _("Failed to disable copr repo {}/{}" .format(copr_username, copr_projectname))) # disable all repos provided by the repo file for repo_id in repo.cfg.sections(): self.base.conf.write_raw_configfile(repo.repofile, repo_id, self.base.conf.substitutions, {"enabled": "0"}) @classmethod def _get_data(cls, f): """ Wrapper around response from server check data and print nice error in case of some error (and return None) otherwise return json object. """ try: output = json.loads(f.read()) except ValueError: dnf.cli.CliError(_("Unknown response from server.")) return return output @classmethod def _check_json_output(cls, json_obj): if "error" in json_obj: raise dnf.exceptions.Error("{}".format(json_obj["error"])) @classmethod def _sanitize_username(cls, copr_username): if copr_username[0] == "@": return "group_{}".format(copr_username[1:]) else: return copr_username @dnf.plugin.register_command class PlaygroundCommand(CoprCommand): """ Playground plugin for DNF """ aliases = ("playground",) summary = _("Interact with Playground repository.") usage = " [enable|disable|upgrade]" def _cmd_enable(self, chroot): self._need_root() self._ask_user( _("Enabling a Playground repository."), _("Do you want to continue?"), ) api_url = "{0}/api/playground/list/".format( self.copr_url) f = self.base.urlopen(api_url, mode="w+") output = self._get_data(f) f.close() if output["output"] != "ok": raise dnf.cli.CliError(_("Unknown response from server.")) for repo in output["repos"]: project_name = "{0}/{1}".format(repo["username"], repo["coprname"]) repo_filename = "{}/_playground_{}.repo".format(self.base.conf.get_reposdir, project_name.replace("/", "-")) try: if chroot not in repo["chroots"]: continue api_url = "{0}/api/coprs/{1}/detail/{2}/".format( self.copr_url, project_name, chroot) f = self.base.urlopen(api_url, mode='w+') output2 = self._get_data(f) f.close() if (output2 and ("output" in output2) and (output2["output"] == "ok")): self._download_repo(project_name, repo_filename) except dnf.exceptions.Error: # likely 404 and that repo does not exist pass def _cmd_disable(self): self._need_root() for repo_filename in glob.glob("{}/_playground_*.repo".format(self.base.conf.get_reposdir)): self._remove_repo(repo_filename) @staticmethod def set_argparser(parser): parser.add_argument('subcommand', nargs=1, choices=['enable', 'disable', 'upgrade']) def run(self): raise dnf.exceptions.Error("Playground is temporarily unsupported") subcommand = self.opts.subcommand[0] chroot = self._guess_chroot() if subcommand == "enable": self._cmd_enable(chroot) logger.info(_("Playground repositories successfully enabled.")) elif subcommand == "disable": self._cmd_disable() logger.info(_("Playground repositories successfully disabled.")) elif subcommand == "upgrade": self._cmd_disable() self._cmd_enable(chroot) logger.info(_("Playground repositories successfully updated.")) else: raise dnf.exceptions.Error( _('Unknown subcommand {}.').format(subcommand))