[PATCH 2/3] Add anaconda-yum

Brian C. Lane bcl at redhat.com
Thu Mar 28 19:32:17 UTC 2013


From: "Brian C. Lane" <bcl at redhat.com>

Add a helper for installing packages via yum. This script reads a yum
transaction file and a yum config file and executes the installation. It
communicates with the called via stdout and a few string prefixes. This
allows us to run the install in a totally different process.
---
 scripts/Makefile.am  |   2 +-
 scripts/anaconda-yum | 340 +++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/makeupdates  |   2 +
 3 files changed, 343 insertions(+), 1 deletion(-)
 create mode 100755 scripts/anaconda-yum

diff --git a/scripts/Makefile.am b/scripts/Makefile.am
index 3813ee6..8bd6626 100644
--- a/scripts/Makefile.am
+++ b/scripts/Makefile.am
@@ -22,7 +22,7 @@ dist_scripts_SCRIPTS = upd-updates run-anaconda
 dist_scripts_DATA    = pyrc.py
 dist_noinst_SCRIPTS  = upd-kernel makeupdates
 
-dist_bin_SCRIPTS = analog anaconda-cleanup instperf
+dist_bin_SCRIPTS = analog anaconda-cleanup instperf anaconda-yum
 
 stage2scriptsdir = $(datadir)/$(PACKAGE_NAME)
 dist_stage2scripts_SCRIPTS = restart-anaconda
