initial import code into git
[platform/upstream/mic.git] / micng / utils / pkgmanagers / yumpkgmgr.py
1 #
2 # yum.py : yum utilities
3 #
4 # Copyright 2007, Red Hat  Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Library General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
19 import glob
20 import os
21 import sys
22 import logging
23
24 import yum
25 import rpmUtils
26 import pykickstart.parser
27
28 import urlparse
29 import urllib2 as u2
30 import tempfile
31 import shutil
32 import subprocess
33
34 from micng.utils.errors import *
35 from micng.utils.fs_related import *
36 from micng.imager.BaseImageCreator import ImageCreator
37
38 class MyYumRepository(yum.yumRepo.YumRepository):
39     def __init__(self, repoid):
40         yum.yumRepo.YumRepository.__init__(self, repoid)
41         self.sslverify = False
42
43     def _setupGrab(self):
44         self.sslverify = False
45         yum.yumRepo.YumRepository._setupGrab(self)
46
47     def __del__(self):
48         pass
49
50 class Yum(yum.YumBase):
51     def __init__(self, creator = None, recording_pkgs=None):
52         if not isinstance(creator, ImageCreator):
53             raise CreatorError("Invalid argument: creator")
54         yum.YumBase.__init__(self)
55         
56         self.creator = creator
57         
58         if self.creator.target_arch:
59             if rpmUtils.arch.arches.has_key(self.creator.target_arch):
60                 self.arch.setup_arch(self.creator.target_arch)
61             else:
62                 raise CreatorError("Invalid target arch: %s" % self.creator.target_arch)
63
64         self.__recording_pkgs = recording_pkgs
65         self.__pkgs_content = {}
66
67     def doFileLogSetup(self, uid, logfile):
68         # don't do the file log for the livecd as it can lead to open fds
69         # being left and an inability to clean up after ourself
70         pass
71
72     def close(self):
73         try:
74             os.unlink(self.conf.installroot + "/yum.conf")
75         except:
76             pass
77         self.closeRpmDB()
78         yum.YumBase.close(self)
79         self._delRepos()
80         self._delSacks()
81
82         if not os.path.exists("/etc/fedora-release") and not os.path.exists("/etc/meego-release"):
83             for i in range(3, os.sysconf("SC_OPEN_MAX")):
84                 try:
85                     os.close(i)
86                 except:
87                     pass
88
89     def __del__(self):
90         pass
91
92     def _writeConf(self, confpath, installroot):
93         conf  = "[main]\n"
94         conf += "installroot=%s\n" % installroot
95         conf += "cachedir=/var/cache/yum\n"
96         conf += "plugins=0\n"
97         conf += "reposdir=\n"
98         conf += "failovermethod=priority\n"
99         conf += "http_caching=packages\n"
100         conf += "sslverify=0\n"
101
102         f = file(confpath, "w+")
103         f.write(conf)
104         f.close()
105
106         os.chmod(confpath, 0644)
107
108     def _cleanupRpmdbLocks(self, installroot):
109         # cleans up temporary files left by bdb so that differing
110         # versions of rpm don't cause problems
111         for f in glob.glob(installroot + "/var/lib/rpm/__db*"):
112             os.unlink(f)
113
114     def setup(self, confpath, installroot):
115         self._writeConf(confpath, installroot)
116         self._cleanupRpmdbLocks(installroot)
117         self.doConfigSetup(fn = confpath, root = installroot)
118         self.conf.cache = 0
119         self.doTsSetup()
120         self.doRpmDBSetup()
121         self.doRepoSetup()
122         self.doSackSetup()
123
124     def selectPackage(self, pkg):
125         """Select a given package.  Can be specified with name.arch or name*"""
126         try:
127             self.install(pattern = pkg)
128             return None
129         except yum.Errors.InstallError, e:
130             return e
131         except yum.Errors.RepoError, e:
132             raise CreatorError("Unable to download from repo : %s" % (e,))
133         except yum.Errors.YumBaseError, e:
134             raise CreatorError("Unable to install: %s" % (e,))
135
136     def deselectPackage(self, pkg):
137         """Deselect package.  Can be specified as name.arch or name*"""
138         sp = pkg.rsplit(".", 2)
139         txmbrs = []
140         if len(sp) == 2:
141             txmbrs = self.tsInfo.matchNaevr(name=sp[0], arch=sp[1])
142
143         if len(txmbrs) == 0:
144             exact, match, unmatch = yum.packages.parsePackages(self.pkgSack.returnPackages(), [pkg], casematch=1)
145             for p in exact + match:
146                 txmbrs.append(p)
147
148         if len(txmbrs) > 0:
149             for x in txmbrs:
150                 self.tsInfo.remove(x.pkgtup)
151                 # we also need to remove from the conditionals
152                 # dict so that things don't get pulled back in as a result
153                 # of them.  yes, this is ugly.  conditionals should die.
154                 for req, pkgs in self.tsInfo.conditionals.iteritems():
155                     if x in pkgs:
156                         pkgs.remove(x)
157                         self.tsInfo.conditionals[req] = pkgs
158         else:
159             logging.warn("No such package %s to remove" %(pkg,))
160
161     def selectGroup(self, grp, include = pykickstart.parser.GROUP_DEFAULT):
162         try:
163             yum.YumBase.selectGroup(self, grp)
164             if include == pykickstart.parser.GROUP_REQUIRED:
165                 map(lambda p: self.deselectPackage(p), grp.default_packages.keys())
166             elif include == pykickstart.parser.GROUP_ALL:
167                 map(lambda p: self.selectPackage(p), grp.optional_packages.keys())
168             return None
169         except (yum.Errors.InstallError, yum.Errors.GroupsError), e:
170             return e
171         except yum.Errors.RepoError, e:
172             raise CreatorError("Unable to download from repo : %s" % (e,))
173         except yum.Errors.YumBaseError, e:
174             raise CreatorError("Unable to install: %s" % (e,))
175
176     def __checkAndDownloadURL(self, u2opener, url, savepath):
177         try:
178             if u2opener:
179                 f = u2opener.open(url)
180             else:
181                 f = u2.urlopen(url)
182         except u2.HTTPError, httperror:
183             if httperror.code in (404, 503):
184                 return None
185             else:
186                 raise CreatorError(httperror)
187         except OSError, oserr:
188             if oserr.errno == 2:
189                 return None
190             else:
191                 raise CreatorError(oserr)
192         except IOError, oserr:
193             if hasattr(oserr, "reason") and oserr.reason.errno == 2:
194                 return None
195             else:
196                 raise CreatorError(oserr)
197         except u2.URLError, err:
198             raise CreatorError(err)
199
200         # save to file
201         licf = open(savepath, "w")
202         licf.write(f.read())
203         licf.close()
204         f.close()
205
206         return savepath
207
208     def __pagerFile(self, savepath):
209         if os.path.splitext(savepath)[1].upper() in ('.HTM', '.HTML'):
210             pagers = ('w3m', 'links', 'lynx', 'less', 'more')
211         else:
212             pagers = ('less', 'more')
213
214         file_showed = None
215         for pager in pagers:
216             try:
217                 subprocess.call([pager, savepath])
218             except OSError:
219                 continue
220             else:
221                 file_showed = True
222                 break
223         if not file_showed:
224             f = open(savepath)
225             print f.read()
226             f.close()
227             raw_input('press <ENTER> to continue...')
228
229     def checkRepositoryEULA(self, name, repo):
230         """ This function is to check the LICENSE file if provided. """
231
232         # when proxy needed, make urllib2 follow it
233         proxy = repo.proxy
234         proxy_username = repo.proxy_username
235         proxy_password = repo.proxy_password
236
237         handlers = []
238         auth_handler = u2.HTTPBasicAuthHandler(u2.HTTPPasswordMgrWithDefaultRealm())
239         u2opener = None
240         if proxy:
241             if proxy_username:
242                 proxy_netloc = urlparse.urlsplit(proxy).netloc
243                 if proxy_password:
244                     proxy_url = 'http://%s:%s@%s' % (proxy_username, proxy_password, proxy_netloc)
245                 else:
246                     proxy_url = 'http://%s@%s' % (proxy_username, proxy_netloc)
247             else:
248                 proxy_url = proxy
249
250             proxy_support = u2.ProxyHandler({'http': proxy_url,
251                                              'ftp': proxy_url})
252             handlers.append(proxy_support)
253
254         # download all remote files to one temp dir
255         baseurl = None
256         repo_lic_dir = tempfile.mkdtemp(prefix = 'repolic')
257
258         for url in repo.baseurl:
259             if not url.endswith('/'):
260                 url += '/'
261             tmphandlers = handlers
262             (scheme, host, path, parm, query, frag) = urlparse.urlparse(url)
263             if scheme not in ("http", "https", "ftp", "ftps", "file"):
264                 raise CreatorError("Error: invalid url %s" % url)
265             if '@' in host:
266                 try:
267                     user_pass, host = host.split('@', 1)
268                     if ':' in user_pass:
269                         user, password = user_pass.split(':', 1)
270                 except ValueError, e:
271                     raise CreatorError('Bad URL: %s' % url)
272                 print "adding HTTP auth: %s, %s" %(user, password)
273                 auth_handler.add_password(None, host, user, password)
274                 tmphandlers.append(auth_handler)
275                 url = scheme + "://" + host + path + parm + query + frag
276             if len(tmphandlers) != 0:
277                 u2opener = u2.build_opener(*tmphandlers)
278             # try to download
279             repo_eula_url = urlparse.urljoin(url, "LICENSE.txt")
280             repo_eula_path = self.__checkAndDownloadURL(
281                                     u2opener,
282                                     repo_eula_url,
283                                     os.path.join(repo_lic_dir, repo.id + '_LICENSE.txt'))
284             if repo_eula_path:
285                 # found
286                 baseurl = url
287                 break
288
289         if not baseurl:
290             return True
291
292         # show the license file
293         print 'For the software packages in this yum repo:'
294         print '    %s: %s' % (name, baseurl)
295         print 'There is an "End User License Agreement" file that need to be checked.'
296         print 'Please read the terms and conditions outlined in it and answer the followed qustions.'
297         raw_input('press <ENTER> to continue...')
298
299         self.__pagerFile(repo_eula_path)
300
301         # Asking for the "Accept/Decline"
302         accept = True
303         while accept:
304             input_accept = raw_input('Would you agree to the terms and conditions outlined in the above End User License Agreement? (Yes/No): ')
305             if input_accept.upper() in ('YES', 'Y'):
306                 break
307             elif input_accept.upper() in ('NO', 'N'):
308                 accept = None
309                 print 'Will not install pkgs from this repo.'
310
311         if not accept:
312             #cleanup
313             shutil.rmtree(repo_lic_dir)
314             return None
315
316         # try to find support_info.html for extra infomation
317         repo_info_url = urlparse.urljoin(baseurl, "support_info.html")
318         repo_info_path = self.__checkAndDownloadURL(
319                                 u2opener,
320                                 repo_info_url,
321                                 os.path.join(repo_lic_dir, repo.id + '_support_info.html'))
322         if repo_info_path:
323             print 'There is one more file in the repo for additional support information, please read it'
324             raw_input('press <ENTER> to continue...')
325             self.__pagerFile(repo_info_path)
326
327         #cleanup
328         shutil.rmtree(repo_lic_dir)
329         return True
330
331     def addRepository(self, name, url = None, mirrorlist = None, proxy = None, proxy_username = None, proxy_password = None, inc = None, exc = None):
332         def _varSubstitute(option):
333             # takes a variable and substitutes like yum configs do
334             option = option.replace("$basearch", rpmUtils.arch.getBaseArch())
335             option = option.replace("$arch", rpmUtils.arch.getCanonArch())
336             return option
337
338         repo = MyYumRepository(name)
339         repo.sslverify = False
340
341         """Set proxy"""
342         repo.proxy = proxy
343         repo.proxy_username = proxy_username
344         repo.proxy_password = proxy_password
345
346         if url:
347             repo.baseurl.append(_varSubstitute(url))
348
349         # check LICENSE files
350         if not self.checkRepositoryEULA(name, repo):
351             return None
352
353         if mirrorlist:
354             repo.mirrorlist = _varSubstitute(mirrorlist)
355         conf = yum.config.RepoConf()
356         for k, v in conf.iteritems():
357             if v or not hasattr(repo, k):
358                 repo.setAttribute(k, v)
359         repo.basecachedir = self.conf.cachedir
360         repo.failovermethod = "priority"
361         repo.metadata_expire = 0
362         # Enable gpg check for verifying corrupt packages
363         repo.gpgcheck = 1
364         repo.enable()
365         repo.setup(0)
366         repo.setCallback(TextProgress())
367         self.repos.add(repo)
368         return repo
369
370     def installHasFile(self, file):
371         provides_pkg = self.whatProvides(file, None, None)
372         dlpkgs = map(lambda x: x.po, filter(lambda txmbr: txmbr.ts_state in ("i", "u"), self.tsInfo.getMembers()))
373         for p in dlpkgs:
374             for q in provides_pkg:
375                 if (p == q):
376                     return True
377         return False
378
379     def runInstall(self, checksize = 0):
380         os.environ["HOME"] = "/"
381         try:
382             (res, resmsg) = self.buildTransaction()
383         except yum.Errors.RepoError, e:
384             raise CreatorError("Unable to download from repo : %s" %(e,))
385         if res != 2:
386             raise CreatorError("Failed to build transaction : %s" % str.join("\n", resmsg))
387
388         dlpkgs = map(lambda x: x.po, filter(lambda txmbr: txmbr.ts_state in ("i", "u"), self.tsInfo.getMembers()))
389
390         # record the total size of installed pkgs
391         pkgs_total_size = sum(map(lambda x: int(x.size), dlpkgs))
392
393         # check needed size before actually download and install
394         if checksize and pkgs_total_size > checksize:
395             raise CreatorError("Size of specified root partition in kickstart file is too small to install all selected packages.")
396
397         if self.__recording_pkgs:
398             # record all pkg and the content
399             for pkg in dlpkgs:
400                 pkg_long_name = "%s-%s.%s.rpm" % (pkg.name, pkg.printVer(), pkg.arch)
401                 self.__pkgs_content[pkg_long_name] = pkg.files
402
403         total_count = len(dlpkgs)
404         cached_count = 0
405         print "Checking packages cache and packages integrity..."
406         for po in dlpkgs:
407             local = po.localPkg()
408             if not os.path.exists(local):
409                 continue
410             if not self.verifyPkg(local, po, False):
411                 print "Package %s is damaged: %s" % (os.path.basename(local), local)
412             else:
413                 cached_count +=1
414         print "%d packages to be installed, %d packages gotten from cache, %d packages to be downloaded" % (total_count, cached_count, total_count - cached_count)
415         try:
416             self.downloadPkgs(dlpkgs)
417             # FIXME: sigcheck?
418     
419             self.initActionTs()
420             self.populateTs(keepold=0)
421             deps = self.ts.check()
422             if len(deps) != 0:
423                 """ This isn't fatal, Ubuntu has this issue but it is ok. """
424                 print deps
425                 logging.warn("Dependency check failed!")
426             rc = self.ts.order()
427             if rc != 0:
428                 raise CreatorError("ordering packages for installation failed!")
429     
430             # FIXME: callback should be refactored a little in yum
431             sys.path.append('/usr/share/yum-cli')
432             import callback
433             cb = callback.RPMInstallCallback()
434             cb.tsInfo = self.tsInfo
435             cb.filelog = False
436             ret = self.runTransaction(cb)
437             print ""
438             self._cleanupRpmdbLocks(self.conf.installroot)
439             return ret
440         except yum.Errors.RepoError, e:
441             raise CreatorError("Unable to download from repo : %s" % (e,))
442         except yum.Errors.YumBaseError, e:
443             raise CreatorError("Unable to install: %s" % (e,))
444
445     def getAllContent(self):
446         return self.__pkgs_content
447
448 _pkgmgr = ["yum", Yum]