anvil/tools/yyoom
Pranesh Pandurangan 3477c118ff Nitpicking an error statement
Make an error statement better to read.

Change-Id: Id7d58510042754806cb31e5d178602daa045891a
2014-06-16 09:56:07 -07:00

449 lines
15 KiB
Python
Executable File

#!/usr/bin/python
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# Copyright 2005 Duke University
# Parts Copyright 2007 Red Hat, Inc
"""YYOOM: a package management utility
Using Yum API instead of /usr/bin/yum provides several interesting
capabilities, some of which we are desperate to use, including:
- installing and removing packages in same transaction;
- JSON output.
"""
import argparse
import collections
import functools
import json
import logging
import pkg_resources
import subprocess
import sys
import yum
from contextlib import contextmanager
LOG = logging.getLogger('yyoom')
ACTION_TYPE_MAP = {
yum.constants.TS_INSTALL: 'install',
yum.constants.TS_TRUEINSTALL: 'install',
yum.constants.TS_UPDATE: 'upgrade',
yum.constants.TS_OBSOLETING: 'upgrade',
yum.constants.TS_ERASE: 'erase',
yum.constants.TS_OBSOLETED: 'erase',
yum.constants.TS_UPDATED: 'erase',
yum.constants.TS_FAILED: 'error'
}
def _write_output(data, where):
"""Dump given object as pretty json."""
where.write(json.dumps(data, indent=4,
separators=(',', ': '),
sort_keys=True) + '\n')
where.flush()
def _extended_yum_raises(method):
"""Decorator to extend error messages when manipulating packages with yum.
"""
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except yum.Errors.YumBaseError as e:
data = dict(
method_name=method.__name__,
args=[str(arg) for arg in args],
kwargs='\n'.join(' %s: %s' % item
for item in sorted(kwargs.items())))
details = yum.i18n._("\nDetails:\n"
" method name: %(method_name)s\n"
" arguments: %(args)s\n"
" keyword arguments:\n%(kwargs)s")
e.value += details % data
raise
return wrapper
class _YyoomBase(yum.YumBase):
def __init__(self, *args, **kwargs):
"""Reintroduced init to preset some settings.
"""
super(_YyoomBase, self).__init__(*args, **kwargs)
self.setCacheDir(force=True)
def _askForGPGKeyImport(self, po, userid, hexkeyid):
"""Tell yum to import GPG keys if needed.
Fixes: https://bugs.launchpad.net/anvil/+bug/1210657
Fixes: https://bugs.launchpad.net/anvil/+bug/1218728
"""
return True
@_extended_yum_raises
def install(self, po=None, **kwargs):
return super(_YyoomBase, self).install(po, **kwargs)
@_extended_yum_raises
def remove(self, po=None, **kwargs):
return super(_YyoomBase, self).remove(po, **kwargs)
def _action_type_from_code(action):
"""Return value according to action code in dictionary mapping Yum states.
Yum has a mapping that sometimes really isn't that accurate enough
for our needs, so make a mapping that will suit our needs instead.
"""
return ACTION_TYPE_MAP.get(action, 'other')
def _package_info(pkg, **kwargs):
if isinstance(pkg, basestring):
result = dict(name=pkg, **kwargs)
else:
result = dict(
name=pkg.name,
epoch=pkg.epoch,
version=pkg.version,
release=pkg.release,
provides=pkg.provides,
repo=str(pkg.repo),
arch=pkg.arch,
**kwargs
)
return result
class _RPMCallback(yum.rpmtrans.RPMBaseCallback):
"""Listen to events from RPM transactions."""
def event(self, package, action, te_current, te_total,
ts_current, ts_total):
pass
def scriptout(self, package, msg):
if not msg or not LOG.isEnabledFor(logging.INFO):
return
for line in msg.splitlines():
line = line.strip()
if line:
LOG.info("%s: %s", package, line)
def errorlog(self, msg):
LOG.error("%s", msg)
def filelog(self, package, action):
if not LOG.isEnabledFor(logging.INFO):
return
LOG.info("Performed %(action_type)s (code %(action)s) on %(package)s",
dict(package=package,
action=action,
action_type=_action_type_from_code(action)))
class _OutputtingRPMCallback(_RPMCallback):
def __init__(self, options):
_RPMCallback.__init__(self)
self._skip_missing = options.skip_missing
self._missing = []
self._options = options
def yyoom_post_transaction(self, base, _code):
output = []
for txmbr in base.tsInfo:
action_type = _action_type_from_code(txmbr.output_state)
info = _package_info(txmbr.po,
action_code=txmbr.output_state,
action_type=action_type)
output.append(info)
with open(self._options.output_file, 'ab') as fh:
_write_output(output + self._missing, fh)
def yyoom_on_missing_package(self, pkg_req):
if not self._skip_missing:
raise yum.Errors.InstallError("The package '%s' was not found." % pkg_req)
req = pkg_resources.Requirement.parse(pkg_req)
self._missing.append(_package_info(req.unsafe_name,
action_type="missing",
requirement=pkg_req,
action=None))
def log_list(items, title=''):
if not items:
return
if title:
if not title.endswith(':'):
title = str(title) + ":"
LOG.info(title)
for i in items:
LOG.info(" - %s" % (i))
def build_yum_map(base):
rpms = base.doPackageLists(ignore_case=True,
showdups=True)
all_rpms = []
for name in ('available', 'installed', 'extras', 'reinstall_available'):
all_rpms.extend(getattr(rpms, name, []))
yum_map = collections.defaultdict(list)
for rpm in all_rpms:
for provides in rpm.provides:
yum_map[provides[0]].append((rpm.version, rpm))
return dict(yum_map)
def _find_packages(yum_map, pkg_req):
"""Find suitable packages in YUM packages map."""
req = pkg_resources.Requirement.parse(pkg_req)
matches = [rpm
for (version, rpm) in yum_map.get(req.unsafe_name, [])
if version in req]
if matches:
return matches
def _run(yum_base, options):
"""Handler of `transaction` command
Installs and erases packages, prints what was done in JSON
"""
log_list(options.erase, title='Erasing packages:')
log_list(options.install, title='Installing packages:')
with _transaction(yum_base,
_OutputtingRPMCallback(options)) as cb:
yum_map = build_yum_map(yum_base)
# erase packages
for pkg_name in options.erase or ():
matches = _find_packages(yum_map, pkg_name)
if matches is None:
cb.yyoom_on_missing_package(pkg_name)
else:
installed_packages = yum_base.rpmdb.returnPackages()
for package in matches:
if package in installed_packages:
yum_base.remove(package)
# install packages
for pkg_name in options.install or ():
matches = _find_packages(yum_map, pkg_name)
if matches is None:
cb.yyoom_on_missing_package(pkg_name)
else:
# try to install package from preferred repositories,
# if not found - install from default ones
repo_matches = [m for m in matches
if m.repoid in options.prefer_repo]
matches = repo_matches if repo_matches else matches
yum_base.install(max(matches))
def _list(yum_base, options):
"""Handler of `list` command."""
pkgnarrow = options.what[0] if len(options.what) == 1 else 'all'
lists = yum_base.doPackageLists(pkgnarrow=pkgnarrow, showdups=True)
LOG.debug("Got packages for '%s': %s installed, %s available,"
"%s available for reinstall, %s extras",
pkgnarrow, len(lists.installed), len(lists.available),
len(lists.reinstall_available), len(lists.extras))
result = []
if 'installed' in options.what:
result.extend(_package_info(pkg, status='installed')
for pkg in lists.installed)
if 'available' in options.what:
result.extend(_package_info(pkg, status='available')
for pkg in lists.available)
result.extend(_package_info(pkg, status='available')
for pkg in lists.reinstall_available)
if 'extras' in options.what:
result.extend(_package_info(pkg, status='installed')
for pkg in lists.extras)
with open(options.output_file, 'ab') as fh:
_write_output(result, fh)
def _cleanall(yum_base, options):
"""Handler of `cleanall` command."""
LOG.info("Running yum cleanup")
code = sum((
_run_yum_api('packages clean up', yum_base.cleanPackages),
_run_yum_api('headers clean up', yum_base.cleanHeaders),
_run_yum_api('metadata clean up', yum_base.cleanMetadata),
_run_yum_api('sqlite clean up', yum_base.cleanSqlite),
_run_yum_api('rpm db clean up', yum_base.cleanRpmDB),
))
return code
def _builddep(yum_base, options):
"""Handler of `builddep` command
Installs build dependencies for given package, prints what was done
in JSON.
"""
LOG.info("Installing build dependencies for package %s", options.srpm)
srpm = yum.packages.YumLocalPackage(yum_base.ts, options.srpm)
with _transaction(yum_base, _OutputtingRPMCallback(options)):
for req in srpm.requiresList():
LOG.debug('Processing dependency: %s', req)
if not (
req.startswith('rpmlib(') or
yum_base.returnInstalledPackagesByDep(req)
):
pkg = yum_base.returnPackageByDep(req)
LOG.debug('Installing %s', pkg)
yum_base.install(pkg)
def _parse_arguments(args):
parser = argparse.ArgumentParser(prog=args[0])
parser.add_argument('--verbose', '-v', action='store_true',
help='verbose operation')
parser.add_argument('--output-file', '-o', required=True)
parser.add_argument('--quiet', '-q', action='store_true')
# TODO(imelnikov): --format
subparsers = parser.add_subparsers(title='subcommands')
# Arg: list
parser_list = subparsers.add_parser('list', help='list packages')
parser_list.add_argument('what', nargs='+',
choices=('installed', 'available', 'extras'),
help='what packages to list')
parser_list.set_defaults(func=_list, operation='List')
# Arg: transaction
parser_run = subparsers.add_parser('transaction',
help='install or remove packages')
parser_run.add_argument('--install', '-i', action='append',
metavar='package',
help='install package')
parser_run.add_argument('--erase', '-e', action='append',
metavar='package',
help='erase package')
parser_run.add_argument('--skip-missing', action='store_true',
default=False,
help='do not fail on missing packages')
parser_run.add_argument('--prefer-repo', '-r', action='append',
metavar='repository',
default=[],
help='preferred repository name')
parser_run.set_defaults(func=_run, operation='Transaction')
# Arg: srpm
parser_builddep = subparsers.add_parser(
'builddep', help='install build dependencies of srpm')
parser_builddep.add_argument('srpm', help='path to source RPM package')
parser_builddep.set_defaults(func=_builddep, operation='Builddep')
# Arg: cleanall
parser_cleanall = subparsers.add_parser('cleanall', help='clean all')
parser_cleanall.set_defaults(func=_cleanall, operation='Cleanall')
return parser.parse_args(args[1:] or ['--help'])
def _setup_logging(verbose=True, quiet=False):
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter('YYOOM %(levelname)s: %(message)s'))
if quiet:
loglevel = logging.ERROR
elif verbose:
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
handler.setLevel(loglevel)
root_logger = logging.getLogger()
root_logger.addHandler(handler)
root_logger.setLevel(logging.DEBUG)
def _run_yum_api(name, func, ok_codes=(0,), *args, **kwargs):
code, results = func(*args, **kwargs)
for msg in results:
LOG.debug(msg)
if code not in ok_codes:
LOG.error('%s failed', name.title())
return code
@contextmanager
def _transaction(base, callback):
"""Manage Yum transactions
Locks and unlocks Yum database, builds and processes transaction
on __exit__.
"""
try:
base.doLock()
yield callback
code = _run_yum_api('building transaction',
base.buildTransaction, ok_codes=(0, 2))
failed = []
if code == 0:
LOG.debug('Nothing to do')
elif code == 2:
base.processTransaction(rpmTestDisplay=callback,
rpmDisplay=callback)
failed = [txmbr for txmbr in base.tsInfo
if txmbr.output_state == yum.constants.TS_FAILED]
else:
raise RuntimeError("Transaction failed: %s" % code)
post_cb = getattr(callback, 'yyoom_post_transaction', None)
if post_cb:
post_cb(base, code)
if failed:
raise RuntimeError("Operation failed for %s" %
', '.join(txmbr.name for txmbr in failed))
finally:
del base.tsInfo
del base.ts
base.doUnlock()
def main(args):
options = _parse_arguments(args)
_setup_logging(options.verbose, options.quiet)
LOG.debug('Running YOOM %s', options.operation)
LOG.debug('Command line: %s', subprocess.list2cmdline(args))
try:
yum_base = _YyoomBase()
code = options.func(yum_base, options) or 0
except Exception as e:
if options.verbose:
LOG.exception("%s failed", options.operation)
else:
LOG.error("%s failed: %s", options.operation, e)
code = 1
LOG.debug('Exiting, code=%s', code)
return code
if __name__ == '__main__':
sys.exit(main(sys.argv))