fix redundant whitespace in logging info
[tools/mic.git] / mic / utils / rpmmisc.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (c) 2008, 2009, 2010, 2011 Intel, Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; version 2 of the License
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc., 59
16 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18 import os
19 import sys
20 import re
21 import rpm
22
23 from mic import msger
24 from mic.utils.errors import CreatorError
25 from mic.utils.proxy import get_proxy_for
26 from mic.utils import runner
27
28
29 class RPMInstallCallback:
30     """ Command line callback class for callbacks from the RPM library.
31     """
32
33     def __init__(self, ts, output=1):
34         self.output = output
35         self.callbackfilehandles = {}
36         self.total_actions = 0
37         self.total_installed = 0
38         self.installed_pkg_names = []
39         self.total_removed = 0
40         self.mark = "+"
41         self.marks = 40
42         self.lastmsg = None
43         self.tsInfo = None # this needs to be set for anything else to work
44         self.ts = ts
45         self.filelog = False
46         self.logString = []
47         self.headmsg = "Installing"
48
49     def _dopkgtup(self, hdr):
50         tmpepoch = hdr['epoch']
51         if tmpepoch is None: epoch = '0'
52         else: epoch = str(tmpepoch)
53
54         return (hdr['name'], hdr['arch'], epoch, hdr['version'], hdr['release'])
55
56     def _makeHandle(self, hdr):
57         handle = '%s:%s.%s-%s-%s' % (hdr['epoch'], hdr['name'], hdr['version'],
58           hdr['release'], hdr['arch'])
59
60         return handle
61
62     def _localprint(self, msg):
63         if self.output:
64             msger.info(msg)
65
66     def _makefmt(self, percent, progress = True):
67         l = len(str(self.total_actions))
68         size = "%s.%s" % (l, l)
69         fmt_done = "[%" + size + "s/%" + size + "s]"
70         done = fmt_done % (self.total_installed + self.total_removed,
71                            self.total_actions)
72         marks = self.marks - (2 * l)
73         width = "%s.%s" % (marks, marks)
74         fmt_bar = "%-" + width + "s"
75         if progress:
76             bar = fmt_bar % (self.mark * int(marks * (percent / 100.0)), )
77             fmt = "%-10.10s: %-20.20s " + bar + " " + done
78         else:
79             bar = fmt_bar % (self.mark * marks, )
80             fmt = "%-10.10s: %-20.20s "  + bar + " " + done
81         return fmt
82
83     def _logPkgString(self, hdr):
84         """return nice representation of the package for the log"""
85         (n,a,e,v,r) = self._dopkgtup(hdr)
86         if e == '0':
87             pkg = '%s.%s %s-%s' % (n, a, v, r)
88         else:
89             pkg = '%s.%s %s:%s-%s' % (n, a, e, v, r)
90
91         return pkg
92
93     def callback(self, what, bytes, total, h, user):
94         if what == rpm.RPMCALLBACK_TRANS_START:
95             if bytes == 6:
96                 self.total_actions = total
97
98         elif what == rpm.RPMCALLBACK_TRANS_PROGRESS:
99             pass
100
101         elif what == rpm.RPMCALLBACK_TRANS_STOP:
102             pass
103
104         elif what == rpm.RPMCALLBACK_INST_OPEN_FILE:
105             self.lastmsg = None
106             hdr = None
107             if h is not None:
108                 try:
109                     hdr, rpmloc = h
110                 except:
111                     rpmloc = h
112                     hdr = readRpmHeader(self.ts, h)
113
114                 handle = self._makeHandle(hdr)
115                 fd = os.open(rpmloc, os.O_RDONLY)
116                 self.callbackfilehandles[handle]=fd
117                 if hdr['name'] not in self.installed_pkg_names:
118                     self.installed_pkg_names.append(hdr['name'])
119                     self.total_installed += 1
120                 return fd
121             else:
122                 self._localprint("No header - huh?")
123
124         elif what == rpm.RPMCALLBACK_INST_CLOSE_FILE:
125             hdr = None
126             if h is not None:
127                 try:
128                     hdr, rpmloc = h
129                 except:
130                     rpmloc = h
131                     hdr = readRpmHeader(self.ts, h)
132
133                 handle = self._makeHandle(hdr)
134                 os.close(self.callbackfilehandles[handle])
135                 fd = 0
136
137                 # log stuff
138                 #pkgtup = self._dopkgtup(hdr)
139                 self.logString.append(self._logPkgString(hdr))
140
141         elif what == rpm.RPMCALLBACK_INST_PROGRESS:
142             if h is not None:
143                 percent = (self.total_installed*100L)/self.total_actions
144                 if total > 0:
145                     try:
146                         hdr, rpmloc = h
147                     except:
148                         rpmloc = h
149
150                     m = re.match("(.*)-(\d+.*)-(\d+\.\d+)\.(.+)\.rpm", os.path.basename(rpmloc))
151                     if m:
152                         pkgname = m.group(1)
153                     else:
154                         pkgname = os.path.basename(rpmloc)
155                 if self.output:
156                     fmt = self._makefmt(percent)
157                     msg = fmt % (self.headmsg, pkgname)
158                     if msg != self.lastmsg:
159                         self.lastmsg = msg
160
161                         msger.info(msg)
162
163                         if self.total_installed == self.total_actions:
164                             msger.raw('')
165                             msger.verbose('\n'.join(self.logString))
166
167         elif what == rpm.RPMCALLBACK_UNINST_START:
168             pass
169
170         elif what == rpm.RPMCALLBACK_UNINST_PROGRESS:
171             pass
172
173         elif what == rpm.RPMCALLBACK_UNINST_STOP:
174             self.total_removed += 1
175
176         elif what == rpm.RPMCALLBACK_REPACKAGE_START:
177             pass
178
179         elif what == rpm.RPMCALLBACK_REPACKAGE_STOP:
180             pass
181
182         elif what == rpm.RPMCALLBACK_REPACKAGE_PROGRESS:
183             pass
184
185 def readRpmHeader(ts, filename):
186     """ Read an rpm header. """
187
188     fd = os.open(filename, os.O_RDONLY)
189     h = ts.hdrFromFdno(fd)
190     os.close(fd)
191     return h
192
193 def splitFilename(filename):
194     """ Pass in a standard style rpm fullname
195
196         Return a name, version, release, epoch, arch, e.g.::
197             foo-1.0-1.i386.rpm returns foo, 1.0, 1, i386
198             1:bar-9-123a.ia64.rpm returns bar, 9, 123a, 1, ia64
199     """
200
201     if filename[-4:] == '.rpm':
202         filename = filename[:-4]
203
204     archIndex = filename.rfind('.')
205     arch = filename[archIndex+1:]
206
207     relIndex = filename[:archIndex].rfind('-')
208     rel = filename[relIndex+1:archIndex]
209
210     verIndex = filename[:relIndex].rfind('-')
211     ver = filename[verIndex+1:relIndex]
212
213     epochIndex = filename.find(':')
214     if epochIndex == -1:
215         epoch = ''
216     else:
217         epoch = filename[:epochIndex]
218
219     name = filename[epochIndex + 1:verIndex]
220     return name, ver, rel, epoch, arch
221
222 def getCanonX86Arch(arch):
223     #
224     if arch == "i586":
225         f = open("/proc/cpuinfo", "r")
226         lines = f.readlines()
227         f.close()
228         for line in lines:
229             if line.startswith("model name") and line.find("Geode(TM)") != -1:
230                 return "geode"
231         return arch
232     # only athlon vs i686 isn't handled with uname currently
233     if arch != "i686":
234         return arch
235
236     # if we're i686 and AuthenticAMD, then we should be an athlon
237     f = open("/proc/cpuinfo", "r")
238     lines = f.readlines()
239     f.close()
240     for line in lines:
241         if line.startswith("vendor") and line.find("AuthenticAMD") != -1:
242             return "athlon"
243         # i686 doesn't guarantee cmov, but we depend on it
244         elif line.startswith("flags") and line.find("cmov") == -1:
245             return "i586"
246
247     return arch
248
249 def getCanonX86_64Arch(arch):
250     if arch != "x86_64":
251         return arch
252
253     vendor = None
254     f = open("/proc/cpuinfo", "r")
255     lines = f.readlines()
256     f.close()
257     for line in lines:
258         if line.startswith("vendor_id"):
259             vendor = line.split(':')[1]
260             break
261     if vendor is None:
262         return arch
263
264     if vendor.find("Authentic AMD") != -1 or vendor.find("AuthenticAMD") != -1:
265         return "amd64"
266     if vendor.find("GenuineIntel") != -1:
267         return "ia32e"
268     return arch
269
270 def getCanonArch():
271     arch = os.uname()[4]
272
273     if (len(arch) == 4 and arch[0] == "i" and arch[2:4] == "86"):
274         return getCanonX86Arch(arch)
275
276     if arch == "x86_64":
277         return getCanonX86_64Arch(arch)
278
279     return arch
280
281 # Copy from libsatsolver:poolarch.c, with cleanup
282 archPolicies = {
283     "x86_64":       "x86_64:i686:i586:i486:i386",
284     "i686":         "i686:i586:i486:i386",
285     "i586":         "i586:i486:i386",
286     "ia64":         "ia64:i686:i586:i486:i386",
287     "armv7tnhl":    "armv7tnhl:armv7thl:armv7nhl:armv7hl",
288     "armv7thl":     "armv7thl:armv7hl",
289     "armv7nhl":     "armv7nhl:armv7hl",
290     "armv7hl":      "armv7hl",
291     "armv7l":       "armv7l:armv6l:armv5tejl:armv5tel:armv5l:armv4tl:armv4l:armv3l",
292     "armv6l":       "armv6l:armv5tejl:armv5tel:armv5l:armv4tl:armv4l:armv3l",
293     "armv5tejl":    "armv5tejl:armv5tel:armv5l:armv4tl:armv4l:armv3l",
294     "armv5tel":     "armv5tel:armv5l:armv4tl:armv4l:armv3l",
295     "armv5l":       "armv5l:armv4tl:armv4l:armv3l",
296 }
297
298 # dict mapping arch -> ( multicompat, best personality, biarch personality )
299 multilibArches = {
300     "x86_64":  ( "athlon", "x86_64", "athlon" ),
301 }
302
303 # from yumUtils.py
304 arches = {
305     # ia32
306     "athlon": "i686",
307     "i686": "i586",
308     "geode": "i586",
309     "i586": "i486",
310     "i486": "i386",
311     "i386": "noarch",
312
313     # amd64
314     "x86_64": "athlon",
315     "amd64": "x86_64",
316     "ia32e": "x86_64",
317
318     # arm
319     "armv7tnhl": "armv7nhl",
320     "armv7nhl": "armv7hl",
321     "armv7hl": "noarch",
322     "armv7l": "armv6l",
323     "armv6l": "armv5tejl",
324     "armv5tejl": "armv5tel",
325     "armv5tel": "noarch",
326
327     #itanium
328     "ia64": "noarch",
329 }
330
331 def isMultiLibArch(arch=None):
332     """returns true if arch is a multilib arch, false if not"""
333     if arch is None:
334         arch = getCanonArch()
335
336     if not arches.has_key(arch): # or we could check if it is noarch
337         return False
338
339     if multilibArches.has_key(arch):
340         return True
341
342     if multilibArches.has_key(arches[arch]):
343         return True
344
345     return False
346
347 def getBaseArch():
348     myarch = getCanonArch()
349     if not arches.has_key(myarch):
350         return myarch
351
352     if isMultiLibArch(arch=myarch):
353         if multilibArches.has_key(myarch):
354             return myarch
355         else:
356             return arches[myarch]
357
358     if arches.has_key(myarch):
359         basearch = myarch
360         value = arches[basearch]
361         while value != 'noarch':
362             basearch = value
363             value = arches[basearch]
364
365         return basearch
366
367 def checkRpmIntegrity(bin_rpm, package):
368     return runner.quiet([bin_rpm, "-K", "--nosignature", package])
369
370 def checkSig(ts, package):
371     """ Takes a transaction set and a package, check it's sigs,
372         return 0 if they are all fine
373         return 1 if the gpg key can't be found
374         return 2 if the header is in someway damaged
375         return 3 if the key is not trusted
376         return 4 if the pkg is not gpg or pgp signed
377     """
378
379     value = 0
380     currentflags = ts.setVSFlags(0)
381     fdno = os.open(package, os.O_RDONLY)
382     try:
383         hdr = ts.hdrFromFdno(fdno)
384
385     except rpm.error, e:
386         if str(e) == "public key not availaiable":
387             value = 1
388         if str(e) == "public key not available":
389             value = 1
390         if str(e) == "public key not trusted":
391             value = 3
392         if str(e) == "error reading package header":
393             value = 2
394     else:
395         error, siginfo = getSigInfo(hdr)
396         if error == 101:
397             os.close(fdno)
398             del hdr
399             value = 4
400         else:
401             del hdr
402
403     try:
404         os.close(fdno)
405     except OSError:
406         pass
407
408     ts.setVSFlags(currentflags) # put things back like they were before
409     return value
410
411 def getSigInfo(hdr):
412     """ checks signature from an hdr hand back signature information and/or
413         an error code
414     """
415
416     import locale
417     locale.setlocale(locale.LC_ALL, 'C')
418
419     string = '%|DSAHEADER?{%{DSAHEADER:pgpsig}}:{%|RSAHEADER?{%{RSAHEADER:pgpsig}}:{%|SIGGPG?{%{SIGGPG:pgpsig}}:{%|SIGPGP?{%{SIGPGP:pgpsig}}:{(none)}|}|}|}|'
420     siginfo = hdr.sprintf(string)
421     if siginfo != '(none)':
422         error = 0
423         sigtype, sigdate, sigid = siginfo.split(',')
424     else:
425         error = 101
426         sigtype = 'MD5'
427         sigdate = 'None'
428         sigid = 'None'
429
430     infotuple = (sigtype, sigdate, sigid)
431     return error, infotuple
432
433 def checkRepositoryEULA(name, repo):
434     """ This function is to check the EULA file if provided.
435         return True: no EULA or accepted
436         return False: user declined the EULA
437     """
438
439     import tempfile
440     import shutil
441     import urlparse
442     import urllib2 as u2
443     import httplib
444     from mic.utils.errors import CreatorError
445
446     def _check_and_download_url(u2opener, url, savepath):
447         try:
448             if u2opener:
449                 f = u2opener.open(url)
450             else:
451                 f = u2.urlopen(url)
452         except u2.HTTPError, httperror:
453             if httperror.code in (404, 503):
454                 return None
455             else:
456                 raise CreatorError(httperror)
457         except OSError, oserr:
458             if oserr.errno == 2:
459                 return None
460             else:
461                 raise CreatorError(oserr)
462         except IOError, oserr:
463             if hasattr(oserr, "reason") and oserr.reason.errno == 2:
464                 return None
465             else:
466                 raise CreatorError(oserr)
467         except u2.URLError, err:
468             raise CreatorError(err)
469         except httplib.HTTPException, e:
470             raise CreatorError(e)
471
472         # save to file
473         licf = open(savepath, "w")
474         licf.write(f.read())
475         licf.close()
476         f.close()
477
478         return savepath
479
480     def _pager_file(savepath):
481
482         if os.path.splitext(savepath)[1].upper() in ('.HTM', '.HTML'):
483             pagers = ('w3m', 'links', 'lynx', 'less', 'more')
484         else:
485             pagers = ('less', 'more')
486
487         file_showed = False
488         for pager in pagers:
489             cmd = "%s %s" % (pager, savepath)
490             try:
491                 os.system(cmd)
492             except OSError:
493                 continue
494             else:
495                 file_showed = True
496                 break
497
498         if not file_showed:
499             f = open(savepath)
500             msger.raw(f.read())
501             f.close()
502             msger.pause()
503
504     # when proxy needed, make urllib2 follow it
505     proxy = repo.proxy
506     proxy_username = repo.proxy_username
507     proxy_password = repo.proxy_password
508
509     if not proxy:
510         proxy = get_proxy_for(repo.baseurl[0])
511
512     handlers = []
513     auth_handler = u2.HTTPBasicAuthHandler(u2.HTTPPasswordMgrWithDefaultRealm())
514     u2opener = None
515     if proxy:
516         if proxy_username:
517             proxy_netloc = urlparse.urlsplit(proxy).netloc
518             if proxy_password:
519                 proxy_url = 'http://%s:%s@%s' % (proxy_username, proxy_password, proxy_netloc)
520             else:
521                 proxy_url = 'http://%s@%s' % (proxy_username, proxy_netloc)
522         else:
523             proxy_url = proxy
524
525         proxy_support = u2.ProxyHandler({'http': proxy_url,
526                                          'https': proxy_url,
527                                          'ftp': proxy_url})
528         handlers.append(proxy_support)
529
530     # download all remote files to one temp dir
531     baseurl = None
532     repo_lic_dir = tempfile.mkdtemp(prefix = 'repolic')
533
534     for url in repo.baseurl:
535         tmphandlers = handlers[:]
536
537         (scheme, host, path, parm, query, frag) = urlparse.urlparse(url.rstrip('/') + '/')
538         if scheme not in ("http", "https", "ftp", "ftps", "file"):
539             raise CreatorError("Error: invalid url %s" % url)
540
541         if '@' in host:
542             try:
543                 user_pass, host = host.split('@', 1)
544                 if ':' in user_pass:
545                     user, password = user_pass.split(':', 1)
546             except ValueError, e:
547                 raise CreatorError('Bad URL: %s' % url)
548
549             msger.verbose("adding HTTP auth: %s, XXXXXXXX" %(user))
550             auth_handler.add_password(None, host, user, password)
551             tmphandlers.append(auth_handler)
552             url = scheme + "://" + host + path + parm + query + frag
553
554         if tmphandlers:
555             u2opener = u2.build_opener(*tmphandlers)
556
557         # try to download
558         repo_eula_url = urlparse.urljoin(url, "LICENSE.txt")
559         repo_eula_path = _check_and_download_url(
560                                 u2opener,
561                                 repo_eula_url,
562                                 os.path.join(repo_lic_dir, repo.id + '_LICENSE.txt'))
563         if repo_eula_path:
564             # found
565             baseurl = url
566             break
567
568     if not baseurl:
569         shutil.rmtree(repo_lic_dir) #cleanup
570         return True
571
572     # show the license file
573     msger.info('For the software packages in this yum repo:')
574     msger.info('    %s: %s' % (name, baseurl))
575     msger.info('There is an "End User License Agreement" file that need to be checked.')
576     msger.info('Please read the terms and conditions outlined in it and answer the followed qustions.')
577     msger.pause()
578
579     _pager_file(repo_eula_path)
580
581     # Asking for the "Accept/Decline"
582     if not msger.ask('Would you agree to the terms and conditions outlined in the above End User License Agreement?'):
583         msger.warning('Will not install pkgs from this repo.')
584         shutil.rmtree(repo_lic_dir) #cleanup
585         return False
586
587     # try to find support_info.html for extra infomation
588     repo_info_url = urlparse.urljoin(baseurl, "support_info.html")
589     repo_info_path = _check_and_download_url(
590                             u2opener,
591                             repo_info_url,
592                             os.path.join(repo_lic_dir, repo.id + '_support_info.html'))
593     if repo_info_path:
594         msger.info('There is one more file in the repo for additional support information, please read it')
595         msger.pause()
596         _pager_file(repo_info_path)
597
598     #cleanup
599     shutil.rmtree(repo_lic_dir)
600     return True