anvil/tools/yyoom
Ivan A. Melnikov 493d68d143 Check if transaction failed for some packages
When yum is processing transaction and some failure occures (e.g. prein
script fails for package being installed) yum API does not raise an
exception. Instead, it sets output_state of corresponding transaction
member to TS_FAILED and continues.

With this commit we ensure that such cases are handled correctly. In
particular, when it happens:
- action_type in output is set for 'error' for such packages;
- yyoom prints a message to stderr and exits with non-zero exit status.

Co-authored-by: Joshua Harlow<harlowja@yahoo-inc.com>
Change-Id: Idc3823a4dfa3641122b7037fa3c6ba23b869d72f
2013-08-07 13:39:50 +03:00

346 lines
11 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 json
import logging
import os
import sys
import yum
from contextlib import contextmanager
LOG = logging.getLogger('yyoom')
OUTPUT = None
def _setup_output():
"""Do some nasty manipulations with fds
Yum internals may sometimes write to stdout, just out of a sudden.
To prevent this output form interfering with our JSON, we save
current stdout to other fd via os.dup, and replace fd 1 with
/dev/null opened for writing.
"""
global OUTPUT
# save current stdout for later use
OUTPUT = os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
# close the stream
sys.stdout.close()
# open /dev/null -- all writes to stdout from now on will go there
devnull_fd = os.open(os.devnull, os.O_WRONLY)
if devnull_fd != 1:
os.dup2(devnull_fd, 1)
os.close(devnull_fd)
sys.stdout = os.fdopen(1, 'w')
def _write_output(data):
"""Dump given object as pretty json"""
OUTPUT.write(json.dumps(data, indent=4,
separators=(',', ': '),
sort_keys=True) + '\n')
def _action_type_from_code(action):
if action in yum.constants.TS_INSTALL_STATES:
return 'install'
elif action in yum.constants.TS_REMOVE_STATES:
return 'erase'
elif action == yum.constants.TS_FAILED:
return 'error'
else:
return '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,
aciton_type=_action_type_from_code(action)))
class _OutputtingRPMCallback(_RPMCallback):
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)
_write_output(output)
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 _run(yum_base, options):
"""Handler of `transaction` command
Installs and erases packages, prints what was done in JSON
"""
def parse_package(p):
pkg_info = {}
try:
(name, version) = p.split(',', 1)
pkg_info['name'] = name
if version:
pkg_info['version'] = version
except ValueError:
pkg_info['name'] = p
return pkg_info
log_list(options.erase, title='Erasing packages:')
log_list(options.install, title='Installing packages:')
with _transaction(yum_base, _OutputtingRPMCallback()):
for pkg in options.erase or ():
pkg = parse_package(pkg)
yum_base.remove(**pkg)
for pkg in options.install or ():
pkg = parse_package(pkg)
yum_base.install(**pkg)
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)
_write_output(result)
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):
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')
# TODO(imelnikov): --format
subparsers = parser.add_subparsers(title='subcommands')
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)
parser_run = subparsers.add_parser('transaction',
help='install or remove packages')
parser_run.set_defaults(func=_run)
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_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)
parser_cleanall = subparsers.add_parser('cleanall', help='clean all')
parser_cleanall.set_defaults(func=_cleanall)
return parser.parse_args(args[1:])
def _setup_logging(verbose=True):
"""Initialize logging"""
# setup logging -- put messages to stderr
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter('YYOOM %(levelname)s: %(message)s'))
root_logger = logging.getLogger()
root_logger.addHandler(handler)
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
def _get_yum_base():
base = yum.YumBase()
base.setCacheDir(force=True)
# See: http://lists.baseurl.org/pipermail/yum-devel/2013-January/009873.html
base._override_sigchecks = True
return base
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
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)
try:
_setup_output()
_setup_logging(options.verbose)
return options.func(_get_yum_base(), options) or 0
except Exception as e:
if options.verbose:
raise # let python runtime write stacktrace
sys.stderr.write("Failed: %s\n" % e)
return 1
if __name__ == '__main__':
sys.exit(main(sys.argv))