source: yum/trunk/output.py@ 1569

Last change on this file since 1569 was 516, checked in by Yuri Dario, 11 years ago

yum: update trunk to 3.4.3.

  • Property svn:eol-style set to native
File size: 96.1 KB
Line 
1#!/usr/bin/python -t
2
3"""This handles actual output from the cli"""
4
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
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# Copyright 2005 Duke University
19
20import sys
21import time
22import logging
23import types
24import gettext
25import pwd
26import rpm
27
28import re # For YumTerm
29
30from weakref import proxy as weakref
31
32from urlgrabber.progress import TextMeter
33import urlgrabber.progress
34from urlgrabber.grabber import URLGrabError
35from yum.misc import prco_tuple_to_string
36from yum.i18n import to_str, to_utf8, to_unicode
37import yum.misc
38from rpmUtils.miscutils import checkSignals, formatRequire
39from yum.constants import *
40
41from yum import logginglevels, _, P_
42from yum.rpmtrans import RPMBaseCallback
43from yum.packageSack import packagesNewestByNameArch
44import yum.packages
45
46import yum.history
47
48from yum.i18n import utf8_width, utf8_width_fill, utf8_text_fill
49
50def _term_width():
51 """ Simple terminal width, limit to 20 chars. and make 0 == 80. """
52 if not hasattr(urlgrabber.progress, 'terminal_width_cached'):
53 return 80
54 ret = urlgrabber.progress.terminal_width_cached()
55 if ret == 0:
56 return 80
57 if ret < 20:
58 return 20
59 return ret
60
61
62class YumTextMeter(TextMeter):
63
64 """
65 Text progress bar output.
66 """
67
68 def update(self, amount_read, now=None):
69 checkSignals()
70 TextMeter.update(self, amount_read, now)
71
72class YumTerm:
73 """some terminal "UI" helpers based on curses"""
74
75 # From initial search for "terminfo and python" got:
76 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
77 # ...it's probably not copyrightable, but if so ASPN says:
78 #
79 # Except where otherwise noted, recipes in the Python Cookbook are
80 # published under the Python license.
81
82 __enabled = True
83
84 if hasattr(urlgrabber.progress, 'terminal_width_cached'):
85 columns = property(lambda self: _term_width())
86
87 __cap_names = {
88 'underline' : 'smul',
89 'reverse' : 'rev',
90 'normal' : 'sgr0',
91 }
92
93 __colors = {
94 'black' : 0,
95 'blue' : 1,
96 'green' : 2,
97 'cyan' : 3,
98 'red' : 4,
99 'magenta' : 5,
100 'yellow' : 6,
101 'white' : 7
102 }
103 __ansi_colors = {
104 'black' : 0,
105 'red' : 1,
106 'green' : 2,
107 'yellow' : 3,
108 'blue' : 4,
109 'magenta' : 5,
110 'cyan' : 6,
111 'white' : 7
112 }
113 __ansi_forced_MODE = {
114 'bold' : '\x1b[1m',
115 'blink' : '\x1b[5m',
116 'dim' : '',
117 'reverse' : '\x1b[7m',
118 'underline' : '\x1b[4m',
119 'normal' : '\x1b(B\x1b[m'
120 }
121 __ansi_forced_FG_COLOR = {
122 'black' : '\x1b[30m',
123 'red' : '\x1b[31m',
124 'green' : '\x1b[32m',
125 'yellow' : '\x1b[33m',
126 'blue' : '\x1b[34m',
127 'magenta' : '\x1b[35m',
128 'cyan' : '\x1b[36m',
129 'white' : '\x1b[37m'
130 }
131 __ansi_forced_BG_COLOR = {
132 'black' : '\x1b[40m',
133 'red' : '\x1b[41m',
134 'green' : '\x1b[42m',
135 'yellow' : '\x1b[43m',
136 'blue' : '\x1b[44m',
137 'magenta' : '\x1b[45m',
138 'cyan' : '\x1b[46m',
139 'white' : '\x1b[47m'
140 }
141
142 def __forced_init(self):
143 self.MODE = self.__ansi_forced_MODE
144 self.FG_COLOR = self.__ansi_forced_FG_COLOR
145 self.BG_COLOR = self.__ansi_forced_BG_COLOR
146
147 def reinit(self, term_stream=None, color='auto'):
148 self.__enabled = True
149 if not hasattr(urlgrabber.progress, 'terminal_width_cached'):
150 self.columns = 80
151 self.lines = 24
152
153 if color == 'always':
154 self.__forced_init()
155 return
156
157 # Output modes:
158 self.MODE = {
159 'bold' : '',
160 'blink' : '',
161 'dim' : '',
162 'reverse' : '',
163 'underline' : '',
164 'normal' : ''
165 }
166
167 # Colours
168 self.FG_COLOR = {
169 'black' : '',
170 'blue' : '',
171 'green' : '',
172 'cyan' : '',
173 'red' : '',
174 'magenta' : '',
175 'yellow' : '',
176 'white' : ''
177 }
178
179 self.BG_COLOR = {
180 'black' : '',
181 'blue' : '',
182 'green' : '',
183 'cyan' : '',
184 'red' : '',
185 'magenta' : '',
186 'yellow' : '',
187 'white' : ''
188 }
189
190 if color == 'never':
191 self.__enabled = False
192 return
193 assert color == 'auto'
194
195 # Curses isn't available on all platforms
196 try:
197 import curses
198 except:
199 self.__enabled = False
200 return
201
202 # If the stream isn't a tty, then assume it has no capabilities.
203 if not term_stream:
204 term_stream = sys.stdout
205 if not term_stream.isatty():
206 self.__enabled = False
207 return
208
209 # Check the terminal type. If we fail, then assume that the
210 # terminal has no capabilities.
211 try:
212 curses.setupterm(fd=term_stream.fileno())
213 except:
214 self.__enabled = False
215 return
216 self._ctigetstr = curses.tigetstr
217
218 if not hasattr(urlgrabber.progress, 'terminal_width_cached'):
219 self.columns = curses.tigetnum('cols')
220 self.lines = curses.tigetnum('lines')
221
222 # Look up string capabilities.
223 for cap_name in self.MODE:
224 mode = cap_name
225 if cap_name in self.__cap_names:
226 cap_name = self.__cap_names[cap_name]
227 self.MODE[mode] = self._tigetstr(cap_name) or ''
228
229 # Colors
230 set_fg = self._tigetstr('setf')
231 if set_fg:
232 for (color, val) in self.__colors.items():
233 self.FG_COLOR[color] = curses.tparm(set_fg, val) or ''
234 set_fg_ansi = self._tigetstr('setaf')
235 if set_fg_ansi:
236 for (color, val) in self.__ansi_colors.items():
237 self.FG_COLOR[color] = curses.tparm(set_fg_ansi, val) or ''
238 set_bg = self._tigetstr('setb')
239 if set_bg:
240 for (color, val) in self.__colors.items():
241 self.BG_COLOR[color] = curses.tparm(set_bg, val) or ''
242 set_bg_ansi = self._tigetstr('setab')
243 if set_bg_ansi:
244 for (color, val) in self.__ansi_colors.items():
245 self.BG_COLOR[color] = curses.tparm(set_bg_ansi, val) or ''
246
247 def __init__(self, term_stream=None, color='auto'):
248 self.reinit(term_stream, color)
249
250 def _tigetstr(self, cap_name):
251 # String capabilities can include "delays" of the form "$<2>".
252 # For any modern terminal, we should be able to just ignore
253 # these, so strip them out.
254 cap = self._ctigetstr(cap_name) or ''
255 return re.sub(r'\$<\d+>[/*]?', '', cap)
256
257 def sub(self, haystack, beg, end, needles, escape=None, ignore_case=False):
258 if not self.__enabled:
259 return haystack
260
261 if not escape:
262 escape = re.escape
263
264 render = lambda match: beg + match.group() + end
265 for needle in needles:
266 pat = escape(needle)
267 if ignore_case:
268 pat = re.template(pat, re.I)
269 haystack = re.sub(pat, render, haystack)
270 return haystack
271 def sub_norm(self, haystack, beg, needles, **kwds):
272 return self.sub(haystack, beg, self.MODE['normal'], needles, **kwds)
273
274 def sub_mode(self, haystack, mode, needles, **kwds):
275 return self.sub_norm(haystack, self.MODE[mode], needles, **kwds)
276
277 def sub_bold(self, haystack, needles, **kwds):
278 return self.sub_mode(haystack, 'bold', needles, **kwds)
279
280 def sub_fg(self, haystack, color, needles, **kwds):
281 return self.sub_norm(haystack, self.FG_COLOR[color], needles, **kwds)
282
283 def sub_bg(self, haystack, color, needles, **kwds):
284 return self.sub_norm(haystack, self.BG_COLOR[color], needles, **kwds)
285
286
287
288class YumOutput:
289
290 """
291 Main output class for the yum command line.
292 """
293
294 def __init__(self):
295 self.logger = logging.getLogger("yum.cli")
296 self.verbose_logger = logging.getLogger("yum.verbose.cli")
297 if hasattr(rpm, "expandMacro"):
298 self.i18ndomains = rpm.expandMacro("%_i18ndomains").split(":")
299 else:
300 self.i18ndomains = ["redhat-dist"]
301
302 self.term = YumTerm()
303 self._last_interrupt = None
304
305
306 def printtime(self):
307 months = [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'),
308 _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')]
309 now = time.localtime(time.time())
310 ret = months[int(time.strftime('%m', now)) - 1] + \
311 time.strftime(' %d %T ', now)
312 return ret
313
314 def failureReport(self, errobj):
315 """failure output for failovers from urlgrabber"""
316
317 self.logger.error('%s: %s', errobj.url, errobj.exception)
318 self.logger.error(_('Trying other mirror.'))
319 raise errobj.exception
320
321
322 def simpleProgressBar(self, current, total, name=None):
323 progressbar(current, total, name)
324
325 def _highlight(self, highlight):
326 hibeg = ''
327 hiend = ''
328 if not highlight:
329 pass
330 elif not isinstance(highlight, basestring) or highlight == 'bold':
331 hibeg = self.term.MODE['bold']
332 elif highlight == 'normal':
333 pass # Minor opt.
334 else:
335 # Turn a string into a specific output: colour, bold, etc.
336 for high in highlight.replace(',', ' ').split():
337 if False: pass
338 elif high == 'normal':
339 hibeg = ''
340 elif high in self.term.MODE:
341 hibeg += self.term.MODE[high]
342 elif high in self.term.FG_COLOR:
343 hibeg += self.term.FG_COLOR[high]
344 elif (high.startswith('fg:') and
345 high[3:] in self.term.FG_COLOR):
346 hibeg += self.term.FG_COLOR[high[3:]]
347 elif (high.startswith('bg:') and
348 high[3:] in self.term.BG_COLOR):
349 hibeg += self.term.BG_COLOR[high[3:]]
350
351 if hibeg:
352 hiend = self.term.MODE['normal']
353 return (hibeg, hiend)
354
355 def _sub_highlight(self, haystack, highlight, needles, **kwds):
356 hibeg, hiend = self._highlight(highlight)
357 return self.term.sub(haystack, hibeg, hiend, needles, **kwds)
358
359 @staticmethod
360 def _calc_columns_spaces_helps(current, data_tups, left):
361 """ Spaces left on the current field will help how many pkgs? """
362 ret = 0
363 for tup in data_tups:
364 if left < (tup[0] - current):
365 break
366 ret += tup[1]
367 return ret
368
369 def calcColumns(self, data, columns=None, remainder_column=0,
370 total_width=None, indent=''):
371 """ Dynamically calculate the width of the fields in the data, data is
372 of the format [column-number][field_length] = rows. """
373
374 if total_width is None:
375 total_width = self.term.columns
376
377 cols = len(data)
378 # Convert the data to ascending list of tuples, (field_length, pkgs)
379 pdata = data
380 data = [None] * cols # Don't modify the passed in data
381 for d in range(0, cols):
382 data[d] = sorted(pdata[d].items())
383
384 # We start allocating 1 char to everything but the last column, and a
385 # space between each (again, except for the last column). Because
386 # at worst we are better with:
387 # |one two three|
388 # | four |
389 # ...than:
390 # |one two three|
391 # | f|
392 # |our |
393 # ...the later being what we get if we pre-allocate the last column, and
394 # thus. the space, due to "three" overflowing it's column by 2 chars.
395 if columns is None:
396 columns = [1] * (cols - 1)
397 columns.append(0)
398
399 total_width -= (sum(columns) + (cols - 1) + utf8_width(indent))
400 if not columns[-1]:
401 total_width += 1
402 while total_width > 0:
403 # Find which field all the spaces left will help best
404 helps = 0
405 val = 0
406 for d in xrange(0, cols):
407 thelps = self._calc_columns_spaces_helps(columns[d], data[d],
408 total_width)
409 if not thelps:
410 continue
411 # We prefer to overflow: the last column, and then earlier
412 # columns. This is so that in the best case (just overflow the
413 # last) ... grep still "works", and then we make it prettier.
414 if helps and (d == (cols - 1)) and (thelps / 2) < helps:
415 continue
416 if thelps < helps:
417 continue
418 helps = thelps
419 val = d
420
421 # If we found a column to expand, move up to the next level with
422 # that column and start again with any remaining space.
423 if helps:
424 diff = data[val].pop(0)[0] - columns[val]
425 if not columns[val] and (val == (cols - 1)):
426 # If we are going from 0 => N on the last column, take 1
427 # for the space before the column.
428 total_width -= 1
429 columns[val] += diff
430 total_width -= diff
431 continue
432
433 overflowed_columns = 0
434 for d in xrange(0, cols):
435 if not data[d]:
436 continue
437 overflowed_columns += 1
438 if overflowed_columns:
439 # Split the remaining spaces among each overflowed column
440 # equally
441 norm = total_width / overflowed_columns
442 for d in xrange(0, cols):
443 if not data[d]:
444 continue
445 columns[d] += norm
446 total_width -= norm
447
448 # Split the remaining spaces among each column equally, except the
449 # last one. And put the rest into the remainder column
450 cols -= 1
451 norm = total_width / cols
452 for d in xrange(0, cols):
453 columns[d] += norm
454 columns[remainder_column] += total_width - (cols * norm)
455 total_width = 0
456
457 return columns
458
459 @staticmethod
460 def _fmt_column_align_width(width):
461 if width < 0:
462 return (u"-", -width)
463 return (u"", width)
464
465 def _col_data(self, col_data):
466 assert len(col_data) == 2 or len(col_data) == 3
467 if len(col_data) == 2:
468 (val, width) = col_data
469 hibeg = hiend = ''
470 if len(col_data) == 3:
471 (val, width, highlight) = col_data
472 (hibeg, hiend) = self._highlight(highlight)
473 return (val, width, hibeg, hiend)
474
475 def fmtColumns(self, columns, msg=u'', end=u'', text_width=utf8_width):
476 """ Return a string for columns of data, which can overflow.
477 text_width parameter finds the width of columns, this defaults to
478 utf8 but can be changed to len() if you know it'll be fine. """
479
480 total_width = len(msg)
481 data = []
482 for col_data in columns[:-1]:
483 (val, width, hibeg, hiend) = self._col_data(col_data)
484
485 if not width: # Don't count this column, invisible text
486 msg += u"%s"
487 data.append(val)
488 continue
489
490 (align, width) = self._fmt_column_align_width(width)
491 val_width = text_width(val)
492 if val_width <= width:
493 # Don't use utf8_width_fill() because it sucks performance
494 # wise for 1,000s of rows. Also allows us to use len(), when
495 # we can.
496 msg += u"%s%s%s%s "
497 if (align == u'-'):
498 data.extend([hibeg, val, " " * (width - val_width), hiend])
499 else:
500 data.extend([hibeg, " " * (width - val_width), val, hiend])
501 else:
502 msg += u"%s%s%s\n" + " " * (total_width + width + 1)
503 data.extend([hibeg, val, hiend])
504 total_width += width
505 total_width += 1
506 (val, width, hibeg, hiend) = self._col_data(columns[-1])
507 (align, width) = self._fmt_column_align_width(width)
508 val = utf8_width_fill(val, width, left=(align == u'-'),
509 prefix=hibeg, suffix=hiend)
510 msg += u"%%s%s" % end
511 data.append(val)
512 return msg % tuple(data)
513
514 def simpleList(self, pkg, ui_overflow=False, indent='', highlight=False,
515 columns=None):
516 """ Simple to use function to print a pkg as a line. """
517
518 if columns is None:
519 columns = (-40, -22, -16) # Old default
520 ver = pkg.printVer()
521 na = '%s%s.%s' % (indent, pkg.name, pkg.arch)
522 hi_cols = [highlight, 'normal', 'normal']
523 rid = pkg.ui_from_repo
524 columns = zip((na, ver, rid), columns, hi_cols)
525 print self.fmtColumns(columns, text_width=len)
526
527 def simpleEnvraList(self, pkg, ui_overflow=False,
528 indent='', highlight=False, columns=None):
529 """ Simple to use function to print a pkg as a line, with the pkg
530 itself in envra format so it can be pased to list/install/etc. """
531
532 if columns is None:
533 columns = (-63, -16) # Old default
534 envra = '%s%s' % (indent, str(pkg))
535 hi_cols = [highlight, 'normal', 'normal']
536 rid = pkg.ui_from_repo
537 columns = zip((envra, rid), columns, hi_cols)
538 print self.fmtColumns(columns, text_width=len)
539
540 def fmtKeyValFill(self, key, val):
541 """ Return a key value pair in the common two column output format. """
542 val = to_str(val)
543 keylen = utf8_width(key)
544 cols = self.term.columns
545 nxt = ' ' * (keylen - 2) + ': '
546 ret = utf8_text_fill(val, width=cols,
547 initial_indent=key, subsequent_indent=nxt)
548 if ret.count("\n") > 1 and keylen > (cols / 3):
549 # If it's big, redo it again with a smaller subsequent off
550 ret = utf8_text_fill(val, width=cols,
551 initial_indent=key,
552 subsequent_indent=' ...: ')
553 return ret
554
555 def fmtSection(self, name, fill='='):
556 name = to_str(name)
557 cols = self.term.columns - 2
558 name_len = utf8_width(name)
559 if name_len >= (cols - 4):
560 beg = end = fill * 2
561 else:
562 beg = fill * ((cols - name_len) / 2)
563 end = fill * (cols - name_len - len(beg))
564
565 return "%s %s %s" % (beg, name, end)
566
567 def _enc(self, s):
568 """Get the translated version from specspo and ensure that
569 it's actually encoded in UTF-8."""
570 s = to_utf8(s)
571 if len(s) > 0:
572 for d in self.i18ndomains:
573 t = gettext.dgettext(d, s)
574 if t != s:
575 s = t
576 break
577 return to_unicode(s)
578
579 def infoOutput(self, pkg, highlight=False):
580 (hibeg, hiend) = self._highlight(highlight)
581 print _("Name : %s%s%s") % (hibeg, to_unicode(pkg.name), hiend)
582 print _("Arch : %s") % to_unicode(pkg.arch)
583 if pkg.epoch != "0":
584 print _("Epoch : %s") % to_unicode(pkg.epoch)
585 print _("Version : %s") % to_unicode(pkg.version)
586 print _("Release : %s") % to_unicode(pkg.release)
587 print _("Size : %s") % self.format_number(float(pkg.size))
588 print _("Repo : %s") % to_unicode(pkg.repoid)
589 if pkg.repoid == 'installed' and 'from_repo' in pkg.yumdb_info:
590 print _("From repo : %s") % to_unicode(pkg.yumdb_info.from_repo)
591 if self.verbose_logger.isEnabledFor(logginglevels.DEBUG_3):
592 print _("Committer : %s") % to_unicode(pkg.committer)
593 print _("Committime : %s") % time.ctime(pkg.committime)
594 print _("Buildtime : %s") % time.ctime(pkg.buildtime)
595 if hasattr(pkg, 'installtime'):
596 print _("Install time: %s") % time.ctime(pkg.installtime)
597 if pkg.repoid == 'installed':
598 uid = None
599 if 'installed_by' in pkg.yumdb_info:
600 try:
601 uid = int(pkg.yumdb_info.installed_by)
602 except ValueError: # In case int() fails
603 uid = None
604 print _("Installed by: %s") % self._pwd_ui_username(uid)
605 uid = None
606 if 'changed_by' in pkg.yumdb_info:
607 try:
608 uid = int(pkg.yumdb_info.changed_by)
609 except ValueError: # In case int() fails
610 uid = None
611 print _("Changed by : %s") % self._pwd_ui_username(uid)
612 print self.fmtKeyValFill(_("Summary : "), self._enc(pkg.summary))
613 if pkg.url:
614 print _("URL : %s") % to_unicode(pkg.url)
615 print self.fmtKeyValFill(_("License : "), to_unicode(pkg.license))
616 print self.fmtKeyValFill(_("Description : "),self._enc(pkg.description))
617 print ""
618
619 def updatesObsoletesList(self, uotup, changetype, columns=None):
620 """takes an updates or obsoletes tuple of pkgobjects and
621 returns a simple printed string of the output and a string
622 explaining the relationship between the tuple members"""
623 (changePkg, instPkg) = uotup
624
625 if columns is not None:
626 # New style, output all info. for both old/new with old indented
627 chi = self.conf.color_update_remote
628 if changePkg.repo.id != 'installed' and changePkg.verifyLocalPkg():
629 chi = self.conf.color_update_local
630 self.simpleList(changePkg, columns=columns, highlight=chi)
631 self.simpleList(instPkg, columns=columns, indent=' ' * 4,
632 highlight=self.conf.color_update_installed)
633 return
634
635 # Old style
636 c_compact = changePkg.compactPrint()
637 i_compact = '%s.%s' % (instPkg.name, instPkg.arch)
638 c_repo = changePkg.repoid
639 print '%-35.35s [%.12s] %.10s %-20.20s' % (c_compact, c_repo, changetype, i_compact)
640
641 def listPkgs(self, lst, description, outputType, highlight_na={},
642 columns=None, highlight_modes={}):
643 """outputs based on whatever outputType is. Current options:
644 'list' - simple pkg list
645 'info' - similar to rpm -qi output
646 ...also highlight_na can be passed, and we'll highlight
647 pkgs with (names, arch) in that set."""
648
649 if outputType in ['list', 'info']:
650 thingslisted = 0
651 if len(lst) > 0:
652 thingslisted = 1
653 print '%s' % description
654 for pkg in sorted(lst):
655 key = (pkg.name, pkg.arch)
656 highlight = False
657 if False: pass
658 elif key not in highlight_na:
659 highlight = highlight_modes.get('not in', 'normal')
660 elif pkg.verEQ(highlight_na[key]):
661 highlight = highlight_modes.get('=', 'normal')
662 elif pkg.verLT(highlight_na[key]):
663 highlight = highlight_modes.get('>', 'bold')
664 else:
665 highlight = highlight_modes.get('<', 'normal')
666
667 if outputType == 'list':
668 self.simpleList(pkg, ui_overflow=True,
669 highlight=highlight, columns=columns)
670 elif outputType == 'info':
671 self.infoOutput(pkg, highlight=highlight)
672 else:
673 pass
674
675 if thingslisted == 0:
676 return 1, ['No Packages to list']
677 return 0, []
678
679
680
681 def userconfirm(self):
682 """gets a yes or no from the user, defaults to No"""
683
684 yui = (to_unicode(_('y')), to_unicode(_('yes')))
685 nui = (to_unicode(_('n')), to_unicode(_('no')))
686 aui = (yui[0], yui[1], nui[0], nui[1])
687 while True:
688 try:
689 choice = raw_input(_('Is this ok [y/N]: '))
690 except UnicodeEncodeError:
691 raise
692 except UnicodeDecodeError:
693 raise
694 except:
695 choice = ''
696 choice = to_unicode(choice)
697 choice = choice.lower()
698 if len(choice) == 0 or choice in aui:
699 break
700 # If the enlish one letter names don't mix, allow them too
701 if u'y' not in aui and u'y' == choice:
702 choice = yui[0]
703 break
704 if u'n' not in aui and u'n' == choice:
705 break
706
707 if len(choice) == 0 or choice not in yui:
708 return False
709 else:
710 return True
711
712 def _cli_confirm_gpg_key_import(self, keydict):
713 # FIXME what should we be printing here?
714 return self.userconfirm()
715
716 def _group_names2aipkgs(self, pkg_names):
717 """ Convert pkg_names to installed pkgs or available pkgs, return
718 value is a dict on pkg.name returning (apkg, ipkg). """
719 ipkgs = self.rpmdb.searchNames(pkg_names)
720 apkgs = self.pkgSack.searchNames(pkg_names)
721 apkgs = packagesNewestByNameArch(apkgs)
722
723 # This is somewhat similar to doPackageLists()
724 pkgs = {}
725 for pkg in ipkgs:
726 pkgs[(pkg.name, pkg.arch)] = (None, pkg)
727 for pkg in apkgs:
728 key = (pkg.name, pkg.arch)
729 if key not in pkgs:
730 pkgs[(pkg.name, pkg.arch)] = (pkg, None)
731 elif pkg.verGT(pkgs[key][1]):
732 pkgs[(pkg.name, pkg.arch)] = (pkg, pkgs[key][1])
733
734 # Convert (pkg.name, pkg.arch) to pkg.name dict
735 ret = {}
736 for (apkg, ipkg) in pkgs.itervalues():
737 pkg = apkg or ipkg
738 ret.setdefault(pkg.name, []).append((apkg, ipkg))
739 return ret
740
741 def _calcDataPkgColumns(self, data, pkg_names, pkg_names2pkgs,
742 indent=' '):
743 for item in pkg_names:
744 if item not in pkg_names2pkgs:
745 continue
746 for (apkg, ipkg) in pkg_names2pkgs[item]:
747 pkg = ipkg or apkg
748 envra = utf8_width(str(pkg)) + utf8_width(indent)
749 rid = len(pkg.ui_from_repo)
750 for (d, v) in (('envra', envra), ('rid', rid)):
751 data[d].setdefault(v, 0)
752 data[d][v] += 1
753
754 def _displayPkgsFromNames(self, pkg_names, verbose, pkg_names2pkgs,
755 indent=' ', columns=None):
756 if not verbose:
757 for item in sorted(pkg_names):
758 print '%s%s' % (indent, item)
759 else:
760 for item in sorted(pkg_names):
761 if item not in pkg_names2pkgs:
762 print '%s%s' % (indent, item)
763 continue
764 for (apkg, ipkg) in sorted(pkg_names2pkgs[item],
765 key=lambda x: x[1] or x[0]):
766 if ipkg and apkg:
767 highlight = self.conf.color_list_installed_older
768 elif apkg:
769 highlight = self.conf.color_list_available_install
770 else:
771 highlight = False
772 self.simpleEnvraList(ipkg or apkg, ui_overflow=True,
773 indent=indent, highlight=highlight,
774 columns=columns)
775
776 def displayPkgsInGroups(self, group):
777 print _('\nGroup: %s') % group.ui_name
778
779 verb = self.verbose_logger.isEnabledFor(logginglevels.DEBUG_3)
780 if verb:
781 print _(' Group-Id: %s') % to_unicode(group.groupid)
782 pkg_names2pkgs = None
783 if verb:
784 pkg_names2pkgs = self._group_names2aipkgs(group.packages)
785 if group.ui_description:
786 print _(' Description: %s') % to_unicode(group.ui_description)
787 if group.langonly:
788 print _(' Language: %s') % group.langonly
789
790 sections = ((_(' Mandatory Packages:'), group.mandatory_packages),
791 (_(' Default Packages:'), group.default_packages),
792 (_(' Optional Packages:'), group.optional_packages),
793 (_(' Conditional Packages:'), group.conditional_packages))
794 columns = None
795 if verb:
796 data = {'envra' : {}, 'rid' : {}}
797 for (section_name, pkg_names) in sections:
798 self._calcDataPkgColumns(data, pkg_names, pkg_names2pkgs)
799 data = [data['envra'], data['rid']]
800 columns = self.calcColumns(data)
801 columns = (-columns[0], -columns[1])
802
803 for (section_name, pkg_names) in sections:
804 if len(pkg_names) > 0:
805 print section_name
806 self._displayPkgsFromNames(pkg_names, verb, pkg_names2pkgs,
807 columns=columns)
808
809 def depListOutput(self, results):
810 """take a list of findDeps results and 'pretty print' the output"""
811
812 verb = self.verbose_logger.isEnabledFor(logginglevels.DEBUG_3)
813 for pkg in sorted(results):
814 print _("package: %s") % pkg.compactPrint()
815 if len(results[pkg]) == 0:
816 print _(" No dependencies for this package")
817 continue
818
819 for req in sorted(results[pkg]):
820 reqlist = results[pkg][req]
821 print _(" dependency: %s") % prco_tuple_to_string(req)
822 if not reqlist:
823 print _(" Unsatisfied dependency")
824 continue
825
826 seen = {}
827 for po in reversed(sorted(reqlist)):
828 key = (po.name, po.arch)
829 if not verb and key in seen:
830 continue
831 seen[key] = po
832 print " provider: %s" % po.compactPrint()
833
834 def format_number(self, number, SI=0, space=' '):
835 """Turn numbers into human-readable metric-like numbers"""
836 symbols = [ ' ', # (none)
837 'k', # kilo
838 'M', # mega
839 'G', # giga
840 'T', # tera
841 'P', # peta
842 'E', # exa
843 'Z', # zetta
844 'Y'] # yotta
845
846 if SI: step = 1000.0
847 else: step = 1024.0
848
849 thresh = 999
850 depth = 0
851 max_depth = len(symbols) - 1
852
853 # we want numbers between 0 and thresh, but don't exceed the length
854 # of our list. In that event, the formatting will be screwed up,
855 # but it'll still show the right number.
856 while number > thresh and depth < max_depth:
857 depth = depth + 1
858 number = number / step
859
860 if type(number) == type(1) or type(number) == type(1L):
861 format = '%i%s%s'
862 elif number < 9.95:
863 # must use 9.95 for proper sizing. For example, 9.99 will be
864 # rounded to 10.0 with the .1f format string (which is too long)
865 format = '%.1f%s%s'
866 else:
867 format = '%.0f%s%s'
868
869 return(format % (float(number or 0), space, symbols[depth]))
870
871 @staticmethod
872 def format_time(seconds, use_hours=0):
873 return urlgrabber.progress.format_time(seconds, use_hours)
874
875 def matchcallback(self, po, values, matchfor=None, verbose=None,
876 highlight=None):
877 """ Output search/provides type callback matches. po is the pkg object,
878 values are the things in the po that we've matched.
879 If matchfor is passed, all the strings in that list will be
880 highlighted within the output.
881 verbose overrides logginglevel, if passed. """
882
883 if self.conf.showdupesfromrepos:
884 msg = '%s : ' % po
885 else:
886 msg = '%s.%s : ' % (po.name, po.arch)
887 msg = self.fmtKeyValFill(msg, self._enc(po.summary))
888 if matchfor:
889 if highlight is None:
890 highlight = self.conf.color_search_match
891 msg = self._sub_highlight(msg, highlight, matchfor,ignore_case=True)
892
893 print msg
894
895 if verbose is None:
896 verbose = self.verbose_logger.isEnabledFor(logginglevels.DEBUG_3)
897 if not verbose:
898 return
899
900 print _("Repo : %s") % po.ui_from_repo
901 done = False
902 for item in yum.misc.unique(values):
903 item = to_utf8(item)
904 if to_utf8(po.name) == item or to_utf8(po.summary) == item:
905 continue # Skip double name/summary printing
906
907 if not done:
908 print _('Matched from:')
909 done = True
910 can_overflow = True
911 if False: pass
912 elif to_utf8(po.description) == item:
913 key = _("Description : ")
914 item = self._enc(item)
915 elif to_utf8(po.url) == item:
916 key = _("URL : %s")
917 can_overflow = False
918 elif to_utf8(po.license) == item:
919 key = _("License : %s")
920 can_overflow = False
921 elif item.startswith("/"):
922 key = _("Filename : %s")
923 item = self._enc(item)
924 can_overflow = False
925 else:
926 key = _("Other : ")
927
928 if matchfor:
929 item = self._sub_highlight(item, highlight, matchfor,
930 ignore_case=True)
931 if can_overflow:
932 print self.fmtKeyValFill(key, to_unicode(item))
933 else:
934 print key % item
935 print '\n\n'
936
937 def matchcallback_verbose(self, po, values, matchfor=None):
938 return self.matchcallback(po, values, matchfor, verbose=True)
939
940 def reportDownloadSize(self, packages, installonly=False):
941 """Report the total download size for a set of packages"""
942 totsize = 0
943 locsize = 0
944 insize = 0
945 error = False
946 for pkg in packages:
947 # Just to be on the safe side, if for some reason getting
948 # the package size fails, log the error and don't report download
949 # size
950 try:
951 size = int(pkg.size)
952 totsize += size
953 try:
954 if pkg.verifyLocalPkg():
955 locsize += size
956 except:
957 pass
958
959 if not installonly:
960 continue
961
962 try:
963 size = int(pkg.installedsize)
964 except:
965 pass
966 insize += size
967 except:
968 error = True
969 self.logger.error(_('There was an error calculating total download size'))
970 break
971
972 if (not error):
973 if locsize:
974 self.verbose_logger.log(logginglevels.INFO_1, _("Total size: %s"),
975 self.format_number(totsize))
976 if locsize != totsize:
977 self.verbose_logger.log(logginglevels.INFO_1, _("Total download size: %s"),
978 self.format_number(totsize - locsize))
979 if installonly:
980 self.verbose_logger.log(logginglevels.INFO_1,
981 _("Installed size: %s"),
982 self.format_number(insize))
983
984 def reportRemoveSize(self, packages):
985 """Report the total size of packages being removed. """
986 totsize = 0
987 error = False
988 for pkg in packages:
989 # Just to be on the safe side, if for some reason getting
990 # the package size fails, log the error and don't report download
991 # size
992 try:
993 size = int(pkg.size)
994 totsize += size
995 except:
996 error = True
997 self.logger.error(_('There was an error calculating installed size'))
998 break
999 if (not error):
1000 self.verbose_logger.log(logginglevels.INFO_1,
1001 _("Installed size: %s"),
1002 self.format_number(totsize))
1003
1004 def listTransaction(self):
1005 """returns a string rep of the transaction in an easy-to-read way."""
1006
1007 self.tsInfo.makelists(True, True)
1008 pkglist_lines = []
1009 data = {'n' : {}, 'v' : {}, 'r' : {}}
1010 a_wid = 0 # Arch can't get "that big" ... so always use the max.
1011
1012 def _add_line(lines, data, a_wid, po, obsoletes=[]):
1013 (n,a,e,v,r) = po.pkgtup
1014 evr = po.printVer()
1015 repoid = po.ui_from_repo
1016 pkgsize = float(po.size)
1017 size = self.format_number(pkgsize)
1018
1019 if a is None: # gpgkeys are weird
1020 a = 'noarch'
1021
1022 # none, partial, full?
1023 if po.repo.id == 'installed':
1024 hi = self.conf.color_update_installed
1025 elif po.verifyLocalPkg():
1026 hi = self.conf.color_update_local
1027 else:
1028 hi = self.conf.color_update_remote
1029 lines.append((n, a, evr, repoid, size, obsoletes, hi))
1030 # Create a dict of field_length => number of packages, for
1031 # each field.
1032 for (d, v) in (("n",len(n)), ("v",len(evr)), ("r",len(repoid))):
1033 data[d].setdefault(v, 0)
1034 data[d][v] += 1
1035 if a_wid < len(a): # max() is only in 2.5.z
1036 a_wid = len(a)
1037 return a_wid
1038
1039 for (action, pkglist) in [(_('Installing'), self.tsInfo.installed),
1040 (_('Updating'), self.tsInfo.updated),
1041 (_('Removing'), self.tsInfo.removed),
1042 (_('Reinstalling'), self.tsInfo.reinstalled),
1043 (_('Downgrading'), self.tsInfo.downgraded),
1044 (_('Installing for dependencies'), self.tsInfo.depinstalled),
1045 (_('Updating for dependencies'), self.tsInfo.depupdated),
1046 (_('Removing for dependencies'), self.tsInfo.depremoved)]:
1047 lines = []
1048 for txmbr in pkglist:
1049 a_wid = _add_line(lines, data, a_wid, txmbr.po, txmbr.obsoletes)
1050
1051 pkglist_lines.append((action, lines))
1052
1053 for (action, pkglist) in [(_('Skipped (dependency problems)'),
1054 self.skipped_packages),
1055 (_('Not installed'), self._not_found_i.values()),
1056 (_('Not available'), self._not_found_a.values())]:
1057 lines = []
1058 for po in pkglist:
1059 a_wid = _add_line(lines, data, a_wid, po)
1060
1061 pkglist_lines.append((action, lines))
1062
1063 if not data['n']:
1064 return u''
1065 else:
1066 data = [data['n'], {}, data['v'], data['r'], {}]
1067 columns = [1, a_wid, 1, 1, 5]
1068 columns = self.calcColumns(data, indent=" ", columns=columns,
1069 remainder_column=2)
1070 (n_wid, a_wid, v_wid, r_wid, s_wid) = columns
1071 assert s_wid == 5
1072
1073 out = [u"""
1074%s
1075%s
1076%s
1077""" % ('=' * self.term.columns,
1078 self.fmtColumns(((_('Package'), -n_wid), (_('Arch'), -a_wid),
1079 (_('Version'), -v_wid), (_('Repository'), -r_wid),
1080 (_('Size'), s_wid)), u" "),
1081 '=' * self.term.columns)]
1082
1083 for (action, lines) in pkglist_lines:
1084 if lines:
1085 totalmsg = u"%s:\n" % action
1086 for (n, a, evr, repoid, size, obsoletes, hi) in lines:
1087 columns = ((n, -n_wid, hi), (a, -a_wid),
1088 (evr, -v_wid), (repoid, -r_wid), (size, s_wid))
1089 msg = self.fmtColumns(columns, u" ", u"\n")
1090 hibeg, hiend = self._highlight(self.conf.color_update_installed)
1091 for obspo in sorted(obsoletes):
1092 appended = _(' replacing %s%s%s.%s %s\n')
1093 appended %= (hibeg, obspo.name, hiend,
1094 obspo.arch, obspo.printVer())
1095 msg = msg+appended
1096 totalmsg = totalmsg + msg
1097
1098 if lines:
1099 out.append(totalmsg)
1100
1101 out.append(_("""
1102Transaction Summary
1103%s
1104""") % ('=' * self.term.columns))
1105 for action, count in (
1106 (_('Install'), len(self.tsInfo.installed) + len(self.tsInfo.depinstalled)),
1107 (_('Upgrade'), len(self.tsInfo.updated) + len(self.tsInfo.depupdated)),
1108 (_('Remove'), len(self.tsInfo.removed) + len(self.tsInfo.depremoved)),
1109 (_('Reinstall'), len(self.tsInfo.reinstalled)),
1110 (_('Downgrade'), len(self.tsInfo.downgraded)),
1111 ):
1112 if count: out.append('%-9s %5d %s\n' % (
1113 action, count, P_('Package', 'Packages', count),
1114 ))
1115 return ''.join(out)
1116
1117 def postTransactionOutput(self):
1118 out = ''
1119
1120 self.tsInfo.makelists()
1121
1122 # Works a bit like calcColumns, but we never overflow a column we just
1123 # have a dynamic number of columns.
1124 def _fits_in_cols(msgs, num):
1125 """ Work out how many columns we can use to display stuff, in
1126 the post trans output. """
1127 if len(msgs) < num:
1128 return []
1129
1130 left = self.term.columns - ((num - 1) + 2)
1131 if left <= 0:
1132 return []
1133
1134 col_lens = [0] * num
1135 col = 0
1136 for msg in msgs:
1137 if len(msg) > col_lens[col]:
1138 diff = (len(msg) - col_lens[col])
1139 if left <= diff:
1140 return []
1141 left -= diff
1142 col_lens[col] = len(msg)
1143 col += 1
1144 col %= len(col_lens)
1145
1146 for col in range(len(col_lens)):
1147 col_lens[col] += left / num
1148 col_lens[col] *= -1
1149 return col_lens
1150
1151 for (action, pkglist) in [(_('Removed'), self.tsInfo.removed),
1152 (_('Dependency Removed'), self.tsInfo.depremoved),
1153 (_('Installed'), self.tsInfo.installed),
1154 (_('Dependency Installed'), self.tsInfo.depinstalled),
1155 (_('Updated'), self.tsInfo.updated),
1156 (_('Dependency Updated'), self.tsInfo.depupdated),
1157 (_('Skipped (dependency problems)'), self.skipped_packages),
1158 (_('Replaced'), self.tsInfo.obsoleted),
1159 (_('Failed'), self.tsInfo.failed)]:
1160 msgs = []
1161 if len(pkglist) > 0:
1162 out += '\n%s:\n' % action
1163 for txmbr in pkglist:
1164 (n,a,e,v,r) = txmbr.pkgtup
1165 msg = "%s.%s %s:%s-%s" % (n,a,e,v,r)
1166 msgs.append(msg)
1167 for num in (8, 7, 6, 5, 4, 3, 2):
1168 cols = _fits_in_cols(msgs, num)
1169 if cols:
1170 break
1171 if not cols:
1172 cols = [-(self.term.columns - 2)]
1173 while msgs:
1174 current_msgs = msgs[:len(cols)]
1175 out += ' '
1176 out += self.fmtColumns(zip(current_msgs, cols), end=u'\n')
1177 msgs = msgs[len(cols):]
1178
1179 return out
1180
1181 def setupProgressCallbacks(self):
1182 """sets up the progress callbacks and various
1183 output bars based on debug level"""
1184
1185 # if we're below 2 on the debug level we don't need to be outputting
1186 # progress bars - this is hacky - I'm open to other options
1187 # One of these is a download
1188 if self.conf.debuglevel < 2 or not sys.stdout.isatty():
1189 progressbar = None
1190 callback = None
1191 else:
1192 progressbar = YumTextMeter(fo=sys.stdout)
1193 callback = CacheProgressCallback()
1194
1195 # setup our failure report for failover
1196 freport = (self.failureReport,(),{})
1197 failure_callback = freport
1198
1199 # setup callback for CTRL-C's
1200 interrupt_callback = self.interrupt_callback
1201 if hasattr(self, 'prerepoconf'):
1202 self.prerepoconf.progressbar = progressbar
1203 self.prerepoconf.callback = callback
1204 self.prerepoconf.failure_callback = failure_callback
1205 self.prerepoconf.interrupt_callback = interrupt_callback
1206 else:
1207 # Just in case some API user decides to do self.repos before
1208 # calling us.
1209 self.repos.setProgressBar(progressbar)
1210 self.repos.callback = callback
1211 self.repos.setFailureCallback(failure_callback)
1212 self.repos.setInterruptCallback(interrupt_callback)
1213
1214 # setup our depsolve progress callback
1215 dscb = DepSolveProgressCallBack(weakref(self))
1216 self.dsCallback = dscb
1217
1218 def setupProgessCallbacks(self):
1219 # api purposes only to protect the typo
1220 self.setupProgressCallbacks()
1221
1222 def setupKeyImportCallbacks(self):
1223 confirm_func = self._cli_confirm_gpg_key_import
1224 gpg_import_func = self.getKeyForRepo
1225 gpgca_import_func = self.getCAKeyForRepo
1226 if hasattr(self, 'prerepoconf'):
1227 self.prerepoconf.confirm_func = confirm_func
1228 self.prerepoconf.gpg_import_func = gpg_import_func
1229 self.prerepoconf.gpgca_import_func = gpgca_import_func
1230 else:
1231 self.repos.confirm_func = confirm_func
1232 self.repos.gpg_import_func = gpg_import_func
1233 self.repos.gpgca_import_func = gpgca_import_func
1234
1235 def interrupt_callback(self, cbobj):
1236 '''Handle CTRL-C's during downloads
1237
1238 If a CTRL-C occurs a URLGrabError will be raised to push the download
1239 onto the next mirror.
1240
1241 If two CTRL-C's occur in quick succession then yum will exit.
1242
1243 @param cbobj: urlgrabber callback obj
1244 '''
1245 delta_exit_chk = 2.0 # Delta between C-c's so we treat as exit
1246 delta_exit_str = _("two") # Human readable version of above
1247
1248 now = time.time()
1249
1250 if not self._last_interrupt:
1251 hibeg = self.term.MODE['bold']
1252 hiend = self.term.MODE['normal']
1253 # For translators: This is output like:
1254# Current download cancelled, interrupt (ctrl-c) again within two seconds
1255# to exit.
1256 # Where "interupt (ctrl-c) again" and "two" are highlighted.
1257 msg = _("""
1258 Current download cancelled, %sinterrupt (ctrl-c) again%s within %s%s%s seconds
1259to exit.
1260""") % (hibeg, hiend, hibeg, delta_exit_str, hiend)
1261 self.verbose_logger.log(logginglevels.INFO_2, msg)
1262 elif now - self._last_interrupt < delta_exit_chk:
1263 # Two quick CTRL-C's, quit
1264 raise KeyboardInterrupt
1265
1266 # Go to next mirror
1267 self._last_interrupt = now
1268 raise URLGrabError(15, _('user interrupt'))
1269
1270 def download_callback_total_cb(self, remote_pkgs, remote_size,
1271 download_start_timestamp):
1272 if len(remote_pkgs) <= 1:
1273 return
1274 if not hasattr(urlgrabber.progress, 'TerminalLine'):
1275 return
1276
1277 tl = urlgrabber.progress.TerminalLine(8)
1278 self.verbose_logger.log(logginglevels.INFO_2, "-" * tl.rest())
1279 dl_time = time.time() - download_start_timestamp
1280 if dl_time <= 0: # This stops divide by zero, among other problems
1281 dl_time = 0.01
1282 ui_size = tl.add(' | %5sB' % self.format_number(remote_size))
1283 ui_time = tl.add(' %9s' % self.format_time(dl_time))
1284 ui_end = tl.add(' ' * 5)
1285 ui_bs = tl.add(' %5sB/s' % self.format_number(remote_size / dl_time))
1286 msg = "%s%s%s%s%s" % (utf8_width_fill(_("Total"), tl.rest(), tl.rest()),
1287 ui_bs, ui_size, ui_time, ui_end)
1288 self.verbose_logger.log(logginglevels.INFO_2, msg)
1289
1290 def _history_uiactions(self, hpkgs):
1291 actions = set()
1292 count = 0
1293 for hpkg in hpkgs:
1294 st = hpkg.state
1295 if st == 'True-Install':
1296 st = 'Install'
1297 if st == 'Dep-Install': # Mask these at the higher levels
1298 st = 'Install'
1299 if st == 'Obsoleted': # This is just a UI tweak, as we can't have
1300 # just one but we need to count them all.
1301 st = 'Obsoleting'
1302 if st in ('Install', 'Update', 'Erase', 'Reinstall', 'Downgrade',
1303 'Obsoleting'):
1304 actions.add(st)
1305 count += 1
1306 assert len(actions) <= 6
1307 if len(actions) > 1:
1308 large2small = {'Install' : _('I'),
1309 'Obsoleting' : _('O'),
1310 'Erase' : _('E'),
1311 'Reinstall' : _('R'),
1312 'Downgrade' : _('D'),
1313 'Update' : _('U'),
1314 }
1315 return count, ", ".join([large2small[x] for x in sorted(actions)])
1316
1317 # So empty transactions work, although that "shouldn't" really happen
1318 return count, "".join(list(actions))
1319
1320 def _pwd_ui_username(self, uid, limit=None):
1321 if type(uid) == type([]):
1322 return [self._pwd_ui_username(u, limit) for u in uid]
1323
1324 # loginuid is set to -1 (0xFFFF_FFFF) on init, in newer kernels.
1325 # loginuid is set to INT_MAX (0x7FFF_FFFF) on init, in older kernels.
1326 if uid is None or uid in (0xFFFFFFFF, 0x7FFFFFFF):
1327 loginid = _("<unset>")
1328 name = _("System") + " " + loginid
1329 if limit is not None and len(name) > limit:
1330 name = loginid
1331 return to_unicode(name)
1332
1333 def _safe_split_0(text, *args):
1334 """ Split gives us a [0] for everything _but_ '', this function
1335 returns '' in that case. """
1336 ret = text.split(*args)
1337 if not ret:
1338 return ''
1339 return ret[0]
1340
1341 try:
1342 user = pwd.getpwuid(uid)
1343 fullname = _safe_split_0(user.pw_gecos, ';', 2)
1344 name = "%s <%s>" % (fullname, user.pw_name)
1345 if limit is not None and len(name) > limit:
1346 name = "%s ... <%s>" % (_safe_split_0(fullname), user.pw_name)
1347 if len(name) > limit:
1348 name = "<%s>" % user.pw_name
1349 return to_unicode(name)
1350 except KeyError:
1351 return to_unicode(str(uid))
1352
1353 @staticmethod
1354 def _historyRangeRTIDs(old, tid):
1355 ''' Convert a user "TID" string of 2..4 into: (2, 4). '''
1356 def str2int(x):
1357 try:
1358 if x == 'last' or x.startswith('last-'):
1359 tid = old.tid
1360 if x.startswith('last-'):
1361 off = int(x[len('last-'):])
1362 if off <= 0:
1363 int("z")
1364 tid -= off
1365 return tid
1366 return int(x)
1367 except ValueError:
1368 return None
1369
1370 if '..' not in tid:
1371 return None
1372 btid, etid = tid.split('..', 2)
1373 btid = str2int(btid)
1374 if btid > old.tid:
1375 return None
1376 elif btid <= 0:
1377 return None
1378 etid = str2int(etid)
1379 if etid > old.tid:
1380 return None
1381
1382 if btid is None or etid is None:
1383 return None
1384
1385 # Have a range ... do a "merged" transaction.
1386 if btid > etid:
1387 btid, etid = etid, btid
1388 return (btid, etid)
1389
1390 def _historyRangeTIDs(self, rtids):
1391 ''' Convert a list of ranged tid typles into all the tids needed, Eg.
1392 [(2,4), (6,8)] == [2, 3, 4, 6, 7, 8]. '''
1393 tids = set()
1394 last_end = -1 # This just makes displaying it easier...
1395 for mtid in sorted(rtids):
1396 if mtid[0] < last_end:
1397 self.logger.warn(_('Skipping merged transaction %d to %d, as it overlaps' % (mtid[0], mtid[1])))
1398 continue # Don't do overlapping
1399 last_end = mtid[1]
1400 for num in range(mtid[0], mtid[1] + 1):
1401 tids.add(num)
1402 return tids
1403
1404 def _history_list_transactions(self, extcmds):
1405 old = self.history.last()
1406 if old is None:
1407 self.logger.critical(_('No transactions'))
1408 return None, None
1409
1410 tids = set()
1411 pats = []
1412 usertids = extcmds[1:]
1413 printall = False
1414 if usertids:
1415 printall = True
1416 if usertids[0] == 'all':
1417 usertids.pop(0)
1418 for tid in usertids:
1419 try:
1420 int(tid)
1421 tids.add(tid)
1422 except ValueError:
1423 rtid = self._historyRangeRTIDs(old, tid)
1424 if rtid:
1425 tids.update(self._historyRangeTIDs([rtid]))
1426 continue
1427 pats.append(tid)
1428 if pats:
1429 tids.update(self.history.search(pats))
1430
1431 if not tids and usertids:
1432 self.logger.critical(_('Bad transaction IDs, or package(s), given'))
1433 return None, None
1434 return tids, printall
1435
1436 def historyListCmd(self, extcmds):
1437 """ Shows the user a list of data about the history. """
1438
1439 tids, printall = self._history_list_transactions(extcmds)
1440 if tids is None:
1441 return 1, ['Failed history list']
1442
1443 limit = 20
1444 if printall:
1445 limit = None
1446
1447 old_tids = self.history.old(tids, limit=limit)
1448 done = 0
1449 if self.conf.history_list_view == 'users':
1450 uids = [1,2]
1451 elif self.conf.history_list_view == 'commands':
1452 uids = [1]
1453 else:
1454 assert self.conf.history_list_view == 'single-user-commands'
1455 uids = set()
1456 blanks = 0
1457 for old in old_tids:
1458 if not printall and done >= limit:
1459 break
1460
1461 done += 1
1462 if old.cmdline is None:
1463 blanks += 1
1464 uids.add(old.loginuid)
1465 if len(uids) == 1 and blanks > (done / 2):
1466 uids.add('blah')
1467
1468 fmt = "%s | %s | %s | %s | %s"
1469 if len(uids) == 1:
1470 name = _("Command line")
1471 else:
1472 name = _("Login user")
1473 print fmt % (utf8_width_fill(_("ID"), 6, 6),
1474 utf8_width_fill(name, 24, 24),
1475 utf8_width_fill(_("Date and time"), 16, 16),
1476 utf8_width_fill(_("Action(s)"), 14, 14),
1477 utf8_width_fill(_("Altered"), 7, 7))
1478 print "-" * 79
1479 fmt = "%6u | %s | %-16.16s | %s | %4u"
1480 done = 0
1481 for old in old_tids:
1482 if not printall and done >= limit:
1483 break
1484
1485 done += 1
1486 if len(uids) == 1:
1487 name = old.cmdline or ''
1488 else:
1489 name = self._pwd_ui_username(old.loginuid, 24)
1490 tm = time.strftime("%Y-%m-%d %H:%M",
1491 time.localtime(old.beg_timestamp))
1492 num, uiacts = self._history_uiactions(old.trans_data)
1493 name = utf8_width_fill(name, 24, 24)
1494 uiacts = utf8_width_fill(uiacts, 14, 14)
1495 rmark = lmark = ' '
1496 if old.return_code is None:
1497 rmark = lmark = '*'
1498 elif old.return_code:
1499 rmark = lmark = '#'
1500 # We don't check .errors, because return_code will be non-0
1501 elif old.output:
1502 rmark = lmark = 'E'
1503 elif old.rpmdb_problems:
1504 rmark = lmark = 'P'
1505 elif old.trans_skip:
1506 rmark = lmark = 's'
1507 if old.altered_lt_rpmdb:
1508 rmark = '<'
1509 if old.altered_gt_rpmdb:
1510 lmark = '>'
1511 print fmt % (old.tid, name, tm, uiacts, num), "%s%s" % (lmark,rmark)
1512 lastdbv = self.history.last()
1513 if lastdbv is None:
1514 self._rpmdb_warn_checks(warn=False)
1515 else:
1516 # If this is the last transaction, is good and it doesn't
1517 # match the current rpmdb ... then mark it as bad.
1518 rpmdbv = self.rpmdb.simpleVersion(main_only=True)[0]
1519 if lastdbv.end_rpmdbversion != rpmdbv:
1520 self._rpmdb_warn_checks()
1521
1522 def _history_get_transactions(self, extcmds):
1523 if len(extcmds) < 2:
1524 self.logger.critical(_('No transaction ID given'))
1525 return None
1526
1527 tids = []
1528 last = None
1529 for extcmd in extcmds[1:]:
1530 try:
1531 if extcmd == 'last' or extcmd.startswith('last-'):
1532 if last is None:
1533 cto = False
1534 last = self.history.last(complete_transactions_only=cto)
1535 if last is None:
1536 int("z")
1537 tid = last.tid
1538 if extcmd.startswith('last-'):
1539 off = int(extcmd[len('last-'):])
1540 if off <= 0:
1541 int("z")
1542 tid -= off
1543 tids.append(str(tid))
1544 continue
1545
1546 if int(extcmd) <= 0:
1547 int("z")
1548 tids.append(extcmd)
1549 except ValueError:
1550 self.logger.critical(_('Bad transaction ID given'))
1551 return None
1552
1553 old = self.history.old(tids)
1554 if not old:
1555 self.logger.critical(_('Not found given transaction ID'))
1556 return None
1557 return old
1558 def _history_get_transaction(self, extcmds):
1559 old = self._history_get_transactions(extcmds)
1560 if old is None:
1561 return None
1562 if len(old) > 1:
1563 self.logger.critical(_('Found more than one transaction ID!'))
1564 return old[0]
1565
1566 def historyInfoCmd(self, extcmds):
1567 def str2int(x):
1568 try:
1569 return int(x)
1570 except ValueError:
1571 return None
1572
1573 tids = set()
1574 mtids = set()
1575 pats = []
1576 old = self.history.last()
1577 if old is None:
1578 self.logger.critical(_('No transactions'))
1579 return 1, ['Failed history info']
1580
1581 for tid in extcmds[1:]:
1582 if self._historyRangeRTIDs(old, tid):
1583 # Have a range ... do a "merged" transaction.
1584 mtids.add(self._historyRangeRTIDs(old, tid))
1585 continue
1586 elif str2int(tid) is not None:
1587 tids.add(str2int(tid))
1588 continue
1589 pats.append(tid)
1590 if pats:
1591 tids.update(self.history.search(pats))
1592 utids = tids.copy()
1593 if mtids:
1594 mtids = sorted(mtids)
1595 tids.update(self._historyRangeTIDs(mtids))
1596
1597 if not tids and len(extcmds) < 2:
1598 old = self.history.last(complete_transactions_only=False)
1599 if old is not None:
1600 tids.add(old.tid)
1601 utids.add(old.tid)
1602
1603 if not tids:
1604 self.logger.critical(_('No transaction ID, or package, given'))
1605 return 1, ['Failed history info']
1606
1607 lastdbv = self.history.last()
1608 if lastdbv is not None:
1609 lasttid = lastdbv.tid
1610 lastdbv = lastdbv.end_rpmdbversion
1611
1612 done = False
1613 bmtid, emtid = -1, -1
1614 mobj = None
1615 if mtids:
1616 bmtid, emtid = mtids.pop(0)
1617 for tid in self.history.old(tids):
1618 if lastdbv is not None and tid.tid == lasttid:
1619 # If this is the last transaction, is good and it doesn't
1620 # match the current rpmdb ... then mark it as bad.
1621 rpmdbv = self.rpmdb.simpleVersion(main_only=True)[0]
1622 if lastdbv != rpmdbv:
1623 tid.altered_gt_rpmdb = True
1624 lastdbv = None
1625
1626 if tid.tid >= bmtid and tid.tid <= emtid:
1627 if mobj is None:
1628 mobj = yum.history.YumMergedHistoryTransaction(tid)
1629 else:
1630 mobj.merge(tid)
1631 elif mobj is not None:
1632 if done:
1633 print "-" * 79
1634 done = True
1635
1636 self._historyInfoCmd(mobj)
1637 mobj = None
1638 if mtids:
1639 bmtid, emtid = mtids.pop(0)
1640 if tid.tid >= bmtid and tid.tid <= emtid:
1641 mobj = yum.history.YumMergedHistoryTransaction(tid)
1642
1643 if tid.tid in utids:
1644 if done:
1645 print "-" * 79
1646 done = True
1647
1648 self._historyInfoCmd(tid, pats)
1649
1650 if mobj is not None:
1651 if done:
1652 print "-" * 79
1653
1654 self._historyInfoCmd(mobj)
1655
1656 def _hpkg2from_repo(self, hpkg):
1657 """ Given a pkg, find the ipkg.ui_from_repo ... if none, then
1658 get an apkg. ... and put a ? in there. """
1659 ipkgs = self.rpmdb.searchPkgTuple(hpkg.pkgtup)
1660 if not ipkgs:
1661 apkgs = self.pkgSack.searchPkgTuple(hpkg.pkgtup)
1662 if not apkgs:
1663 return '?'
1664 return '@?' + str(apkgs[0].repoid)
1665
1666 return ipkgs[0].ui_from_repo
1667
1668 def _historyInfoCmd(self, old, pats=[]):
1669 name = self._pwd_ui_username(old.loginuid)
1670
1671 _pkg_states_installed = {'i' : _('Installed'), 'e' : _('Erased'),
1672 'o' : _('Updated'), 'n' : _('Downgraded')}
1673 _pkg_states_available = {'i' : _('Installed'), 'e' : _('Not installed'),
1674 'o' : _('Older'), 'n' : _('Newer')}
1675 # max() only in 2.5.z
1676 maxlen = sorted([len(x) for x in (_pkg_states_installed.values() +
1677 _pkg_states_available.values())])[-1]
1678 _pkg_states_installed['maxlen'] = maxlen
1679 _pkg_states_available['maxlen'] = maxlen
1680 def _simple_pkg(pkg, prefix_len, was_installed=False, highlight=False,
1681 pkg_max_len=0):
1682 prefix = " " * prefix_len
1683 if was_installed:
1684 _pkg_states = _pkg_states_installed
1685 else:
1686 _pkg_states = _pkg_states_available
1687 state = _pkg_states['i']
1688 ipkgs = self.rpmdb.searchNames([hpkg.name])
1689 ipkgs.sort()
1690 if not ipkgs:
1691 state = _pkg_states['e']
1692 elif hpkg.pkgtup in (ipkg.pkgtup for ipkg in ipkgs):
1693 pass
1694 elif ipkgs[-1] > hpkg:
1695 state = _pkg_states['o']
1696 elif ipkgs[0] < hpkg:
1697 state = _pkg_states['n']
1698 else:
1699 assert False, "Impossible, installed not newer and not older"
1700 if highlight:
1701 (hibeg, hiend) = self._highlight('bold')
1702 else:
1703 (hibeg, hiend) = self._highlight('normal')
1704 state = utf8_width_fill(state, _pkg_states['maxlen'])
1705 print "%s%s%s%s %-*s %s" % (prefix, hibeg, state, hiend,
1706 pkg_max_len, hpkg,
1707 self._hpkg2from_repo(hpkg))
1708
1709 if type(old.tid) == type([]):
1710 print _("Transaction ID :"), "%u..%u" % (old.tid[0], old.tid[-1])
1711 else:
1712 print _("Transaction ID :"), old.tid
1713 begtm = time.ctime(old.beg_timestamp)
1714 print _("Begin time :"), begtm
1715 if old.beg_rpmdbversion is not None:
1716 if old.altered_lt_rpmdb:
1717 print _("Begin rpmdb :"), old.beg_rpmdbversion, "**"
1718 else:
1719 print _("Begin rpmdb :"), old.beg_rpmdbversion
1720 if old.end_timestamp is not None:
1721 endtm = time.ctime(old.end_timestamp)
1722 endtms = endtm.split()
1723 if begtm.startswith(endtms[0]): # Chop uninteresting prefix
1724 begtms = begtm.split()
1725 sofar = 0
1726 for i in range(len(endtms)):
1727 if i > len(begtms):
1728 break
1729 if begtms[i] != endtms[i]:
1730 break
1731 sofar += len(begtms[i]) + 1
1732 endtm = (' ' * sofar) + endtm[sofar:]
1733 diff = old.end_timestamp - old.beg_timestamp
1734 if diff < 5 * 60:
1735 diff = _("(%u seconds)") % diff
1736 elif diff < 5 * 60 * 60:
1737 diff = _("(%u minutes)") % (diff / 60)
1738 elif diff < 5 * 60 * 60 * 24:
1739 diff = _("(%u hours)") % (diff / (60 * 60))
1740 else:
1741 diff = _("(%u days)") % (diff / (60 * 60 * 24))
1742 print _("End time :"), endtm, diff
1743 if old.end_rpmdbversion is not None:
1744 if old.altered_gt_rpmdb:
1745 print _("End rpmdb :"), old.end_rpmdbversion, "**"
1746 else:
1747 print _("End rpmdb :"), old.end_rpmdbversion
1748 if type(name) == type([]):
1749 for name in name:
1750 print _("User :"), name
1751 else:
1752 print _("User :"), name
1753 if type(old.return_code) == type([]):
1754 codes = old.return_code
1755 if codes[0] is None:
1756 print _("Return-Code :"), "**", _("Aborted"), "**"
1757 codes = codes[1:]
1758 if codes:
1759 print _("Return-Code :"), _("Failures:"), ", ".join(codes)
1760 elif old.return_code is None:
1761 print _("Return-Code :"), "**", _("Aborted"), "**"
1762 elif old.return_code:
1763 print _("Return-Code :"), _("Failure:"), old.return_code
1764 else:
1765 print _("Return-Code :"), _("Success")
1766
1767 if old.cmdline is not None:
1768 if type(old.cmdline) == type([]):
1769 for cmdline in old.cmdline:
1770 print _("Command Line :"), cmdline
1771 else:
1772 print _("Command Line :"), old.cmdline
1773
1774 if type(old.tid) != type([]):
1775 addon_info = self.history.return_addon_data(old.tid)
1776
1777 # for the ones we create by default - don't display them as there
1778 default_addons = set(['config-main', 'config-repos', 'saved_tx'])
1779 non_default = set(addon_info).difference(default_addons)
1780 if len(non_default) > 0:
1781 print _("Additional non-default information stored: %d"
1782 % len(non_default))
1783
1784 if old.trans_with:
1785 # This is _possible_, but not common
1786 print _("Transaction performed with:")
1787 pkg_max_len = max((len(str(hpkg)) for hpkg in old.trans_with))
1788 for hpkg in old.trans_with:
1789 _simple_pkg(hpkg, 4, was_installed=True, pkg_max_len=pkg_max_len)
1790 print _("Packages Altered:")
1791 self.historyInfoCmdPkgsAltered(old, pats)
1792
1793 if old.trans_skip:
1794 print _("Packages Skipped:")
1795 pkg_max_len = max((len(str(hpkg)) for hpkg in old.trans_skip))
1796 for hpkg in old.trans_skip:
1797 _simple_pkg(hpkg, 4, pkg_max_len=pkg_max_len)
1798
1799 if old.rpmdb_problems:
1800 print _("Rpmdb Problems:")
1801 for prob in old.rpmdb_problems:
1802 key = "%s%s: " % (" " * 4, prob.problem)
1803 print self.fmtKeyValFill(key, prob.text)
1804 if prob.packages:
1805 pkg_max_len = max((len(str(hpkg)) for hpkg in prob.packages))
1806 for hpkg in prob.packages:
1807 _simple_pkg(hpkg, 8, was_installed=True, highlight=hpkg.main,
1808 pkg_max_len=pkg_max_len)
1809
1810 if old.output:
1811 print _("Scriptlet output:")
1812 num = 0
1813 for line in old.output:
1814 num += 1
1815 print "%4d" % num, line
1816 if old.errors:
1817 print _("Errors:")
1818 num = 0
1819 for line in old.errors:
1820 num += 1
1821 print "%4d" % num, line
1822
1823 _history_state2uistate = {'True-Install' : _('Install'),
1824 'Install' : _('Install'),
1825 'Dep-Install' : _('Dep-Install'),
1826 'Obsoleted' : _('Obsoleted'),
1827 'Obsoleting' : _('Obsoleting'),
1828 'Erase' : _('Erase'),
1829 'Reinstall' : _('Reinstall'),
1830 'Downgrade' : _('Downgrade'),
1831 'Downgraded' : _('Downgraded'),
1832 'Update' : _('Update'),
1833 'Updated' : _('Updated'),
1834 }
1835 def historyInfoCmdPkgsAltered(self, old, pats=[]):
1836 last = None
1837 # Note that these don't use _simple_pkg() because we are showing what
1838 # happened to them in the transaction ... not the difference between the
1839 # version in the transaction and now.
1840 all_uistates = self._history_state2uistate
1841 maxlen = 0
1842 pkg_max_len = 0
1843 for hpkg in old.trans_data:
1844 uistate = all_uistates.get(hpkg.state, hpkg.state)
1845 if maxlen < len(uistate):
1846 maxlen = len(uistate)
1847 if pkg_max_len < len(str(hpkg)):
1848 pkg_max_len = len(str(hpkg))
1849
1850 for hpkg in old.trans_data:
1851 prefix = " " * 4
1852 if not hpkg.done:
1853 prefix = " ** "
1854
1855 highlight = 'normal'
1856 if pats:
1857 x,m,u = yum.packages.parsePackages([hpkg], pats)
1858 if x or m:
1859 highlight = 'bold'
1860 (hibeg, hiend) = self._highlight(highlight)
1861
1862 # To chop the name off we need nevra strings, str(pkg) gives envra
1863 # so we have to do it by hand ... *sigh*.
1864 cn = hpkg.ui_nevra
1865
1866 uistate = all_uistates.get(hpkg.state, hpkg.state)
1867 uistate = utf8_width_fill(uistate, maxlen)
1868 # Should probably use columns here...
1869 if False: pass
1870 elif (last is not None and
1871 last.state == 'Updated' and last.name == hpkg.name and
1872 hpkg.state == 'Update'):
1873 ln = len(hpkg.name) + 1
1874 cn = (" " * ln) + cn[ln:]
1875 elif (last is not None and
1876 last.state == 'Downgrade' and last.name == hpkg.name and
1877 hpkg.state == 'Downgraded'):
1878 ln = len(hpkg.name) + 1
1879 cn = (" " * ln) + cn[ln:]
1880 else:
1881 last = None
1882 if hpkg.state in ('Updated', 'Downgrade'):
1883 last = hpkg
1884 print "%s%s%s%s %-*s %s" % (prefix, hibeg, uistate, hiend,
1885 pkg_max_len, cn,
1886 self._hpkg2from_repo(hpkg))
1887
1888 def historySummaryCmd(self, extcmds):
1889 tids, printall = self._history_list_transactions(extcmds)
1890 if tids is None:
1891 return 1, ['Failed history info']
1892
1893 fmt = "%s | %s | %s | %s"
1894 print fmt % (utf8_width_fill(_("Login user"), 26, 26),
1895 utf8_width_fill(_("Time"), 19, 19),
1896 utf8_width_fill(_("Action(s)"), 16, 16),
1897 utf8_width_fill(_("Altered"), 8, 8))
1898 print "-" * 79
1899 fmt = "%s | %s | %s | %8u"
1900 data = {'day' : {}, 'week' : {},
1901 'fortnight' : {}, 'quarter' : {}, 'half' : {},
1902 'year' : {}, 'all' : {}}
1903 for old in self.history.old(tids):
1904 name = self._pwd_ui_username(old.loginuid, 26)
1905 period = 'all'
1906 now = time.time()
1907 if False: pass
1908 elif old.beg_timestamp > (now - (24 * 60 * 60)):
1909 period = 'day'
1910 elif old.beg_timestamp > (now - (24 * 60 * 60 * 7)):
1911 period = 'week'
1912 elif old.beg_timestamp > (now - (24 * 60 * 60 * 14)):
1913 period = 'fortnight'
1914 elif old.beg_timestamp > (now - (24 * 60 * 60 * 7 * 13)):
1915 period = 'quarter'
1916 elif old.beg_timestamp > (now - (24 * 60 * 60 * 7 * 26)):
1917 period = 'half'
1918 elif old.beg_timestamp > (now - (24 * 60 * 60 * 365)):
1919 period = 'year'
1920 data[period].setdefault(name, []).append(old)
1921 _period2user = {'day' : _("Last day"),
1922 'week' : _("Last week"),
1923 'fortnight' : _("Last 2 weeks"), # US default :p
1924 'quarter' : _("Last 3 months"),
1925 'half' : _("Last 6 months"),
1926 'year' : _("Last year"),
1927 'all' : _("Over a year ago")}
1928 done = 0
1929 for period in ('day', 'week', 'fortnight', 'quarter', 'half', 'year',
1930 'all'):
1931 if not data[period]:
1932 continue
1933 for name in sorted(data[period]):
1934 if not printall and done > 19:
1935 break
1936 done += 1
1937
1938 hpkgs = []
1939 for old in data[period][name]:
1940 hpkgs.extend(old.trans_data)
1941 count, uiacts = self._history_uiactions(hpkgs)
1942 uperiod = _period2user[period]
1943 # Should probably use columns here, esp. for uiacts?
1944 print fmt % (utf8_width_fill(name, 26, 26),
1945 utf8_width_fill(uperiod, 19, 19),
1946 utf8_width_fill(uiacts, 16, 16), count)
1947
1948 def historyAddonInfoCmd(self, extcmds):
1949 tid = None
1950 if len(extcmds) > 1:
1951 tid = extcmds[1]
1952 if tid == 'last':
1953 tid = None
1954 if tid is not None:
1955 try:
1956 int(tid)
1957 except ValueError:
1958 self.logger.critical(_('Bad transaction ID given'))
1959 return 1, ['Failed history addon-info']
1960
1961 if tid is not None:
1962 old = self.history.old(tids=[tid])
1963 else:
1964 old = [self.history.last(complete_transactions_only=False)]
1965 if old[0] is None:
1966 self.logger.critical(_('No transaction ID, or package, given'))
1967 return 1, ['Failed history addon-info']
1968
1969 if not old:
1970 self.logger.critical(_('No Transaction %s found') % tid)
1971 return 1, ['Failed history addon-info']
1972
1973 hist_data = old[0]
1974 addon_info = self.history.return_addon_data(hist_data.tid)
1975 if len(extcmds) <= 2:
1976 print _("Transaction ID:"), hist_data.tid
1977 print _('Available additional history information:')
1978 for itemname in self.history.return_addon_data(hist_data.tid):
1979 print ' %s' % itemname
1980 print ''
1981
1982 return 0, ['history addon-info']
1983
1984 for item in extcmds[2:]:
1985 if item in addon_info:
1986 print '%s:' % item
1987 print self.history.return_addon_data(hist_data.tid, item)
1988 else:
1989 print _('%s: No additional data found by this name') % item
1990
1991 print ''
1992
1993 def historyPackageListCmd(self, extcmds):
1994 """ Shows the user a list of data about the history, from the point
1995 of a package(s) instead of via. transactions. """
1996 tids = self.history.search(extcmds)
1997 limit = None
1998 if extcmds and not tids:
1999 self.logger.critical(_('Bad transaction IDs, or package(s), given'))
2000 return 1, ['Failed history packages-list']
2001 if not tids:
2002 limit = 20
2003
2004 all_uistates = self._history_state2uistate
2005
2006 fmt = "%s | %s | %s"
2007 # REALLY Needs to use columns!
2008 print fmt % (utf8_width_fill(_("ID"), 6, 6),
2009 utf8_width_fill(_("Action(s)"), 14, 14),
2010 utf8_width_fill(_("Package"), 53, 53))
2011 print "-" * 79
2012 fmt = "%6u | %s | %-50s"
2013 num = 0
2014 for old in self.history.old(tids, limit=limit):
2015 if limit is not None and num and (num +len(old.trans_data)) > limit:
2016 break
2017 last = None
2018
2019 # Copy and paste from list ... uh.
2020 rmark = lmark = ' '
2021 if old.return_code is None:
2022 rmark = lmark = '*'
2023 elif old.return_code:
2024 rmark = lmark = '#'
2025 # We don't check .errors, because return_code will be non-0
2026 elif old.output:
2027 rmark = lmark = 'E'
2028 elif old.rpmdb_problems:
2029 rmark = lmark = 'P'
2030 elif old.trans_skip:
2031 rmark = lmark = 's'
2032 if old.altered_lt_rpmdb:
2033 rmark = '<'
2034 if old.altered_gt_rpmdb:
2035 lmark = '>'
2036
2037 for hpkg in old.trans_data: # Find a pkg to go with each cmd...
2038 if limit is None:
2039 x,m,u = yum.packages.parsePackages([hpkg], extcmds)
2040 if not x and not m:
2041 continue
2042
2043 uistate = all_uistates.get(hpkg.state, hpkg.state)
2044 uistate = utf8_width_fill(uistate, 14)
2045
2046 # To chop the name off we need nevra strings, str(pkg) gives
2047 # envra so we have to do it by hand ... *sigh*.
2048 cn = hpkg.ui_nevra
2049
2050 # Should probably use columns here...
2051 if False: pass
2052 elif (last is not None and
2053 last.state == 'Updated' and last.name == hpkg.name and
2054 hpkg.state == 'Update'):
2055 ln = len(hpkg.name) + 1
2056 cn = (" " * ln) + cn[ln:]
2057 elif (last is not None and
2058 last.state == 'Downgrade' and last.name == hpkg.name and
2059 hpkg.state == 'Downgraded'):
2060 ln = len(hpkg.name) + 1
2061 cn = (" " * ln) + cn[ln:]
2062 else:
2063 last = None
2064 if hpkg.state in ('Updated', 'Downgrade'):
2065 last = hpkg
2066
2067 num += 1
2068 print fmt % (old.tid, uistate, cn), "%s%s" % (lmark,rmark)
2069
2070 # And, again, copy and paste...
2071 lastdbv = self.history.last()
2072 if lastdbv is None:
2073 self._rpmdb_warn_checks(warn=False)
2074 else:
2075 # If this is the last transaction, is good and it doesn't
2076 # match the current rpmdb ... then mark it as bad.
2077 rpmdbv = self.rpmdb.simpleVersion(main_only=True)[0]
2078 if lastdbv.end_rpmdbversion != rpmdbv:
2079 self._rpmdb_warn_checks()
2080
2081
2082class DepSolveProgressCallBack:
2083 """provides text output callback functions for Dependency Solver callback"""
2084
2085 def __init__(self, ayum=None):
2086 """requires yum-cli log and errorlog functions as arguments"""
2087 self.verbose_logger = logging.getLogger("yum.verbose.cli")
2088 self.loops = 0
2089 self.ayum = ayum
2090
2091 def pkgAdded(self, pkgtup, mode):
2092 modedict = { 'i': _('installed'),
2093 'u': _('an update'),
2094 'e': _('erased'),
2095 'r': _('reinstalled'),
2096 'd': _('a downgrade'),
2097 'o': _('obsoleting'),
2098 'ud': _('updated'),
2099 'od': _('obsoleted'),}
2100 (n, a, e, v, r) = pkgtup
2101 modeterm = modedict[mode]
2102 self.verbose_logger.log(logginglevels.INFO_2,
2103 _('---> Package %s.%s %s:%s-%s will be %s'), n, a, e, v, r,
2104 modeterm)
2105
2106 def start(self):
2107 self.loops += 1
2108
2109 def tscheck(self):
2110 self.verbose_logger.log(logginglevels.INFO_2, _('--> Running transaction check'))
2111
2112 def restartLoop(self):
2113 self.loops += 1
2114 self.verbose_logger.log(logginglevels.INFO_2,
2115 _('--> Restarting Dependency Resolution with new changes.'))
2116 self.verbose_logger.debug('---> Loop Number: %d', self.loops)
2117
2118 def end(self):
2119 self.verbose_logger.log(logginglevels.INFO_2,
2120 _('--> Finished Dependency Resolution'))
2121
2122
2123 def procReq(self, name, formatted_req):
2124 self.verbose_logger.log(logginglevels.INFO_2,
2125 _('--> Processing Dependency: %s for package: %s'), formatted_req,
2126 name)
2127
2128 def procReqPo(self, po, formatted_req):
2129 self.verbose_logger.log(logginglevels.INFO_2,
2130 _('--> Processing Dependency: %s for package: %s'), formatted_req,
2131 po)
2132
2133 def groupRemoveReq(self, po, hits):
2134 self.verbose_logger.log(logginglevels.INFO_2,
2135 _('---> Keeping package: %s'), po)
2136
2137 def unresolved(self, msg):
2138 self.verbose_logger.log(logginglevels.INFO_2, _('--> Unresolved Dependency: %s'),
2139 msg)
2140
2141 def format_missing_requires(self, reqPo, reqTup):
2142 """ Create a message for errorlist, non-cli users could also store this
2143 data somewhere and not print errorlist. """
2144 needname, needflags, needversion = reqTup
2145
2146 yb = self.ayum
2147
2148 prob_pkg = "%s (%s)" % (reqPo, reqPo.ui_from_repo)
2149 msg = _('Package: %s') % (prob_pkg,)
2150 ui_req = formatRequire(needname, needversion, needflags)
2151 msg += _('\n Requires: %s') % (ui_req,)
2152
2153 # if DepSolveProgressCallback() is used instead of DepSolveProgressCallback(ayum=<YumBase Object>)
2154 # then ayum has no value and we can't continue to find details about the missing requirements
2155 if not yb:
2156 return msg
2157
2158 def _msg_pkg(action, pkg, needname):
2159 " Add a package to the message, including any provides matches. "
2160 msg = _('\n %s: %s (%s)') % (action, pkg, pkg.ui_from_repo)
2161 needtup = (needname, None, (None, None, None))
2162 done = False
2163 for pkgtup in pkg.matchingPrcos('provides', needtup):
2164 done = True
2165 msg += _('\n %s') % yum.misc.prco_tuple_to_string(pkgtup)
2166 if not done:
2167 msg += _('\n Not found')
2168 return msg
2169
2170 def _run_inst_pkg(pkg, msg):
2171 nevr = (pkg.name, pkg.epoch, pkg.version, pkg.release)
2172 if nevr in seen_pkgs or (pkg.verEQ(last) and pkg.arch == last.arch):
2173 return msg
2174
2175 seen_pkgs.add(nevr)
2176 action = _('Installed')
2177 rmed = yb.tsInfo.getMembersWithState(pkg.pkgtup, TS_REMOVE_STATES)
2178 if rmed:
2179 action = _('Removing')
2180 msg += _msg_pkg(action, pkg, needname)
2181 # These should be the only three things we care about:
2182 relmap = {'updatedby' : _('Updated By'),
2183 'downgradedby' : _('Downgraded By'),
2184 'obsoletedby' : _('Obsoleted By'),
2185 }
2186 for txmbr in rmed:
2187 for (rpkg, rtype) in txmbr.relatedto:
2188 if rtype not in relmap:
2189 continue
2190 nevr = (rpkg.name, rpkg.epoch, rpkg.version, rpkg.release)
2191 seen_pkgs.add(nevr)
2192 msg += _msg_pkg(relmap[rtype], rpkg, needname)
2193 return msg
2194
2195 def _run_avail_pkg(pkg, msg):
2196 # We don't want to see installed packages, or N packages of the
2197 # same version, from different repos.
2198 nevr = (pkg.name, pkg.epoch, pkg.version, pkg.release)
2199 if nevr in seen_pkgs or (pkg.verEQ(last) and pkg.arch == last.arch):
2200 return False, last, msg
2201 seen_pkgs.add(nevr)
2202 action = _('Available')
2203 if yb.tsInfo.getMembersWithState(pkg.pkgtup, TS_INSTALL_STATES):
2204 action = _('Installing')
2205 msg += _msg_pkg(action, pkg, needname)
2206 return True, pkg, msg
2207
2208 last = None
2209 seen_pkgs = set()
2210 for pkg in sorted(yb.rpmdb.getProvides(needname)):
2211 msg = _run_inst_pkg(pkg, msg)
2212
2213 available_names = set()
2214 for pkg in sorted(yb.pkgSack.getProvides(needname)):
2215 tst, last, msg = _run_avail_pkg(pkg, msg)
2216 if tst:
2217 available_names.add(pkg.name)
2218
2219 last = None
2220 for pkg in sorted(yb.rpmdb.searchNames(available_names)):
2221 msg = _run_inst_pkg(pkg, msg)
2222 last = None
2223 for pkg in sorted(yb.pkgSack.searchNames(available_names)):
2224 tst, last, msg = _run_avail_pkg(pkg, msg)
2225 return msg
2226
2227 def procConflict(self, name, confname):
2228 self.verbose_logger.log(logginglevels.INFO_2,
2229 _('--> Processing Conflict: %s conflicts %s'),
2230 name, confname)
2231
2232 def procConflictPo(self, po, confname):
2233 self.verbose_logger.log(logginglevels.INFO_2,
2234 _('--> Processing Conflict: %s conflicts %s'),
2235 po, confname)
2236
2237 def transactionPopulation(self):
2238 self.verbose_logger.log(logginglevels.INFO_2, _('--> Populating transaction set '
2239 'with selected packages. Please wait.'))
2240
2241 def downloadHeader(self, name):
2242 self.verbose_logger.log(logginglevels.INFO_2, _('---> Downloading header for %s '
2243 'to pack into transaction set.'), name)
2244
2245
2246class CacheProgressCallback:
2247
2248 '''
2249 The class handles text output callbacks during metadata cache updates.
2250 '''
2251
2252 def __init__(self):
2253 self.logger = logging.getLogger("yum.cli")
2254 self.verbose_logger = logging.getLogger("yum.verbose.cli")
2255 self.file_logger = logging.getLogger("yum.filelogging.cli")
2256
2257 def log(self, level, message):
2258 self.verbose_logger.log(level, message)
2259
2260 def errorlog(self, level, message):
2261 self.logger.log(level, message)
2262
2263 def filelog(self, level, message):
2264 self.file_logger.log(level, message)
2265
2266 def progressbar(self, current, total, name=None):
2267 progressbar(current, total, name)
2268
2269def _pkgname_ui(ayum, pkgname, ts_states=None):
2270 """ Get more information on a simple pkgname, if we can. We need to search
2271 packages that we are dealing with atm. and installed packages (if the
2272 transaction isn't complete). """
2273 if ayum is None:
2274 return pkgname
2275
2276 if ts_states is None:
2277 # Note 'd' is a placeholder for downgrade, and
2278 # 'r' is a placeholder for reinstall. Neither exist atm.
2279 ts_states = ('d', 'e', 'i', 'r', 'u', 'od', 'ud')
2280
2281 matches = []
2282 def _cond_add(po):
2283 if matches and matches[0].arch == po.arch and matches[0].verEQ(po):
2284 return
2285 matches.append(po)
2286
2287 for txmbr in ayum.tsInfo.matchNaevr(name=pkgname):
2288 if txmbr.ts_state not in ts_states:
2289 continue
2290 _cond_add(txmbr.po)
2291
2292 if not matches:
2293 return pkgname
2294 fmatch = matches.pop(0)
2295 if not matches:
2296 return str(fmatch)
2297
2298 show_ver = True
2299 show_arch = True
2300 for match in matches:
2301 if not fmatch.verEQ(match):
2302 show_ver = False
2303 if fmatch.arch != match.arch:
2304 show_arch = False
2305
2306 if show_ver: # Multilib. *sigh*
2307 if fmatch.epoch == '0':
2308 return '%s-%s-%s' % (fmatch.name, fmatch.version, fmatch.release)
2309 else:
2310 return '%s:%s-%s-%s' % (fmatch.epoch, fmatch.name,
2311 fmatch.version, fmatch.release)
2312
2313 if show_arch:
2314 return '%s.%s' % (fmatch.name, fmatch.arch)
2315
2316 return pkgname
2317
2318class YumCliRPMCallBack(RPMBaseCallback):
2319
2320 """
2321 Yum specific callback class for RPM operations.
2322 """
2323
2324 width = property(lambda x: _term_width())
2325
2326 def __init__(self, ayum=None):
2327 RPMBaseCallback.__init__(self)
2328 self.lastmsg = to_unicode("")
2329 self.lastpackage = None # name of last package we looked at
2330 self.output = logging.getLogger("yum.verbose.cli").isEnabledFor(logginglevels.INFO_2)
2331
2332 # for a progress bar
2333 self.mark = "#"
2334 self.marks = 22
2335 self.ayum = ayum
2336
2337 # Installing things have pkg objects passed to the events, so only need to
2338 # lookup for erased/obsoleted.
2339 def pkgname_ui(self, pkgname, ts_states=('e', 'od', 'ud', None)):
2340 """ Get more information on a simple pkgname, if we can. """
2341 return _pkgname_ui(self.ayum, pkgname, ts_states)
2342
2343 def event(self, package, action, te_current, te_total, ts_current, ts_total):
2344 # this is where a progress bar would be called
2345 process = self.action[action]
2346
2347 if not hasattr(self, '_max_action_wid'):
2348 wid1 = 0
2349 for val in self.action.values():
2350 wid_val = utf8_width(val)
2351 if wid1 < wid_val:
2352 wid1 = wid_val
2353 self._max_action_wid = wid1
2354 wid1 = self._max_action_wid
2355
2356 if type(package) not in types.StringTypes:
2357 pkgname = str(package)
2358 else:
2359 pkgname = self.pkgname_ui(package)
2360
2361 self.lastpackage = package
2362 if te_total == 0:
2363 percent = 0
2364 else:
2365 percent = (te_current*100L)/te_total
2366
2367 if self.output and (sys.stdout.isatty() or te_current == te_total):
2368 (fmt, wid1, wid2) = self._makefmt(percent, ts_current, ts_total,
2369 pkgname=pkgname, wid1=wid1)
2370 msg = fmt % (utf8_width_fill(process, wid1, wid1),
2371 utf8_width_fill(pkgname, wid2, wid2))
2372 if msg != self.lastmsg:
2373 sys.stdout.write(to_unicode(msg))
2374 sys.stdout.flush()
2375 self.lastmsg = msg
2376 if te_current == te_total:
2377 print " "
2378
2379 def scriptout(self, package, msgs):
2380 if msgs:
2381 sys.stdout.write(to_unicode(msgs))
2382 sys.stdout.flush()
2383
2384 def _makefmt(self, percent, ts_current, ts_total, progress = True,
2385 pkgname=None, wid1=15):
2386 l = len(str(ts_total))
2387 size = "%s.%s" % (l, l)
2388 fmt_done = "%" + size + "s/%" + size + "s"
2389 done = fmt_done % (ts_current, ts_total)
2390
2391 # This should probably use TerminLine, but we don't want to dep. on
2392 # that. So we kind do an ok job by hand ... at least it's dynamic now.
2393 if pkgname is None:
2394 pnl = 22
2395 else:
2396 pnl = utf8_width(pkgname)
2397
2398 overhead = (2 * l) + 2 # Length of done, above
2399 overhead += 2+ wid1 +2 # Length of begining (" " action " :")
2400 overhead += 1 # Space between pn and done
2401 overhead += 2 # Ends for progress
2402 overhead += 1 # Space for end
2403 width = self.width
2404 if width < overhead:
2405 width = overhead # Give up
2406 width -= overhead
2407 if pnl > width / 2:
2408 pnl = width / 2
2409
2410 marks = self.width - (overhead + pnl)
2411 width = "%s.%s" % (marks, marks)
2412 fmt_bar = "[%-" + width + "s]"
2413 # pnl = str(28 + marks + 1)
2414 full_pnl = pnl + marks + 1
2415
2416 if progress and percent == 100: # Don't chop pkg name on 100%
2417 fmt = "\r %s: %s " + done
2418 wid2 = full_pnl
2419 elif progress:
2420 bar = fmt_bar % (self.mark * int(marks * (percent / 100.0)), )
2421 fmt = "\r %s: %s " + bar + " " + done
2422 wid2 = pnl
2423 elif percent == 100:
2424 fmt = " %s: %s " + done
2425 wid2 = full_pnl
2426 else:
2427 bar = fmt_bar % (self.mark * marks, )
2428 fmt = " %s: %s " + bar + " " + done
2429 wid2 = pnl
2430 return fmt, wid1, wid2
2431
2432
2433def progressbar(current, total, name=None):
2434 """simple progress bar 50 # marks"""
2435
2436 mark = '#'
2437 if not sys.stdout.isatty():
2438 return
2439
2440 if current == 0:
2441 percent = 0
2442 else:
2443 if total != 0:
2444 percent = float(current) / total
2445 else:
2446 percent = 0
2447
2448 width = _term_width()
2449
2450 if name is None and current == total:
2451 name = '-'
2452
2453 end = ' %d/%d' % (current, total)
2454 width -= len(end) + 1
2455 if width < 0:
2456 width = 0
2457 if name is None:
2458 width -= 2
2459 if width < 0:
2460 width = 0
2461 hashbar = mark * int(width * percent)
2462 output = '\r[%-*s]%s' % (width, hashbar, end)
2463 elif current == total: # Don't chop name on 100%
2464 output = '\r%s%s' % (utf8_width_fill(name, width, width), end)
2465 else:
2466 width -= 4
2467 if width < 0:
2468 width = 0
2469 nwid = width / 2
2470 if nwid > utf8_width(name):
2471 nwid = utf8_width(name)
2472 width -= nwid
2473 hashbar = mark * int(width * percent)
2474 output = '\r%s: [%-*s]%s' % (utf8_width_fill(name, nwid, nwid), width,
2475 hashbar, end)
2476
2477 if current <= total:
2478 sys.stdout.write(output)
2479
2480 if current == total:
2481 sys.stdout.write('\n')
2482
2483 sys.stdout.flush()
2484
2485
2486if __name__ == "__main__":
2487 if len(sys.argv) > 1 and sys.argv[1] == "format_number":
2488 print ""
2489 print " Doing format_number tests, right column should align"
2490 print ""
2491
2492 x = YumOutput()
2493 for i in (0, 0.0, 0.1, 1, 1.0, 1.1, 10, 11, 11.1, 100, 111.1,
2494 1000, 1111, 1024 * 2, 10000, 11111, 99999, 999999,
2495 10**19, 10**20, 10**35):
2496 out = x.format_number(i)
2497 print "%36s <%s> %s <%5s>" % (i, out, ' ' * (14 - len(out)), out)
2498
2499 if len(sys.argv) > 1 and sys.argv[1] == "progress":
2500 print ""
2501 print " Doing progress, small name"
2502 print ""
2503 for i in xrange(0, 101):
2504 progressbar(i, 100, "abcd")
2505 time.sleep(0.1)
2506 print ""
2507 print " Doing progress, big name"
2508 print ""
2509 for i in xrange(0, 101):
2510 progressbar(i, 100, "_%s_" % ("123456789 " * 5))
2511 time.sleep(0.1)
2512 print ""
2513 print " Doing progress, no name"
2514 print ""
2515 for i in xrange(0, 101):
2516 progressbar(i, 100)
2517 time.sleep(0.1)
2518
2519 if len(sys.argv) > 1 and sys.argv[1] in ("progress", "rpm-progress"):
2520 cb = YumCliRPMCallBack()
2521 cb.output = True
2522 cb.action["foo"] = "abcd"
2523 cb.action["bar"] = "_12345678_.end"
2524 print ""
2525 print " Doing CB, small proc / small pkg"
2526 print ""
2527 for i in xrange(0, 101):
2528 cb.event("spkg", "foo", i, 100, i, 100)
2529 time.sleep(0.1)
2530 print ""
2531 print " Doing CB, big proc / big pkg"
2532 print ""
2533 for i in xrange(0, 101):
2534 cb.event("lpkg" + "-=" * 15 + ".end", "bar", i, 100, i, 100)
2535 time.sleep(0.1)
2536
2537 if len(sys.argv) > 1 and sys.argv[1] in ("progress", "i18n-progress",
2538 "rpm-progress",
2539 'i18n-rpm-progress'):
2540 yum.misc.setup_locale()
2541 if len(sys.argv) > 1 and sys.argv[1] in ("progress", "i18n-progress"):
2542 print ""
2543 print " Doing progress, i18n: small name"
2544 print ""
2545 for i in xrange(0, 101):
2546 progressbar(i, 100, to_unicode('\xe6\xad\xa3\xe5\x9c\xa8\xe5\xae\x89\xe8\xa3\x85'))
2547 time.sleep(0.1)
2548 print ""
2549
2550 print ""
2551 print " Doing progress, i18n: big name"
2552 print ""
2553 for i in xrange(0, 101):
2554 progressbar(i, 100, to_unicode('\xe6\xad\xa3\xe5\x9c\xa8\xe5\xae\x89\xe8\xa3\x85' * 5 + ".end"))
2555 time.sleep(0.1)
2556 print ""
2557
2558
2559 if len(sys.argv) > 1 and sys.argv[1] in ("progress", "i18n-progress",
2560 "rpm-progress",
2561 'i18n-rpm-progress'):
2562 cb = YumCliRPMCallBack()
2563 cb.output = True
2564 cb.action["foo"] = to_unicode('\xe6\xad\xa3\xe5\x9c\xa8\xe5\xae\x89\xe8\xa3\x85')
2565 cb.action["bar"] = cb.action["foo"] * 5 + ".end"
2566 print ""
2567 print " Doing CB, i18n: small proc / small pkg"
2568 print ""
2569 for i in xrange(0, 101):
2570 cb.event("spkg", "foo", i, 100, i, 100)
2571 time.sleep(0.1)
2572 print ""
2573 print " Doing CB, i18n: big proc / big pkg"
2574 print ""
2575 for i in xrange(0, 101):
2576 cb.event("lpkg" + "-=" * 15 + ".end", "bar", i, 100, i, 100)
2577 time.sleep(0.1)
2578 print ""
2579
Note: See TracBrowser for help on using the repository browser.