Change in vdsm[master]: bootstrap: use yum API

Alon Bar-Lev alonbl at redhat.com
Wed Sep 19 01:44:53 UTC 2012


Alon Bar-Lev has uploaded a new change for review.

Change subject: bootstrap: use yum API
......................................................................

bootstrap: use yum API

PREVIOUS IMPLEMENTATION

Use of yum command-line to automate package installation.

Install package almost one by once, so valid status can be reported to
master.

PROBLEMS IN PREVIOUS IMPLEMENTATION

 - As each package was installed separately, conflicts could not be
   resolved.

 - Dependency list should have been maintained, to match dependencies'
   changes over time.

 - Alternate packages, or any alternate dependency trees should have
   been maintained separately.

 - Each execution of yum recalculate the cache, mirrors and
   dependencies, this took time.

 - If another instance is running, yum waits for ever.

NEW IMPLEMENTATION

Use the yum python API, use single transaction, only top-level
components, proper logging.

This implementation resolves all the issue of previous implementation.

Also removing the architecture specific package naming.

PROBLEMS IN NEW IMPLEMENTATION

As it turns out, the yum API is not exactly pure API, it needs a lot
more work especially at log interface, as its lazy use of logs and
direct print of messages to stdout/stderr is not something that is
expected from an API.

The new implementation applies workarounds to these issues, for now we
are good.

CLEANUP

As the vdsm-bootstrap package is to be installed on older engines,
legacy code could not have been removed. The usage of yum from
deployUtils, and the installation functions from the vds_bootstrap.py
could have been removed, ~340 lines of code.

Change-Id: I65796801bc2db7c5abf71c1e9e4ad8ca308138b9
Signed-off-by: Alon Bar-Lev <alonbl at redhat.com>
---
M vds_bootstrap/Makefile.am
A vds_bootstrap/MiniYum.py
M vds_bootstrap/vds_bootstrap.py
3 files changed, 590 insertions(+), 12 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/39/8039/1

diff --git a/vds_bootstrap/Makefile.am b/vds_bootstrap/Makefile.am
index bf9008b..dff60c7 100644
--- a/vds_bootstrap/Makefile.am
+++ b/vds_bootstrap/Makefile.am
@@ -44,6 +44,7 @@
 dist_interface2_SCRIPTS = \
 	vds_bootstrap_complete.py \
 	vds_bootstrap.py \
+	MiniYum.py \
 	setup
 
 nodist_interface2_SCRIPTS = \
