Alon Bar-Lev has uploaded a new change for review. Change subject: packaging: use yum API ......................................................................
packaging: use yum API PREVIOUS IMPLEMENTATION Use hybrid of yum API, yum cli, rpm cli NEW IMPLEMENTATION Use the yum python API, use single transaction, only top-level components, proper logging. The minyum module is shared between this module and bootstrap. Change-Id: I01c0c10c3bca42770bf05222c8b2b82b01fee0a6 Signed-off-by: Alon Bar-Lev <alo...@redhat.com> --- M Makefile M packaging/fedora/setup/engine-upgrade.py A packaging/fedora/setup/miniyum.py M packaging/fedora/spec/ovirt-engine.spec.in 4 files changed, 997 insertions(+), 265 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/21/8221/1 diff --git a/Makefile b/Makefile index c0ff1ad..c80067a 100644 --- a/Makefile +++ b/Makefile @@ -258,6 +258,7 @@ install -m 644 packaging/fedora/setup/setup_sequences.py $(DESTDIR)$(DATA_DIR)/scripts install -m 644 packaging/fedora/setup/setup_controller.py $(DESTDIR)$(DATA_DIR)/scripts install -m 644 packaging/fedora/setup/common_utils.py $(DESTDIR)$(DATA_DIR)/scripts + install -m 644 packaging/fedora/setup/miniyum.py $(DESTDIR)$(DATA_DIR)/scripts install -m 644 packaging/fedora/setup/output_messages.py $(DESTDIR)$(DATA_DIR)/scripts install -m 644 packaging/fedora/setup/post_upgrade.py $(DESTDIR)$(DATA_DIR)/scripts diff --git a/packaging/fedora/setup/engine-upgrade.py b/packaging/fedora/setup/engine-upgrade.py index c2d43ed..f03aca4 100755 --- a/packaging/fedora/setup/engine-upgrade.py +++ b/packaging/fedora/setup/engine-upgrade.py @@ -5,13 +5,14 @@ import os import shutil import logging +import signal import traceback import types import pwd from optparse import OptionParser -import yum import common_utils as utils import basedefs +from miniyum import MiniYum # Consts #TODO: Work with a real list here @@ -117,6 +118,7 @@ MSG_INFO_UPGRADE_OK = "%s upgrade completed successfully!" % basedefs.APP_NAME MSG_INFO_STOP_INSTALL_EXIT="Upgrade stopped, Goodbye." MSG_INFO_UPDATE_ENGINE_PROFILE="Updating ovirt-engine Profile" +MSG_INSTALL_GPG = "\nApprove installing GPG key userid=%s hexkeyid=%s" MSG_ALERT_STOP_ENGINE="\nDuring the upgrade process, %s will not be accessible.\n\ All existing running virtual machines will continue but you will not be able to\n\ @@ -215,44 +217,80 @@ print MSG_ERROR_USER_NOT_ROOT%(username) sys.exit(1) -class MYum(): +class _miniyumsink(object): + + def _currentfds(self): + return ( + os.dup(sys.stdin.fileno()), + os.dup(sys.stdout.fileno()), + os.dup(sys.stderr.fileno()) + ) + def __init__(self): - self.updated = False - self.yumbase = None - self.upackages = [] - self.ipackages = [] - self.__initbase() - self.tid = None + self._fds = self._currentfds() - def __initbase(self): - self.yumbase = yum.YumBase() - self.yumbase.preconf.disabled_plugins = ['versionlock'] - self.yumbase.conf.cache = False # Do not relay on existing cache - self.yumbase.cleanMetadata() - self.yumbase.cleanSqlite() + def __del__(self): + os.close(self._in) + os.close(self._out) - def _validateRpmLockList(self): - rpmLockList = [] - for rpmName in basedefs.RPM_LOCK_LIST.split(): - cmd = [ - basedefs.EXEC_RPM, "-q", rpmName, - ] - output, rc = utils.execCmd(cmdList=cmd) - if rc == 0: - rpmLockList.append(rpmName) + def verbose(self, msg): + logging.debug("YUM: VERB: %s" % msg) - return rpmLockList + def info(self, msg): + logging.info("YUM: OK: %s" % msg) + + def error(self, msg): + logging.error("YUM: FAIL: %s" % msg) + + def keepAlive(self, msg): + pass + + def askForGPGKeyImport(self, userid, hexkeyid): + logging.warning("YUM: APPROVE-GPG: userid=%s, hexkeyid=%s" % ( + userid, + hexkeyid + )) + save = self._currentfds() + for i in range(3): + os.dup2(self._fds[i], i) + print MSG_INSTALL_GPG % (userid, hexkeyid) + ret = utils.askYesNo(INFO_Q_PROCEED) + for i in range(3): + os.dup2(save[i], i) + return ret + +class MYum(): + + class _transaction(object): + def __init__(self, parent, transaction): + self._parent = parent + self._transaction = transaction + + def __enter__(self): + self._parent._unlock() + self._transaction.__enter__() + + def __exit__(self, exc_type, exc_value, traceback): + self._transaction.__exit__(exc_type, exc_value, traceback) + self._parent._lock() + + def __init__(self, miniyum): + self._miniyum = miniyum def _lock(self): logging.debug("Yum lock started") + pkgs = [ + p['display_name'] for p in + self._miniyum.queryPackages( + patterns=basedefs.RPM_LOCK_LIST.split() + ) + if p['operation'] == 'installed' + ] + # Create RPM lock list - cmd = [ - basedefs.EXEC_RPM, "-q", - ] + self._validateRpmLockList() - output, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg=MSG_ERROR_YUM_LOCK) with open(basedefs.FILE_YUM_VERSION_LOCK, 'a') as yumlock: - yumlock.write(output) + yumlock.write("\n".join(pkgs)) logging.debug("Yum lock completed successfully") def _unlock(self): @@ -270,163 +308,49 @@ fd.close() logging.debug("Yum unlock completed successfully") + def clean(self): + with self._miniyum.transaction(): + self._miniyum.clean() + + def transaction(self): + return MYum._transaction(self, self._miniyum.transaction()) + + def begin(self): + self._miniyum.update(['ovirt-engine']) + self.emptyTransaction = not self._miniyum.buildTransaction() + def update(self): - self.tid = self.getLatestTid(False) - self._unlock() - try: - # yum update ovirt-engine - # TODO: Run test transaction - logging.debug("Yum update started") - cmd = [ - basedefs.EXEC_YUM, "update", "-q", "-y", - ] + RPM_LIST.split() - output, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg=MSG_ERROR_YUM_UPDATE) - logging.debug("Yum update completed successfully") - finally: - self._lock() + self._miniyum.processTransaction() - def updateAvailable(self): - logging.debug("Yum list updates started") + def getPackages(self): + return self._miniyum.queryTransaction() - # Get packages info from yum - rpms = RPM_LIST.split() - logging.debug("Getting list of packages to upgrade") - pkgs = self.yumbase.doPackageLists(patterns=rpms) - upkgs = self.yumbase.doPackageLists(pkgnarrow="updates", patterns=rpms) - - # Save update candidates - if upkgs.updates: - self.upackages = [str(i) for i in sorted(upkgs.updates)] # list of rpm names to update - logging.debug("%s Packages marked for update:"%(len(self.upackages))) - logging.debug(self.upackages) - else: - logging.debug("No packages marked for update") - - # Save installed packages - self.ipackages = [str(i) for i in sorted(pkgs.installed)] # list of rpm names already installed - logging.debug("Installed packages:") - logging.debug(self.ipackages) - - logging.debug("Yum list updated completed successfully") - - - # Return - if upkgs.updates: - return True - else: - return False - - def packageAvailable(self, pkg): - pkglist = self.yumbase.doPackageLists(patterns=[pkg]).available - return len(pkglist) > 0 - - def packageInstalled(self, pkg): - pkglist = self.yumbase.doPackageLists(patterns=[pkg]).installed - return len(pkglist) > 0 - - def depListForRemoval(self, pkgs): - - deplist = [] - - # Create list of all packages to remove - pkgs = self.yumbase.doPackageLists(patterns=pkgs).installed - for pkg in pkgs: - self.yumbase.remove(name=pkg.name) - - # Resolve dependencies for removing packages - self.yumbase.resolveDeps() - - # Create a list of deps packages - for pkg in self.yumbase.tsInfo.getMembers(): - if pkg.isDep: - deplist.append(pkg.name) - - # Clear transactions from the 'self' object - self.yumbase.closeRpmDB() - - # Return the list of dependencies - return deplist - - def rollbackAvailable(self): + def rollbackAvailable(self, packages): logging.debug("Yum rollback-avail started") # Get All available packages in yum rpms = RPM_LIST.split() - pkgs = self.yumbase.pkgSack.returnPackages(patterns=rpms) - available = [str(i) for i in sorted(pkgs)] # list of available rpm names - logging.debug("%s Packages available in yum:"%(len(available))) - logging.debug(available) + pkgs = [ + x['display_name'] for x in + self._miniyum.queryLocalCachePackages(patterns=rpms) + ] + logging.debug("%s Packages available in yum:"%(len(pkgs))) + logging.debug(pkgs) # Verify all installed packages available in yum # self.ipackages is populated in updateAvailable - for installed in self.ipackages: - if installed not in available: + name_packages = [p['name'] for p in packages] + for installed in [ + x['display_name'] for x in + self._miniyum.queryPackages(patterns=rpms) + if x['operation'] == 'installed' + ]: + if installed in name_packages and not installed not in pkgs: logging.debug("%s not available in yum"%(installed)) return False logging.debug("Yum rollback-avail completed successfully") return True - - def rollback(self): - upgradeTid = self.getLatestTid(True) - if int(upgradeTid) <= int(self.tid): - logging.error("Mismatch in yum TID, target TID (%s) is not higher than %s" %(upgradeTid, self.tid)) - raise Exception(MSG_ERROR_YUM_TID) - - if self.updated: - self._unlock() - try: - # yum history undo 17 - # Do rollback only if update went well - logging.debug("Yum rollback started") - cmd = [ - basedefs.EXEC_YUM, "history", "-y", "undo", upgradeTid, - ] - output, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg=MSG_ERROR_YUM_HISTORY_UNDO) - logging.debug("Yum rollback completed successfully") - finally: - self._lock() - else: - logging.debug("No rollback needed") - - def getLatestTid(self, updateOnly=False): - logging.debug("Yum getLatestTid started") - tid = None - - # Get the list - cmd = [ - basedefs.EXEC_YUM, "history", "list", basedefs.ENGINE_RPM_NAME, - ] - output, rc = utils.execCmd(cmdList=cmd, failOnError=True, msg=MSG_ERROR_YUM_HISTORY_LIST) - - # Parse last tid - for line in output.splitlines(): - lsplit = line.split("|") - if len(lsplit) > 3: - if updateOnly: - if 'Update' in lsplit[3].split() or "U" in lsplit[3].split(): - tid = lsplit[0].strip() - break - else: - if "Action" not in lsplit[3]: # Don't get header of output - tid = lsplit[0].strip() - break - if tid is None: - raise ValueError(MSG_ERROR_YUM_HISTORY_GETLAST) - - logging.debug("Found TID: %s" %(tid)) - logging.debug("Yum getLatestTid completed successfully") - return tid - - def isCandidateForUpdate(self, rpm): - candidate = False - for package in self.upackages: - if rpm in package: - candidate = True - return candidate - - def getUpdateCandidates(self): - return self.upackages class DB(): def __init__(self): @@ -577,7 +501,7 @@ print ("[ " + utils.getColoredText(MSG_INFO_ERROR, basedefs.RED) + " ]").rjust(spaceLen+3) raise -def isUpdateRelatedToDb(yumo): +def isUpdateRelatedToDb(packages): """ Verifies current update needs DB manipulation (backup/update/rollback) """ @@ -586,7 +510,7 @@ related = False for rpm in RPM_BACKEND, RPM_DBSCRIPTS: - if yumo.isCandidateForUpdate(rpm): + if rpm in [p['name'] for p in packages]: related = True logging.debug("isUpdateRelatedToDb value is %s"%(str(related))) @@ -681,7 +605,18 @@ return False def main(options): - rhyum = MYum() + # BEGIN: PROCESS-INITIALIZATION + miniyumsink = _miniyumsink() + MiniYum.setup_log_hook(sink=miniyumsink) + extraLog = open(LOG_FILE, "a") + miniyum = MiniYum(sink=miniyumsink, extraLog=extraLog) + miniyum.selinux_role() + # END: PROCESS-INITIALIZATION + + # we do not wish to be interrupted + signal.signal(signal.SIGINT, signal.SIG_IGN) + + rhyum = MYum(miniyum) db = DB() DB_NAME_TEMP = "%s_%s" % (basedefs.DB_NAME, utils.getCurrentDateTime()) @@ -710,116 +645,106 @@ print MSG_ERROR_INCOMPATIBLE_UPGRADE raise Exception(MSG_ERROR_INCOMPATIBLE_UPGRADE) - # Check for upgrade, else exit - print MSG_INFO_CHECK_UPDATE - if not rhyum.updateAvailable(): - logging.debug(MSG_INFO_NO_UPGRADE_AVAIL) - print MSG_INFO_NO_UPGRADE_AVAIL - sys.exit(0) - else: - updates = rhyum.getUpdateCandidates() - print MSG_INFO_UPGRADE_AVAIL % (len(updates)) - for package in updates: - print " * %s" % package + rhyum.clean() + + with rhyum.transaction(): + # Check for upgrade, else exit + runFunc([rhyum.begin], MSG_INFO_CHECK_UPDATE) + if rhyum.emptyTransaction: + logging.debug(MSG_INFO_NO_UPGRADE_AVAIL) + print MSG_INFO_NO_UPGRADE_AVAIL + sys.exit(0) + + packages = rhyum.getPackages() + + print MSG_INFO_UPGRADE_AVAIL % (len(packages)) + for p in packages: + print " * %s" % p['display_name'] if options.check_update: sys.exit(100) - # Check for setup package - if rhyum.isCandidateForUpdate(RPM_SETUP) and not options.force_current_setup_rpm: - logging.debug(MSG_ERROR_NEW_SETUP_AVAIL) - print MSG_ERROR_NEW_SETUP_AVAIL - sys.exit(3) + name_packages = [p['name'] for p in packages] + if RPM_SETUP in name_packages and not options.force_current_setup_rpm: + logging.debug(MSG_ERROR_NEW_SETUP_AVAIL) + print MSG_ERROR_NEW_SETUP_AVAIL + sys.exit(3) - # Make sure we will be able to rollback - if not rhyum.rollbackAvailable() and options.yum_rollback: - logging.debug(MSG_ERROR_NO_ROLLBACK_AVAIL) - print MSG_ERROR_NO_ROLLBACK_AVAIL - print MSG_ERROR_CHECK_LOG%(LOG_FILE) - sys.exit(2) + # Make sure we will be able to rollback + if not rhyum.rollbackAvailable(packages) and options.yum_rollback: + logging.debug(MSG_ERROR_NO_ROLLBACK_AVAIL) + print MSG_ERROR_NO_ROLLBACK_AVAIL + print MSG_ERROR_CHECK_LOG%(LOG_FILE) + sys.exit(2) - # No rollback in this case - try: - # We ask the user before stoping ovirt-engine or take command line option - if options.unattended_upgrade or checkEngine(engineService): - # Stopping engine - runFunc(stopEngineService, MSG_INFO_STOP_ENGINE) - else: - # This means that user chose not to stop ovirt-engine - logging.debug("exiting gracefully") - print MSG_INFO_STOP_INSTALL_EXIT - sys.exit(0) + # No rollback in this case + try: + # We ask the user before stoping ovirt-engine or take command line option + if options.unattended_upgrade or checkEngine(engineService): + # Stopping engine + runFunc(stopEngineService, MSG_INFO_STOP_ENGINE) + else: + # This means that user chose not to stop ovirt-engine + logging.debug("exiting gracefully") + print MSG_INFO_STOP_INSTALL_EXIT + sys.exit(0) - # Backup DB - if isUpdateRelatedToDb(rhyum): - runFunc([db.backup], MSG_INFO_BACKUP_DB) - runFunc([[db.rename, DB_NAME_TEMP]], MSG_INFO_RENAME_DB) + # Backup DB + if isUpdateRelatedToDb(packages): + runFunc([db.backup], MSG_INFO_BACKUP_DB) + runFunc([[db.rename, DB_NAME_TEMP]], MSG_INFO_RENAME_DB) - except Exception as e: - print e - raise + except Exception as e: + print e + raise - # In case of failure, do rollback - try: - # yum update - runFunc(upgradeFunc, MSG_INFO_YUM_UPDATE) + # In case of failure, do rollback + try: + # yum update + runFunc(upgradeFunc, MSG_INFO_YUM_UPDATE) - # If we're here, update/upgrade went fine, so - rhyum.updated = True + # define db connections services + etlService = utils.Service("ovirt-engine-etl") + notificationService = utils.Service("ovirt-engine-notifierd") - # define db connections services - etlService = utils.Service("ovirt-engine-etl") - notificationService = utils.Service("ovirt-engine-notifierd") + # check if update is relevant to db update + if isUpdateRelatedToDb(packages): + stopDbRelatedServices(etlService, notificationService) - # check if update is relevant to db update - if isUpdateRelatedToDb(rhyum): - stopDbRelatedServices(etlService, notificationService) + # Update the db and restore its name back + runFunc([db.update], MSG_INFO_DB_UPDATE) + runFunc([[db.rename, basedefs.DB_NAME]], MSG_INFO_RESTORE_DB) - # Update the db and restore its name back - runFunc([db.update], MSG_INFO_DB_UPDATE) - runFunc([[db.rename, basedefs.DB_NAME]], MSG_INFO_RESTORE_DB) + # Bring up any services we shut down before db upgrade + startDbRelatedServices(etlService, notificationService) - # Bring up any services we shut down before db upgrade - startDbRelatedServices(etlService, notificationService) + # post install conf + runFunc(postFunc, MSG_INFO_RUN_POST) - # post install conf - runFunc(postFunc, MSG_INFO_RUN_POST) + except: + logging.error(traceback.format_exc()) + logging.error("Rolling back update") - except: - logging.error(traceback.format_exc()) - logging.error("Rolling back update") + print MSG_ERROR_UPGRADE + print MSG_INFO_REASON%(sys.exc_info()[1]) - print MSG_ERROR_UPGRADE - print MSG_INFO_REASON%(sys.exc_info()[1]) + # allow db restore + if isUpdateRelatedToDb(packages): + try: + runFunc([db.restore], MSG_INFO_DB_RESTORE) + except: + # This Exception have already been logged, so just pass along + pass - # allow db restore - if isUpdateRelatedToDb(rhyum): - try: - runFunc([db.restore], MSG_INFO_DB_RESTORE) - except: - # This Exception have already been logged, so just pass along - pass + raise - # allow yum rollback even if db restore failed - if options.yum_rollback: - try: - runFunc([rhyum.rollback], MSG_INFO_YUM_ROLLBACK) - except: - # This Exception have already been logged, so just pass along - pass - else: - print MSG_INFO_NO_YUM_ROLLBACK - logging.debug("Skipping yum rollback") + finally: + # start engine + runFunc([startEngine], MSG_INFO_START_ENGINE) - raise - - finally: - # start engine - runFunc([startEngine], MSG_INFO_START_ENGINE) - - # Print log location on success - addAdditionalMessages(etlService.isServiceAvailable()) - print "\n%s\n" % MSG_INFO_UPGRADE_OK - printMessages() + # Print log location on success + addAdditionalMessages(etlService.isServiceAvailable()) + print "\n%s\n" % MSG_INFO_UPGRADE_OK + printMessages() if __name__ == '__main__': try: diff --git a/packaging/fedora/setup/miniyum.py b/packaging/fedora/setup/miniyum.py new file mode 100755 index 0000000..31cf35b --- /dev/null +++ b/packaging/fedora/setup/miniyum.py @@ -0,0 +1,805 @@ +#!/usr/bin/python +# +# Copyright 2012 Red Hat, Inc. +# +# 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Refer to the README and COPYING files for full details of the license +# + +import os +import sys +import logging +import time +import traceback + +import yum + + +from yum.rpmtrans import RPMBaseCallback +from yum.callbacks import PT_MESSAGES, PT_DOWNLOAD_PKGS +from yum.Errors import YumBaseError +from yum.callbacks import DownloadBaseCallback + + +class MiniYum(object): + + class _loghandler(logging.Handler): + """Required for extracting yum log output.""" + + def __init__(self, sink): + logging.Handler.__init__(self) + self._sink = sink + + def emit(self, record): + if self._sink is not None: + self._sink.verbose(record.getMessage()) + + class _yumlogger(logging.Logger): + """Required for hacking yum log.""" + + _sink = None + + def __init__(self, name, level=logging.NOTSET): + logging.Logger.__init__(self, name, level) + + def addHandler(self, hdlr): + if self.name.startswith('yum') or self.name.startswith('rhsm'): + self.handlers = [] + logging.Logger.addHandler( + self, + MiniYum._loghandler( + MiniYum._yumlogger._sink + ) + ) + else: + logging.Logger.addHandler(self, hdlr) + + class _yumlistener(object): + """Required for extracting yum events.""" + + def __init__(self, sink): + self._sink = sink + + def event(self, event, *args, **kwargs): + msg = "Status: " + if event in PT_MESSAGES: + msg += "%s" % PT_MESSAGES[event] + else: + msg += "Unknown(%d)" % event + + if event == PT_DOWNLOAD_PKGS: + msg += " packages:" + for po in args[0]: + msg += " " + MiniYum._get_package_name(po) + + self._sink.verbose(msg) + else: + self._sink.info(msg) + + class _rpmcallback(RPMBaseCallback): + """Required for extracting rpm events.""" + + def __init__(self, sink): + RPMBaseCallback.__init__(self) + self._sink = sink + self._lastaction = None + self._lastpackage = None + + def event( + self, package, action, te_current, te_total, + ts_current, ts_total + ): + if self._lastaction != action or package != self._lastpackage: + self._lastaction = action + self._lastpackage = package + self._sink.info("%s: %u/%u: %s" % ( + self.action[action], ts_current, + ts_total, package)) + + def scriptout(self, package, msgs): + if msgs: + self._sink.verbose("Script sink: " + msgs) + + self._sink.verbose("Done: %s" % (package)) + + def errorlog(self, msg): + self._sink.error(msg) + + def filelog(self, package, action): + RPMBaseCallback.filelog(self, package, action) + + def verify_txmbr(self, base, txmbr, count): + self._sink.info( + "Verify: %u/%u: %s" % ( + count, + len(base.tsInfo), + txmbr + ) + ) + + class _downloadcallback(DownloadBaseCallback): + """Required for extracting progress messages.""" + + def __init__(self, sink): + DownloadBaseCallback.__init__(self) + self._sink = sink + + def updateProgress(self, name, frac, fread, ftime): + msg = "Downloading: %s %s(%d%%)" % ( + name, + fread, + int(float(frac) * 100) + ) + self._sink.verbose(msg) + self._sink.keepAlive(msg) + DownloadBaseCallback.updateProgress(self, name, frac, fread, ftime) + + class _voidsink(object): + def verbose(self, msg): + """verbose log. + + Keyword arguments: + msg -- message to print + + """ + pass + + def info(self, msg): + """info log. + + Keyword arguments: + msg -- message to print + + """ + pass + + def error(self, msg): + """error log. + + Keyword arguments: + msg -- message to print + + """ + pass + + def keepAlive(self, msg): + """keepAlive log. + + Keyword arguments: + msg -- message to print + + """ + pass + + def askForGPGKeyImport(self, userid, hexkeyid): + """Ask for GPG Key import. + + Keyword arguments: + userid -- user + hexkeyid - key id + + return True to accept. + + """ + return False + + class _disable_stdhandles(object): + """Disable stdin/stdout/stderr + + Even after handling all logs, there are + some tools that writes to stderr/stdout!!! + these are not important messages, so we just + ignore for now + + """ + + def __init__(self, rfile=None): + self._refcount = 0 + self._rstdin = os.open(os.devnull, os.O_RDONLY) + if rfile is None: + self._rstdout = os.open(os.devnull, os.O_WRONLY) + self._should_close_rstdout = True + else: + self._rstdout = rfile.fileno() + self._should_close_rstdout = False + + def __del__(self): + os.close(self._rstdin) + if self._should_close_rstdout: + os.close(self._rstdout) + + def __enter__(self): + self._refcount += 1 + if self._refcount == 1: + self._oldfds = [] + + for i in range(3): + self._oldfds.append(os.dup(i)) + if i == 0: + os.dup2(self._rstdin, i) + else: + os.dup2(self._rstdout, i) + + def __exit__(self, exc_type, exc_value, traceback): + self._refcount -= 1 + if self._refcount == 0: + for i in range(len(self._oldfds)): + os.dup2(self._oldfds[i], i) + os.close(self._oldfds[i]) + + class _YumBase(yum.YumBase): + """Require to overrde base functions.""" + + def __init__(self, sink): + yum.YumBase.__init__(self) + + self._sink = sink + self._lastpkg = None + + def _askForGPGKeyImport(self, po, userid, hexkeyid): + return self._sink.askForGPGKeyImport(userid, hexkeyid) + + def verifyPkg(self, fo, po, raiseError): + if self._lastpkg != po: + self._lastpkg = po + self._sink.info( + "Download/Verify: %s" % MiniYum._get_package_name(po) + ) + yum.YumBase.verifyPkg(self, fo, po, raiseError) + + class _MiniYumTransaction(object): + def __init__(self, managed): + self._managed = managed + + def __enter__(self): + self._managed._beginTransaction() + + def __exit__(self, exc_type, exc_value, traceback): + self._managed._endTransaction(exc_type is not None) + + @staticmethod + def _get_package_name(po): + return "%s-%s%s-%s.%s" % ( + po.name, + "%s:" % po.epoch if po.epoch == 0 else "", + po.version, + po.release, + po.arch + ) + + @staticmethod + def _get_package_info(po): + info = {} + info['display_name'] = MiniYum._get_package_name(po) + for f in ( + 'name', + 'version', + 'release', + 'epoch', + 'arch' + ): + info[f] = getattr(po, f) + return info + + @staticmethod + def setup_log_hook(sink=None): + """logging hack for yum. + + Keyword arguments: + sink -- callback sink (default None) + + Yum packages uses logging package + intensively, but we have no clue which + log is used. + What we have done in constructor should have + redirect all output to us. + However, its lazy initialization of the + log handlers, diverse some output to its own + handlers. + So we set our own class to remove the hostile + handlers for the yum loggers. + + Maybe someday this code can be removed. + + Tested: rhel-6.3 + + """ + MiniYum._yumlogger._sink = sink + logging.setLoggerClass(MiniYum._yumlogger) + + def _beginTransaction(self): + """Lock (begin of transaction) + + Need to disbale output as: + Freeing read locks for locker 0x84: 1316/139849637029632 + Freeing read locks for locker 0x86: 1316/139849637029632 + + """ + with self._disableOutput: + self._transactionBase = self._yb.history.last() + self._yb.doLock() + + def _endTransaction(self, rollback=False): + """Unlock (end of transaction).""" + with self._disableOutput: + try: + if rollback: + self._sink.verbose("Performing rollback") + transactionCurrent = self._yb.history.last( + complete_transactions_only=False + ) + if ( + transactionCurrent is not None and + self._transactionBase.tid < transactionCurrent.tid + ): + if ( + transactionCurrent.altered_lt_rpmdb or + transactionCurrent.altered_gt_rpmdb + ): + # safe guard? + pass + else: + try: + self._yb.repos.populateSack( + mdtype='all', + cacheonly=1 + ) + del self._yb.tsInfo + if self._yb.history_undo(transactionCurrent): + if self.buildTransaction(): + self.processTransaction() + finally: + self._yb.repos.populateSack( + mdtype='all', + cacheonly=0 + ) + + except Exception: + self._sink.error('Transaction end failed: %s' % ( + traceback.format_exc() + )) + finally: + self._transactionBase = None + + # forget current transaction + del self._yb.tsInfo + self._yb.doUnlock() + + def _queue(self, action, call, packages, ignoreErrors=False): + ret = True + + with self._disableOutput: + for package in packages: + try: + self._sink.verbose( + "queue package %s for %s" % (package, action) + ) + call(name=package) + self._sink.verbose("package %s queued" % package) + except YumBaseError as e: + ret = False + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + + self._sink.error( + "cannot queue package %s: %s" % (package, msg) + ) + + if not ignoreErrors: + raise + + except Exception as e: + self._sink.error( + "cannot queue package %s: %s" % (package, e) + ) + raise + + return ret + + def __init__(self, sink=None, extraLog=None): + """Constructor. + + Keyword arguments: + sink -- sink to use for interaction. + extraLog -- a File object for stdout/stderr redirection. + + Notes: + extraLog is required in order to collect noise output + of yum going into stdout/stderr directly. + + """ + try: + if sink is None: + self._sink = MiniYum._voidsink() + else: + self._sink = sink + + self._disableOutput = MiniYum._disable_stdhandles(rfile=extraLog) + + self._yb = MiniYum._YumBase(self._sink) + + for l in ('yum', 'rhsm'): + log = logging.getLogger(l) + log.propagate = False + log.handlers = [] + log.addHandler( + MiniYum._loghandler(self._sink) + ) + + self._yb.repos.setProgressBar( + MiniYum._downloadcallback(self._sink) + ) + + except YumBaseError as e: + self._sink.error(str(e.value)) + except Exception as e: + self._sink.error(str(e)) + + def selinux_role(self): + """Setup proper selinux role. + + this must be called at beginning of process + to adjust proper roles for selinux. + it will re-execute the process with same arguments. + + This has similar effect of: + # chcon -t rpm_exec_t miniyum.py + + We must do this dynamic as this class is to be + used at bootstrap stage, so we cannot put any + persistent selinux policy changes. + + """ + + try: + selinux = __import__('selinux', globals(), locals(), [], -1) + except ImportError: + with self.transaction(): + self.install(['libselinux-python']) + if self.buildTransaction(): + self.processTransaction() + + if not 'selinux' in globals(): + selinux = __import__('selinux', globals(), locals(), [], -1) + if selinux.is_selinux_enabled() and "MINIYUM_2ND" not in os.environ: + env = os.environ.copy() + env["MINIYUM_2ND"] = "1" + rc, ctx = selinux.getcon() + if rc != 0: + raise Exception("Cannot get selinux context") + ctx1 = selinux.context_new(ctx) + if not ctx1: + raise Exception("Cannot create selinux context") + if selinux.context_type_set(ctx1, 'rpm_t') != 0: + raise Exception("Cannot set type within selinux context") + if selinux.context_role_set(ctx1, 'system_r') != 0: + raise Exception("Cannot set role within selinux context") + if selinux.context_user_set(ctx1, 'unconfined_u') != 0: + raise Exception("Cannot set user within selinux context") + if selinux.setexeccon(selinux.context_str(ctx1)) != 0: + raise Exception("Cannot set selinux exec context") + os.execve(sys.executable, [sys.executable] + sys.argv, env) + os._exit(1) + + def transaction(self): + """Manage transaction. + + Usage: + with miniyum.transaction(): + do anything + """ + return MiniYum._MiniYumTransaction(self) + + def clean(self): + """Clean yum data.""" + with self._disableOutput: + self._yb.cleanMetadata() + self._yb.cleanPackages() + self._yb.cleanSqlite() + + def install(self, packages, **kwargs): + """Install packages. + + Keyword arguments: + packages -- packages to install. + ignoreErrors - to ignore errors, will return False + + """ + return self._queue("install", self._yb.install, packages, **kwargs) + + def update(self, packages, **kwargs): + """Update packages. + + Keyword arguments: + packages -- packages to install. + ignoreErrors - to ignore errors, will return False + + """ + return self._queue("update", self._yb.update, packages, **kwargs) + + def installUpdate(self, packages, **kwargs): + """Install or update packages. + + Keyword arguments: + packages -- packages to install. + ignoreErrors - to ignore errors, will return False + + """ + return ( + self.install(packages, **kwargs) or + self.update(packages, **kwargs) + ) + + def remove(self, packages, **kwargs): + """Remove packages. + + Keyword arguments: + packages -- packages to install. + ignoreErrors - to ignore errors, will return False + + """ + return self._queue("remove", self._yb.remove, packages, **kwargs) + + def buildTransaction(self): + """Build transaction. + + returns False if empty. + + """ + try: + with self._disableOutput: + ret = False + self._sink.verbose("Building transaction") + rc, msg = self._yb.buildTransaction() + if rc == 0: + self._sink.verbose("Empty transaction") + elif rc == 2: + ret = True + self._sink.verbose("Transaction built") + else: + raise YumBaseError(msg) + + return ret + + except YumBaseError as e: + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + self._sink.error(msg) + raise + + except Exception as e: + self._sink.error(str(e)) + raise + + def queryTransaction(self): + try: + with self._disableOutput: + ret = [] + self._yb.tsInfo.makelists() + for op, l in ( + ('install', self._yb.tsInfo.installed), + ('update', self._yb.tsInfo.updated), + ('install', self._yb.tsInfo.depinstalled), + ('update', self._yb.tsInfo.depupdated), + ): + for p in l: + info = MiniYum._get_package_info(p) + info['operation'] = op + ret.append(info) + return ret + + except YumBaseError as e: + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + self._sink.error(msg) + raise + + except Exception as e: + self._sink.error(str(e)) + raise + + def queryPackages(self, pkgnarrow='all', patterns=None): + try: + with self._disableOutput: + ret = [] + holder = self._yb.doPackageLists( + pkgnarrow=pkgnarrow, + patterns=patterns + ) + for op, l in ( + ('available', holder.available), + ('installed', holder.installed), + ('updates', holder.updates), + ('extras', holder.extras), + ('obsoletes', holder.obsoletes), + ('recent', holder.recent) + ): + for entry in l: + if isinstance(entry, tuple): + info = MiniYum._get_package_info(entry[0]) + info['operation'] = op + ret.append(info) + info = MiniYum._get_package_info(entry[1]) + info['operation'] = 'installed' + ret.append(info) + else: + info = MiniYum._get_package_info(entry) + info['operation'] = op + ret.append(info) + + return ret + + except YumBaseError as e: + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + self._sink.error(msg) + raise + + except Exception as e: + self._sink.error(str(e)) + raise + + def queryLocalCachePackages(self, patterns=None): + try: + with self._disableOutput: + return [ + MiniYum._get_package_info(p) + for p in self._yb.pkgSack.returnPackages(patterns=patterns) + ] + except YumBaseError as e: + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + self._sink.error(msg) + raise + + except Exception as e: + self._sink.error(str(e)) + raise + + def processTransaction(self): + """Process built transaction.""" + + try: + with self._disableOutput: + self._sink.verbose("Processing transaction") + self._yb.processTransaction( + callback=MiniYum._yumlistener(sink=self._sink), + rpmTestDisplay=MiniYum._rpmcallback(sink=self._sink), + rpmDisplay=MiniYum._rpmcallback(sink=self._sink) + ) + self._sink.verbose("Transaction processed") + + except YumBaseError as e: + msg = "" + if type(e.value) is list: + for s in e.value: + msg += str(s) + "\n" + else: + msg = str(e.value) + self._sink.error(msg) + raise + + except Exception as e: + self._sink.error(str(e)) + raise + + +class example(object): + class myminiyumsink(object): + + KEEPALIVE_INTERVAL = 60 + + def __init__(self): + """dup the stdout as during yum operation so we redirect it.""" + self._stream = os.dup(sys.stdout.fileno()) + self._touch() + + def __del__(self): + os.close(self._stream) + + def _touch(self): + self._last = time.time() + + def verbose(self, msg): + os.write(self._stream, ("VERB: -->%s<--\n" % msg).encode('utf-8')) + + def info(self, msg): + self._touch() + os.write(self._stream, ("OK: -->%s<--\n" % msg).encode('utf-8')) + + def error(self, msg): + self._touch() + os.write(self._stream, ("FAIL: -->%s<--\n" % msg).encode('utf-8')) + + def keepAlive(self, msg): + if time.time() - self._last >= \ + example.myminiyumsink.KEEPALIVE_INTERVAL: + self.info(msg) + + def askForGPGKeyImport(self, userid, hexkeyid): + os.write( + self._stream, + ( + "APPROVE-GPG: -->%s-%s<--\n" % (userid, hexkeyid) + ).encode('utf-8') + ) + return True + + @staticmethod + def main(): + # BEGIN: PROCESS-INITIALIZATION + miniyumsink = example.myminiyumsink() + MiniYum.setup_log_hook(sink=miniyumsink) + extraLog = open("/tmp/miniyum.log", "a") + miniyum = MiniYum(sink=miniyumsink, extraLog=extraLog) + miniyum.selinux_role() + # END: PROCESS-INITIALIZATION + + with miniyum.transaction(): + miniyum.clean() + + miniyumsink.info("Search Summary:") + for p in miniyum.queryPackages(patterns=['vdsm']): + miniyumsink.info(" %s - %s" % ( + p['operation'], + p['display_name'] + )) + + with miniyum.transaction(): + miniyum.remove(('cman',), ignoreErrors=True) + miniyum.install(('qemu-kvm-tools',)) + miniyum.installUpdate(('vdsm', 'vdsm-cli')) + if miniyum.buildTransaction(): + miniyumsink.info("Transaction Summary:") + for p in miniyum.queryTransaction(): + miniyumsink.info(" %s - %s" % ( + p['operation'], + p['display_name'] + )) + miniyum.processTransaction() + + try: + with miniyum.transaction(): + miniyum.install(('pcsc-lite',)) + if miniyum.buildTransaction(): + miniyum.processTransaction() + raise Exception("Fail please") + except Exception as e: + if str(e) != "Fail please": + raise + +if __name__ == "__main__": + example.main() diff --git a/packaging/fedora/spec/ovirt-engine.spec.in b/packaging/fedora/spec/ovirt-engine.spec.in index 22f4baa..6f8bb77 100644 --- a/packaging/fedora/spec/ovirt-engine.spec.in +++ b/packaging/fedora/spec/ovirt-engine.spec.in @@ -746,6 +746,7 @@ %{engine_data}/scripts/setup_sequences.py* %{engine_data}/scripts/setup_controller.py* %{engine_data}/scripts/common_utils.py* +%{engine_data}/scripts/miniyum.py* %{engine_data}/scripts/output_messages.py* %{engine_data}/scripts/nfsutils.py* %{engine_data}/scripts/engine-setup.py* -- To view, visit http://gerrit.ovirt.org/8221 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I01c0c10c3bca42770bf05222c8b2b82b01fee0a6 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: master Gerrit-Owner: Alon Bar-Lev <alo...@redhat.com> _______________________________________________ Engine-patches mailing list Engine-patches@ovirt.org http://lists.ovirt.org/mailman/listinfo/engine-patches