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