diff --git a/vds_bootstrap/MiniYum.py b/vds_bootstrap/MiniYum.py
new file mode 100755
index 0000000..4c51c01
--- /dev/null
+++ b/vds_bootstrap/MiniYum.py
@@ -0,0 +1,470 @@
+#!/usr/bin/python
+
+import os
+import sys
+import yum
+import logging
+
+
+from yum.rpmtrans import RPMBaseCallback
+from yum.callbacks import PT_MESSAGES, PT_DOWNLOAD_PKGS
+from yum.Errors import YumBaseError
+
+
+class MiniYum():
+
+    class _loghandler(logging.Handler):
+        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):
+
+        _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'):
+                logging.Logger.handlers = []
+                logging.Logger.addHandler(
+                    self,
+                    MiniYum._loghandler(
+                        MiniYum._yumlogger._sink
+                    )
+                )
+            else:
+                logging.Logger.addHandler(self, hdlr)
+
+    class _yumlistener():
+        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):
+
+        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 verify_txmbr(self, base, txmbr, count):
+            self._sink.info(
+                "Verify: %u/%u: %s" % (
+                    count,
+                    len(base.tsInfo),
+                    txmbr
+                )
+            )
+
+    class _voidsink():
+        def verbose(self, msg):
+            pass
+
+        def info(self, msg):
+            pass
+
+        def error(self, msg):
+            pass
+
+        def askForGPGKeyImport(self, userid, hexkeyid):
+            return False
+
+    class _disable_stdhandles():
+        """
+            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._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._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):
+            for i in range(len(self._oldfds)):
+                os.dup2(self._oldfds[i], i)
+                os.close(self._oldfds[i])
+
+    class _YumBase(yum.YumBase):
+
+        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)
+
+    @staticmethod
+    def _get_package_name(po):
+        return "%s-%s-%s%s.%s" % (
+            po.name,
+            po.version,
+            po.release,
+            po.epoch,
+            po.arch
+        )
+
+    @staticmethod
+    def setup_log_hook(sink=None):
+        """
+            logging hack for yum.
+
+            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 _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, 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, e:
+                    self._sink.error(
+                        "cannot queue package %s: %s" % (package, str(e))
+                    )
+                    raise
+
+        return ret
+
+    def __init__(self, sink=None, extraLog=None):
+        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)
+                )
+
+        except YumBaseError, e:
+            self._sink.error(str(e.value))
+        except Exception, e:
+            self._sink.error(str(e))
+
+    def __enter__(self):
+        """
+            Call at start of trabsaction.
+
+            Usage:
+                miniyum = MiniYum()
+                with miniyum:
+                    do work
+
+            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._yb.doLock()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """
+            Call at end of trabsaction.
+        """
+        with self._disableOutput:
+            self._yb.doUnlock()
+
+    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:
+            import selinux
+        except:
+            with self:
+                self.install(['libselinux-python'])
+                if self.buildTransaction():
+                    self.processTransaction()
+
+        if not selinux in globals():
+            import selinux
+        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 clean(self):
+        with self._disableOutput:
+            self._yb.cleanMetadata()
+            self._yb.cleanPackages()
+            self._yb.cleanSqlite()
+
+    def install(self, packages, **kwargs):
+        return self._queue("install", self._yb.install, packages, **kwargs)
+
+    def update(self, packages, **kwargs):
+        return self._queue("update", self._yb.update, packages, **kwargs)
+
+    def installUpdate(self, packages, **kwargs):
+        self.install(packages, **kwargs)
+        self.update(packages, **kwargs)
+
+    def remove(self, packages, **kwargs):
+        return self._queue("remove", self._yb.remove, packages, **kwargs)
+
+    def buildTransaction(self):
+        try:
+            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, 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, e:
+            self._sink.error(str(e))
+            raise
+
+    def processTransaction(self):
+        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, 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, e:
+            self._sink.error(str(e))
+            raise
+
+
+##########################################################################
+
+
+class myminiyumsink():
+    def __init__(self):
+        """
+            We dup the stdout as during yum operation
+            we redirect it.
+        """
+        self._stream = None
+        self._stream = os.dup(sys.stdout.fileno())
+
+    def __del__(self):
+        if self._stream is not None:
+            os.close(self._stream)
+
+    def verbose(self, msg):
+        os.write(self._stream, ("VERB: -->%s<--\n" % msg).encode('utf-8'))
+
+    def info(self, msg):
+        os.write(self._stream, ("OK:   -->%s<--\n" % msg).encode('utf-8'))
+
+    def error(self, msg):
+        os.write(self._stream, ("FAIL: -->%s<--\n" % msg).encode('utf-8'))
+
+    def askForGPGKeyImport(self, userid, hexkeyid):
+        os.write(
+            self._stream,
+            ("APPROVE-GPG: -->%s-%s<--\n" % (userid, hexkeyid)).encode('utf-8')
+        )
+        return True
+
+
+def main():
+    gluster = False
+
+    # BEGIN: PROCESS-INITIALIZATION
+    miniyumsink = myminiyumsink()
+    MiniYum.setup_log_hook(miniyumsink)
+    extraLog = open("/tmp/MiniYum.log", "a")
+    miniyum = MiniYum(sink=miniyumsink, extraLog=extraLog)
+    try:
+        miniyum.selinux_role()
+    except Exception, e:
+        print "FAILED: cannot initialize selinux: " + str(e)
+    # END: PROCESS-INITIALIZATION
+
+    with miniyum:
+        miniyum.clean()
+
+    with miniyum:
+        try:
+            miniyum.remove(('cman',), ignoreErrors=True)
+            miniyum.install(('qemu-kvm-tools',))
+            miniyum.installUpdate(('vdsm', 'vdsm-cli'))
+            if gluster:
+                miniyum.install(
+                    ('glusterfs-rdma', 'glusterfs-geo-replication')
+                )
+                miniyum.installUpdate(('vdsm-gluster',))
+            if miniyum.buildTransaction():
+                miniyum.processTransaction()
+        except Exception, e:
+            print "FAILED: " + str(e)
+
+if __name__ == "__main__":
+    main()
diff --git a/vds_bootstrap/vds_bootstrap.py b/vds_bootstrap/vds_bootstrap.py
index 5c8cf84..5ec43b5 100755
--- a/vds_bootstrap/vds_bootstrap.py
+++ b/vds_bootstrap/vds_bootstrap.py
@@ -58,13 +58,66 @@
     LOGDIR=os.environ["OVIRT_LOGDIR"]
 except KeyError:
     LOGDIR=tempfile.gettempdir()
+LOGFILE='%s/vdsm-bootstrap-%s-%s.log' % (LOGDIR, "phase1",
+    strftime("%Y%m%d%H%M%S"))
 logging.basicConfig(level=logging.DEBUG,
                     format='%(asctime)s %(levelname)-8s %(module)s '
                            '%(lineno)d %(message)s',
                     datefmt='%a, %d %b %Y %H:%M:%S',
-                    filename='%s/vdsm-bootstrap-%s-%s.log' %
-                             (LOGDIR, "phase1", strftime("%Y%m%d%H%M%S")),
+                    filename=LOGFILE,
                     filemode='w')
+
+
+class myminiyumsink():
+
+    def __init__(self):
+        """
+            We dup the stdout as during yum operation
+            we redirect it.
+        """
+        self._stream = None
+        self._stream = os.dup(sys.stdout.fileno())
+        self._component = 'PACKAGES'
+        self._group = 'yum'
+
+    def __del__(self):
+        if self._stream is not None:
+            os.close(self._stream)
+
+    def _status(self, status, message):
+        os.write(
+            self._stream,
+            ((
+                "<BSTRAP component='%s' "
+                "status='%s' result='%s' "
+                "message='%s'/>\n"
+            ) % (
+                self._component,
+                status,
+                self._group,
+                message
+            )).encode('utf-8')
+        )
+
+    def verbose(self, msg):
+        logging.debug("MiniYum: VERB:  %s", msg)
+
+    def info(self, msg):
+        logging.info("MiniYum: INFO:  %s", msg)
+        self._status('OK', msg)
+
+    def error(self, msg):
+        logging.error("MiniYum: ERROR: %s", msg)
+
+    def askForGPGKeyImport(self, userid, hexkeyid):
+        msg = "Approving GnuPG key: userid=%s hexkeyid=%s" % (
+            userid,
+            hexkeyid
+        )
+        logging.warning("MiniYum: WARN:  %s", msg)
+        self._status('WARN', msg)
+        return True
+
 
 rhel6based = deployUtil.versionCompare(deployUtil.getOSVersion(), "6.0") >= 0
 
@@ -546,7 +599,6 @@
 
         if not self.rc:
             self._getAllPackages()
-            deployUtil.setService("vdsmd", "stop")
             self._installPackages()
 
         return self.rc
@@ -857,7 +909,8 @@
 # End of deploy class.
 
 def VdsValidation(iurl, subject, random_num, rev_num, orgName, systime,
-        firewallRulesFile, engine_ssh_key, installVirtualizationService, installGlusterService):
+        firewallRulesFile, engine_ssh_key, installVirtualizationService,
+        installGlusterService, miniyum):
     """ --- Check VDS Compatibility.
     """
     logging.debug("Entered VdsValidation(subject = '%s', random_num = '%s', rev_num = '%s', installVirtualizationService = '%s', installGlusterService = '%s')"%(subject, random_num, rev_num, installVirtualizationService, installGlusterService))
@@ -896,14 +949,44 @@
         logging.error('kernelArgs failed')
         return False
 
-    if oDeploy.packagesExplorer():
-        logging.error('packagesExplorer test failed')
-        return False
+    #
+    # stop vdsm at this point,
+    # before any setting is changed.
+    #
+    # stopping vdsm at installation is important
+    # so master will not connect to the old instance
+    # before reboot.
+    #
+    deployUtil.setService("vdsmd", "stop")
 
-    if installGlusterService:
-        if oDeploy.installGlusterPackages():
-            logging.error('installGlusterPackages failed')
+    if miniyum is not None:
+        try:
+            with miniyum:
+                miniyum.clean()
+
+            with miniyum:
+                miniyum.remove(('cman',), ignoreErrors=True)
+                miniyum.install(('qemu-kvm-tools',))
+                miniyum.installUpdate(('vdsm', 'vdsm-cli'))
+
+                if installGlusterService:
+                    miniyum.install(('glusterfs-rdma', 'glusterfs-geo-replication'))
+                    miniyum.installUpdate(('vdsm-gluster',))
+
+                if miniyum.buildTransaction():
+                    miniyum.processTransaction()
+        except:
+            logging.error('package installation failed', exc_info=True)
             return False
+    else:
+        if oDeploy.packagesExplorer():
+            logging.error('packagesExplorer test failed')
+            return False
+
+        if installGlusterService:
+            if oDeploy.installGlusterPackages():
+                logging.error('installGlusterPackages failed')
+                return False
 
     if not oDeploy.createConf():
         logging.error('createConf failed')
@@ -987,10 +1070,34 @@
         print main.__doc__
         return False
 
+    #
+    # miniyum setup must be 1st as process
+    # is probably going to be reexecute with
+    # proper selinux role
+    #
+    miniyum = None
+    if deployUtil.getBootstrapInterfaceVersion() >= 2:
+        try:
+            from MiniYum import MiniYum
+
+            miniyumsink = myminiyumsink()
+            MiniYum.setup_log_hook(miniyumsink)
+            extraLog = open(LOGFILE, "a")
+            miniyum = MiniYum(sink=miniyumsink, extraLog=extraLog)
+            miniyum.selinux_role()
+        except:
+            logging.error("MiniYum selinux setup failed", exc_info=True)
+            print "<BSTRAP component='RHEV_INSTALL' status='FAIL'/>"
+            return False
+
     logging.debug('**** Start VDS Validation ****')
     try:
-        ret = VdsValidation(url, subject, random_num, rev_num,
-                            orgName, systime, firewallRulesFile, engine_ssh_key, installVirtualizationService, installGlusterService)
+        ret = VdsValidation(
+            url, subject, random_num, rev_num, orgName, systime,
+            firewallRulesFile, engine_ssh_key,
+            installVirtualizationService, installGlusterService,
+            miniyum
+        )
     except:
         logging.error("VDS validation failed", exc_info=True)
         logging.error(main.__doc__)


--
To view, visit http://gerrit.ovirt.org/8039
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I65796801bc2db7c5abf71c1e9e4ad8ca308138b9
Gerrit-PatchSet: 1
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Alon Bar-Lev <alonbl at redhat.com>


More information about the vdsm-patches mailing list