diff --git a/scripts/anaconda-yum b/scripts/anaconda-yum
new file mode 100755
index 0000000..f3b2aea
--- /dev/null
+++ b/scripts/anaconda-yum
@@ -0,0 +1,340 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2013  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Brian C. Lane <bcl at redhat.com>
+#
+import logging
+import os
+import sys
+import argparse
+import rpm
+import rpmUtils
+import yum
+from urlgrabber.grabber import URLGrabError
+
+YUM_PLUGINS = ["blacklist", "whiteout", "fastestmirror", "langpacks"]
+
+def setup_parser():
+    """ Setup argparse with supported arguments
+
+        :rtype: ArgumentParser
+    """
+    parser = argparse.ArgumentParser(description="anaconda-yum")
+    parser.add_argument("-a", "--arch", required=True, help="Arch to install")
+    parser.add_argument("-c", "--config", help="Path to yum config file", default="/tmp/anaconda-yum.conf")
+    parser.add_argument("-t", "--tsfile", required=True, help="Path to yum transaction file")
+    parser.add_argument("-l", "--rpmlog", help="Path to rpm script logfile", default="/tmp/rpm-script.log")
+    parser.add_argument("-r", "--release", required=True, help="Release version")
+    parser.add_argument("-i", "--installroot", help="Path to top directory of installroot", default="/mnt/sysimage")
+    parser.add_argument("-T", "--test", action="store_true", help="Test transaction, don't actually install")
+    parser.add_argument("-d", "--debug", action="store_true", help="Extra debugging output")
+
+    return parser
+
+
+def run_yum_transaction(release, arch, yum_conf, install_root, ts_file, script_log,
+                        testing=False, debug=False):
+    """ Execute a yum transaction loaded from a transaction file
+
+        :param release: The release version to use
+        :type release: string
+        :param arch: The arch to install
+        :type arch: string
+        :param yum_conf: Path to yum config file to use
+        :type yum_conf: string
+        :param install_root: Path to install root
+        :type install_root: string
+        :param ts_file: Path to yum transaction file to load and execute
+        :type ts_file: string
+        :param script_log: Path to file to store rpm script logs in
+        :type script_log: string
+        :param testing: True sets RPMTRANS_FLAG_TEST (default is false)
+        :type testing: bool
+        :returns: Nothing
+
+        This is used to run the yum transaction in a separate process, preventing
+        problems with threads and rpm chrooting during the install.
+    """
+    from yum.Errors import PackageSackError, RepoError, YumBaseError, YumRPMTransError
+
+    # remove some environmental variables that can cause problems with package scripts
+    env_remove = ('DISPLAY', 'DBUS_SESSION_BUS_ADDRESS')
+    [os.environ.pop(k) for k in env_remove if k in os.environ]
+
+    try:
+        # Setup the basics, point to the config file and install_root
+        yb = yum.YumBase()
+        yb.use_txmbr_in_callback = True
+
+        # Set some configuration parameters that don't get set through a config
+        # file.  yum will know what to do with these.
+        # Enable all types of yum plugins. We're somewhat careful about what
+        # plugins we put in the environment.
+        yb.preconf.plugin_types = yum.plugins.ALL_TYPES
+        yb.preconf.enabled_plugins = YUM_PLUGINS
+        yb.preconf.fn = yum_conf
+        yb.preconf.root = install_root
+        yb.preconf.releasever = release
+
+        if debug:
+            yb.preconf.debuglevel = 10
+            yb.preconf.errorlevel = 10
+            yb.preconf.rpmverbosity = "debug"
+
+        # Setup yum cache dir outside the installroot
+        if yb.conf.cachedir.startswith(yb.conf.installroot):
+            root = yb.conf.installroot
+            yb.conf.cachedir = yb.conf.cachedir[len(root):]
+
+        # Load the transaction file and execute it
+        yb.load_ts(ts_file)
+        yb.initActionTs()
+
+        if rpmUtils and rpmUtils.arch.isMultiLibArch():
+            yb.ts.ts.setColor(3)
+
+        print("DEBUG: populate transaction set")
+        try:
+            # uses dsCallback.transactionPopulation
+            yb.populateTs(keepold=0)
+        except RepoError as e:
+            print("ERROR: error populating transaction: %s" % e)
+            print("QUIT:")
+            return
+
+        print("DEBUG: check transaction set")
+        yb.ts.check()
+        print("DEBUG: order transaction set")
+        yb.ts.order()
+        yb.ts.clean()
+
+        # Write scriptlet output to a file to be logged later
+        logfile = open(script_log, "w")
+        yb.ts.ts.scriptFd = logfile.fileno()
+        rpm.setLogFile(logfile)
+
+        # create the install callback
+        rpmcb = RPMCallback(yb, arch, logfile, debug)
+
+        if testing:
+            yb.ts.setFlags(rpm.RPMTRANS_FLAG_TEST)
+
+        print("INFO: running transaction")
+        try:
+            yb.runTransaction(cb=rpmcb)
+        except PackageSackError as e:
+            print("ERROR: PackageSackError: %s" % e)
+        except YumRPMTransError as e:
+            print("ERROR: YumRPMTransError: %s" % e)
+        except YumBaseError as e:
+            print("ERROR: YumBaseError: %s" % e)
+            for error in e.errors:
+                print("ERROR:    %s" % error)
+        else:
+            print("INFO: transaction complete")
+        finally:
+            yb.ts.close()
+            logfile.close()
+    except Exception as e:
+        print("ERROR: transaction error: %s" % e)
+    finally:
+        print("QUIT:")
+
+
+class RPMCallback(object):
+    """ Custom RPMTransaction Callback class. You need one of these to actually
+        make a transaction work.
+        If you subclass it, make sure you preserve the behavior of
+        inst_open_file and inst_close_file, or nothing will actually happen.
+    """
+    callback_map = dict((rpm.__dict__[k], k[12:].lower())
+                         for k in rpm.__dict__
+                         if k.startswith('RPMCALLBACK_'))
+
+    def callback(self, what, amount, total, key, data):
+        """ Handle calling appropriate method, if it exists.
+        """
+        if what not in self.callback_map:
+            print("DEBUG: Ignoring unknown callback number %i", what)
+            return
+        name = self.callback_map[what]
+        func = getattr(self, name, None)
+        if callable(func):
+            return func(amount, total, key, data)
+
+    def __init__(self, yb, arch, log, debug=False):
+        """ :param yb: YumBase object
+            :type yb: YumBase
+            :param log: script logfile
+            :type log: string
+            :param statusQ: status communication back to other process
+            :type statusQ: Queue
+        """
+        self.yb = yb                # yum.YumBase
+        self.base_arch = arch
+        self.install_log = log      # logfile for yum script logs
+        self.debug = debug
+
+        self.package_file = None    # file instance (package file management)
+        self.total_actions = 0
+        self.completed_actions = None   # will be set to 0 when starting tx
+
+    def _get_txmbr(self, key):
+        """ Return a (name, TransactionMember) tuple from cb key. """
+        if hasattr(key, "po"):
+            # New-style callback, key is a TransactionMember
+            txmbr = key
+            name = key.name
+        else:
+            # cleanup/remove error
+            name = key
+            txmbr = None
+
+        return (name, txmbr)
+
+    def trans_start(self, amount, total, key, data):
+        """ Start of the install transaction
+
+            Reset the actions counter and save the total to be completed.
+        """
+        if amount == 6:
+            print("PROGRESS_PREP:")
+        self.total_actions = total
+        self.completed_actions = 0
+
+    def inst_open_file(self, amount, total, key, data):
+        """ Open a file for installation
+
+            :returns: open file descriptor
+            :rtype: int
+        """
+        txmbr = self._get_txmbr(key)[1]
+        if self.debug:
+            print("DEBUG: txmbr = %s" % txmbr)
+
+        # If self.completed_actions is still None, that means this package
+        # is being opened to retrieve a %pretrans script. Don't log that
+        # we're installing the package unless trans_start() has been called.
+        if self.completed_actions is not None:
+            self.completed_actions += 1
+            msg_format = "%s (%d/%d)"
+            progress_package = txmbr.name
+            if txmbr.arch not in ["noarch", self.base_arch]:
+                progress_package = "%s.%s" % (txmbr.name, txmbr.arch)
+
+            progress_msg =  msg_format % (progress_package,
+                                          self.completed_actions,
+                                          self.total_actions)
+            log_msg = msg_format % (txmbr.po,
+                                    self.completed_actions,
+                                    self.total_actions)
+            self.install_log.write(log_msg+"\n")
+            print("PROGRESS_INSTALL: %s" % progress_msg)
+
+        try:
+            repo = self.yb.repos.getRepo(txmbr.po.repoid)
+        except Exception as e:
+            print("ERROR: getRepo failed: %s" % e)
+            raise Exception("rpmcallback getRepo failed")
+
+        self.package_file = None
+        while self.package_file is None:
+            try:
+                # checkfunc gets passed to yum's use of URLGrabber which
+                # then calls it with the file being fetched. verifyPkg
+                # makes sure the checksum matches the one in the metadata.
+                #
+                # From the URLGrab documents:
+                # checkfunc=(function, ('arg1', 2), {'kwarg': 3})
+                # results in a callback like:
+                #   function(obj, 'arg1', 2, kwarg=3)
+                #     obj.filename = '/tmp/stuff'
+                #     obj.url = 'http://foo.com/stuff'
+                checkfunc = (self.yb.verifyPkg, (txmbr.po, 1), {})
+                if self.debug:
+                    print("DEBUG: getPackage %s" % txmbr.name)
+                package_path = repo.getPackage(txmbr.po, checkfunc=checkfunc)
+            except URLGrabError as e:
+                print("ERROR: URLGrabError: %s" % e)
+                raise Exception("rpmcallback failed")
+            except (yum.Errors.NoMoreMirrorsRepoError, IOError) as e:
+                if os.path.exists(txmbr.po.localPkg()):
+                    os.unlink(txmbr.po.localPkg())
+                    print("DEBUG: retrying download of %s" % txmbr.po)
+                    continue
+                print("ERROR: getPackage error: %s" % e)
+                raise Exception("getPackage failed")
+            except yum.Errors.RepoError as e:
+                print("DEBUG: RepoError: %s" % e)
+                continue
+
+            self.package_file = open(package_path)
+
+        if self.debug:
+            print("DEBUG: opening package %s" % self.package_file.name)
+        return self.package_file.fileno()
+
+    def inst_close_file(self, amount, total, key, data):
+        """ close and remove the file
+
+            Update the count of installed packages
+        """
+        package_path = self.package_file.name
+        self.package_file.close()
+        self.package_file = None
+
+        if package_path.startswith(self.yb.conf.cachedir):
+            try:
+                os.unlink(package_path)
+            except OSError as e:
+                print("WARN: unable to remove file %s" % e.strerror)
+
+        # rpm doesn't tell us when it's started post-trans stuff which can
+        # take a very long time.  So when it closes the last package, just
+        # display the message.
+        if self.completed_actions == self.total_actions:
+            print("PROGRESS_POST:")
+
+    def cpio_error(self, amount, total, key, data):
+        name = self._get_txmbr(key)[0]
+        print("ERROR: cpio error with package %s" % name)
+        raise Exception("cpio error")
+
+    def unpack_error(self, amount, total, key, data):
+        name = self._get_txmbr(key)[0]
+        print("ERROR: unpack error with package %s" % name)
+        raise Exception("unpack error")
+
+    def script_error(self, amount, total, key, data):
+        name = self._get_txmbr(key)[0]
+        # Script errors store whether or not they're fatal in "total".
+        if total:
+            print("ERROR: script error with package %s" % name)
+            raise Exception("script error")
+
+
+if __name__ == "__main__":
+    parser = setup_parser()
+    args = parser.parse_args()
+
+    # force output to be flushed
+    sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
+
+    run_yum_transaction(args.release, args.arch, args.config, args.installroot,
+                        args.tsfile, args.rpmlog, args.test, args.debug)
+
diff --git a/scripts/makeupdates b/scripts/makeupdates
index 1ffb166..a0d8366 100755
--- a/scripts/makeupdates
+++ b/scripts/makeupdates
@@ -171,6 +171,8 @@ def copyUpdatedFiles(tag, updates, cwd):
             install_to_dir(file, "usr/share/anaconda/ui")
         elif file.startswith("data/post-scripts/"):
             install_to_dir(file, "usr/share/anaconda/post-scripts")
+        elif file.endswith("anaconda-yum"):
+            install_to_dir(file, "usr/bin")
         elif file.find('/') != -1:
             fields = file.split('/')
             subdir = fields[0]
-- 
1.8.1.4



More information about the anaconda-patches mailing list