[Yum-devel] [PATCH] Adds the new yum-cron.py and yum-cron.conf files.

Nick Jacek njacek at redhat.com
Fri Jun 15 21:17:35 UTC 2012


I've been working on rebuilding and upgrading yum-cron.  Currently, yum-cron is implemented as a shell script that runs yum, but it doesn't do things people want, such as provide a way to keep the yum metadata up to date.  Rather than try to modify yum-cron as it is, I think it will be easier and more maintainable to rebuild it more or less from scratch.  I've begun doing so, by heavily modifying the code from yum-updatesd to remove the code to run as a daemon and the dbus integration, and to make it easy to run as a cron job.  This is an early prototype that still needs a good deal of work, especially in the way that messages are sent out. However, I think most of the core functionality, i.e. updating metadata and checking for, downloading, and applying updates, is working.  I'm interested in what people think of this approach, and what features and functionality should or shouldn't be added.
Cheers,
Nick Jacek

---
 new-yum-cron/yum-cron.conf   |   13 +
 new-yum-cron/yum-cron.py     |  605 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 618 insertions(+)
 create mode 100644 new-yum-cron/yum-cron.conf
 create mode 100755 new-yum-cron/yum-cron.py

diff --git a/new-yum-cron/yum-cron.conf   b/new-yum-cron/yum-cron.conf
new file mode 100644
index 0000000..fb396ea
--- /dev/null
+++ b/new-yum-cron/yum-cron.conf
@@ -0,0 +1,13 @@
+[main]
+# how to send notifications (valid: email, syslog, stdio)
+emit_via = stdio, syslog
+
+
+# send messages that updates are available
+update_messages = yes
+# automatically download updates
+download_updates = no
+# automatically download deps of updates
+download_deps = no
+# automatically install updates
+install_updates = no
\ No newline at end of file
diff --git a/new-yum-cron/yum-cron.py b/new-yum-cron/yum-cron.py
new file mode 100755
index 0000000..cd8864e
--- /dev/null
+++ b/new-yum-cron/yum-cron.py
@@ -0,0 +1,605 @@
+#!/usr/bin/python -tt
+
+
+import os
+import sys
+import time
+import gzip
+import dbus
+import dbus.service
+import dbus.glib
+import gobject
+import smtplib
+import threading
+from email.mime.text import MIMEText
+
+import yum
+import yum.Errors
+import syslog
+from yum.config import BaseConfig, Option, IntOption, ListOption, BoolOption
+from yum.parser import ConfigPreProcessor
+from ConfigParser import ConfigParser, ParsingError
+from yum.constants import *
+from yum.update_md import UpdateMetadata
+
+# FIXME: is it really sane to use this from here?
+sys.path.append('/usr/share/yum-cli')
+import callback
+
+config_file = '/etc/yum-cron.conf'
+initial_directory = os.getcwd()
+
+class UpdateEmitter(object):
+    """Abstract class for implementing different types of
+       emitters.
+    """
+
+    def __init__(self):
+        pass
+    def updatesAvailable(self, updateInfo):
+        """Emitted when there are updates available to be installed.
+        If not doing the download here, then called immediately on finding
+        new updates.  If we do the download here, then called after the
+        updates have been downloaded.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        pass
+
+    def updatesDownloading(self, updateInfo):
+        """Emitted to give feedback of update download starting.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        pass
+
+    def updatesApplied(self, updateInfo):
+        """Emitted on successful installation of updates.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        pass
+
+    def updatesFailed(self, errmsgs):
+        """Emitted when an update has failed to install.
+
+        :param errmsgs: a list of error messages
+        """
+        pass
+
+    def checkFailed(self, error):
+        """Emitted when checking for updates failed.
+
+        :param error: an error message
+        """
+        pass
+
+    def setupFailed(self, error, translation_domain):
+        """Emitted when plugin initialization failed.
+
+        :param error: an error message
+        :param translation_domain: the translation domain supplied by
+           the plugin
+        """
+        pass
+
+class SyslogUpdateEmitter(UpdateEmitter):
+    """Emitter class to send messages to syslog."""
+
+    def __init__(self, syslog_facility, ident = "yum-cron",
+                 level = "WARN"):
+        UpdateEmitter.__init__(self)
+        syslog.openlog(ident, 0, self._facilityMap(syslog_facility))
+        self.level = level
+        
+    def updatesAvailable(self, updateInfo):
+        """Emit a message stating that updates are available.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        num = len(updateInfo)
+        level = self.level
+        if num > 1:
+            msg = "%d updates available" %(num,)
+        elif num == 1:
+            msg = "1 update available"
+        else:
+            msg = "No updates available"
+            level = syslog.LOG_DEBUG
+
+        syslog.syslog(self._levelMap(level), msg)
+
+    def _levelMap(self, lvl):
+        level_map = { "EMERG": syslog.LOG_EMERG,
+                      "ALERT": syslog.LOG_ALERT,
+                      "CRIT": syslog.LOG_CRIT,
+                      "ERR": syslog.LOG_ERR,
+                      "WARN": syslog.LOG_WARNING,
+                      "NOTICE": syslog.LOG_NOTICE,
+                      "INFO": syslog.LOG_INFO,
+                      "DEBUG": syslog.LOG_DEBUG }
+        if type(lvl) == int:
+            return lvl
+        return level_map.get(lvl.upper(), syslog.LOG_INFO)
+
+    def _facilityMap(self, facility):
+        facility_map = { "KERN": syslog.LOG_KERN,
+                         "USER": syslog.LOG_USER,
+                         "MAIL": syslog.LOG_MAIL,
+                         "DAEMON": syslog.LOG_DAEMON,
+                         "AUTH": syslog.LOG_AUTH,
+                         "LPR": syslog.LOG_LPR,
+                         "NEWS": syslog.LOG_NEWS,
+                         "UUCP": syslog.LOG_UUCP,
+                         "CRON": syslog.LOG_CRON,
+                         "LOCAL0": syslog.LOG_LOCAL0,
+                         "LOCAL1": syslog.LOG_LOCAL1,
+                         "LOCAL2": syslog.LOG_LOCAL2,
+                         "LOCAL3": syslog.LOG_LOCAL3,
+                         "LOCAL4": syslog.LOG_LOCAL4,
+                         "LOCAL5": syslog.LOG_LOCAL5,
+                         "LOCAL6": syslog.LOG_LOCAL6,
+                         "LOCAL7": syslog.LOG_LOCAL7,}
+        if type(facility) == int:
+            return facility
+        return facility_map.get(facility.upper(), syslog.LOG_DAEMON)
+
+
+class EmailUpdateEmitter(UpdateEmitter):
+    """Emitter class to send messages via email."""
+
+    def __init__(self, sender, rcpt):
+        UpdateEmitter.__init__(self)        
+        self.sender = sender
+        self.rcpt = rcpt
+
+    def updatesAvailable(self, updateInfo):
+        """Emit a message stating that updates are available.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        num = len(updateInfo)
+        if num < 1:
+            return
+
+        output = """
+        Hi,
+        There are %d package updates available. Please run the system
+        updater.
+        
+        Thank You,
+        Your Computer
+        """ % num
+                
+        msg = MIMEText(output)
+        msg['Subject'] = "%d Updates Available" %(num,)
+        msg['From'] = self.sender
+        msg['To'] = ",".join(self.rcpt)
+        s = smtplib.SMTP()
+        s.connect()
+        s.sendmail(self.sender, self.rcpt, msg.as_string())
+        s.close()
+
+class StdIOEmitter(UpdateEmitter):
+    """Emitter class to send messages to syslog."""
+
+    def __init__(self):
+        UpdateEmitter.__init__(self)
+        
+    def updatesAvailable(self, updateInfo):
+        """Emit a message stating that updates are available.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+
+        num = len(updateInfo)
+        if num > 1:
+            msg = "%d updates available" %(num,)
+        elif num == 1:
+            msg = "1 update available"
+        else:
+            msg = "No updates available"
+
+        print msg
+
+    def updatesApplied(self, updateInfo):
+        """Emit a message stating that updates are available.
+
+        :param updateInfo: a list of tuples of dictionaries.  Each
+           dictionary contains information about a package, and each
+           tuple specifies an available upgrade: the second dictionary
+           in the tuple has information about a package that is
+           currently installed, and the first dictionary has
+           information what the package can be upgraded to
+        """
+        num = len(updateInfo)
+        if num > 1:
+            msg = "%d updates applied" %(num,)
+        elif num == 1:
+            msg = "1 update applied"
+        else:
+            msg = "No updates applied"
+        print msg
+
+
+    def updatesFailed(self, errmsgs):
+        print errmsgs
+
+
+class YumCronConfig(BaseConfig):
+    """Class to parse configuration information from the config file, and
+    to store this information.
+    """
+    run_interval = IntOption(3600)
+    nonroot_workdir = Option("/var/tmp/yum-updatesd")
+    emit_via = ListOption(['dbus', 'email', 'syslog','stdio'])
+    email_to = ListOption(["root"])
+    email_from = Option("root")
+    dbus_listener = BoolOption(True)
+    update_messages = BoolOption(False)
+    install_updates = BoolOption(False)
+    download_updates = BoolOption(False)
+    download_deps = BoolOption(False)
+    updaterefresh = IntOption(3600)
+    syslog_facility = Option("DAEMON")
+    syslog_level = Option("WARN")
+    syslog_ident = Option("yum-cron")
+    yum_config_file = Option("/etc/yum.conf")
+
+class YumCronBase(yum.YumBase):
+    """Class to implement the update checking daemon."""
+
+    def __init__(self, opts):
+        """Create a YumCronBase object, and perform initial setup.
+
+        :param opts: :class:`YumCronConfig` object containing the
+           configuration options
+        """
+        yum.YumBase.__init__(self)
+        self.opts = opts
+
+        # Create the emitters, and add them to the list
+        self.emitters = []
+        if 'email' in self.opts.emit_via:
+            self.emitters.append(EmailUpdateEmitter(self.opts.email_from,
+                                                    self.opts.email_to))
+        if 'syslog' in self.opts.emit_via:
+            self.emitters.append(SyslogUpdateEmitter(self.opts.syslog_facility,
+                                                     self.opts.syslog_ident,
+                                                     self.opts.syslog_level))
+        if 'stdio' in self.opts.emit_via:
+            self.emitters.append(StdIOEmitter())
+
+        self.updateInfo = []
+        self.updateInfoTime = None
+
+    def doSetup(self):
+        """Perform set up, including setting up directories and
+        parsing options.
+        """
+        # if we are not root do the special subdir thing
+        if os.geteuid() != 0:
+            if not os.path.exists(self.opts.nonroot_workdir):
+                os.makedirs(self.opts.nonroot_workdir)
+            self.repos.setCacheDir(self.opts.nonroot_workdir)
+
+        # Set the configuration file
+        self.preconf.fn = self.opts.yum_config_file
+
+        self.conf
+
+    def refreshUpdates(self):
+        """Retrieve information about what updates are available."""
+
+        try:
+            # self.doRepoSetup()
+            # self.doSackSetup()
+            self.updateCheckSetup()
+        except Exception, e:
+            syslog.syslog(syslog.LOG_WARNING,
+                          "error getting update info: %s" %(e,))
+            self.emitCheckFailed("%s" %(e,))
+            self.doUnlock()
+            return False
+        return True
+
+    def createTransaction(self):
+        """Build the transaction, and download the packages to be
+        updated.  Finally, call self.processPkgs. This method must be
+        provided by a subclass, and will determine what, if any,
+        action is taken with the packages after they are downloaded.
+        """
+        self.tsInfo.makelists()
+        try:
+            (result, msgs) = self.buildTransaction()
+        except yum.Errors.RepoError, errmsg: # error downloading hdrs
+            msgs = ["Error downloading headers"]
+            self.emitUpdateFailed(msgs)
+            return
+
+        dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
+                                            txmbr.ts_state in ("i", "u"),
+                                            self.tsInfo.getMembers()))
+        self.downloadPkgs(dlpkgs)
+        return dlpkgs
+
+    def processPkgs(self, dlpkgs):
+        """Apply the available updates.
+
+        :param dlpkgs: a list of package objecs to update
+        """
+        for po in dlpkgs:
+            result, err = self.sigCheckPkg(po)
+            if result == 0:
+                continue
+            elif result == 1:
+                try:
+                    self.getKeyForPackage(po)
+                except yum.Errors.YumBaseError, errmsg:
+                    self.emitUpdateFailed([str(errmsg)])
+
+        del self.ts
+        self.initActionTs() # make a new, blank ts to populate
+        self.populateTs(keepold=0)
+        self.ts.check() #required for ordering
+        self.ts.order() # order
+        cb = callback.RPMInstallCallback(output = 0)
+        cb.filelog = True
+            
+        cb.tsInfo = self.tsInfo
+        try:
+            self.runTransaction(cb=cb)
+        except yum.Errors.YumBaseError, err:
+            self.emitUpdateFailed([str(err)])
+
+
+    def populateUpdateMetadata(self):
+        """Populate the metadata for the packages in the update."""
+
+        self.updateMetadata = UpdateMetadata()
+        repos = []
+
+        for (new, old) in self.up.getUpdatesTuples():
+            pkg = self.getPackageObject(new)
+            if pkg.repoid not in repos:
+                repo = self.repos.getRepo(pkg.repoid)
+                repos.append(repo.id)
+                try: # grab the updateinfo.xml.gz from the repodata
+                    md = repo.retrieveMD('updateinfo')
+                except Exception: # can't find any; silently move on
+                    continue
+                md = gzip.open(md)
+                self.updateMetadata.add(md)
+                md.close()
+
+    def populateUpdates(self):
+        """Retrieve and set up information about the updates available
+        for installed packages.
+        """
+        def buildPackageDict(pkg):
+            """Returns a dictionary corresponding to the package object
+            in the form that we can send over the wire for dbus."""
+            pkgDict = {
+                    "name": pkg.name,
+                    "version": pkg.version,
+                    "release": pkg.release,
+                    "epoch": pkg.epoch,
+                    "arch": pkg.arch,
+                    "sourcerpm": pkg.sourcerpm,
+                    "summary": pkg.summary or "",
+            }
+
+            # check if any updateinfo is available
+            md = self.updateMetadata.get_notice((pkg.name, pkg.ver, pkg.rel))
+            if md:
+                # right now we only want to know if it is a security update
+                pkgDict['type'] = md['type']
+
+            return pkgDict
+
+        # if self.up is None:
+        #     # we're _only_ called after updates are setup
+        #     return
+
+        self.populateUpdateMetadata()
+
+        self.updateInfo = []
+        for (new, old) in self.up.getUpdatesTuples():
+            n = buildPackageDict(self.getPackageObject(new))
+            o = buildPackageDict(self.rpmdb.searchPkgTuple(old)[0])
+            self.updateInfo.append((n, o))
+
+        if self.conf.obsoletes:
+            for (obs, inst) in self.up.getObsoletesTuples():
+                n = buildPackageDict(self.getPackageObject(obs))
+                o = buildPackageDict(self.rpmdb.searchPkgTuple(inst)[0])
+                self.updateInfo.append((n, o))
+
+        self.updateInfoTime = time.time()
+
+    def populateTsInfo(self):
+        """Set up information about the update in the tsInfo object."""
+
+        # figure out the updates
+        for (new, old) in self.up.getUpdatesTuples():
+            updating = self.getPackageObject(new)
+            updated = self.rpmdb.searchPkgTuple(old)[0]
+                
+            self.tsInfo.addUpdate(updating, updated)
+
+        # and the obsoletes
+        if self.conf.obsoletes:
+            for (obs, inst) in self.up.getObsoletesTuples():
+                obsoleting = self.getPackageObject(obs)
+                installed = self.rpmdb.searchPkgTuple(inst)[0]
+                
+                self.tsInfo.addObsoleting(obsoleting, installed)
+                self.tsInfo.addObsoleted(installed, obsoleting)
+
+    def updatesCheck(self):
+        """Check to see whether updates are available for any
+        installed packages. If updates are available, install them,
+        download them, or just emit a message, depending on what
+        options are selected in the configuration file.
+ 
+        :return: whether the daemon should continue looping
+        """
+
+        #Perform the initial setup
+
+        try:
+            self.doSetup()
+        except Exception, e:
+            syslog.syslog(syslog.LOG_WARNING,
+                          "error initializing: %s" % e)
+            
+            if isinstance(e, yum.plugins.PluginYumExit):
+                self.emitSetupFailed(e.value, e.translation_domain)
+            else:
+                # if we don't know where the string is from, then assume
+                # it's not marked for translation (versus sending 
+                # gettext.textdomain() and assuming it's from the default
+                # domain for this app)
+                self.emitSetupFailed(str(e))
+
+        #Acquire the yum lock
+        try:
+            self.doLock()
+        except yum.Errors.LockError, e:
+            self.emitLockFailed(str(e))
+        # TODO: make that an actual function
+
+            
+        # try:
+        #     if not self.refreshUpdates():
+        #         return
+        # except yum.Errors.LockError:
+        #     return True # just pass for now
+
+        try:
+            #Retrieve metadata and information about the update
+            self.populateTsInfo()
+            self.populateUpdates()
+
+            if self.opts.install_updates:
+                # Download the update, and apply it
+                dlpkgs = self.createTransaction()
+                self.processPkgs(dlpkgs)
+                self.emitUpdateApplied()
+                #Perform clean up 
+                self.updateInfo = None
+                self.updateInfoTime = None
+        
+            elif self.opts.download_updates:
+                # Just call createTransaction to build the transaction
+                # and download the updates, and emit relevant messages
+                self.emitDownloading()
+                self.createTransaction()
+                self.emitAvailable()
+                
+            elif self.opts.update_messages:
+                # Just emit a message that updates are available
+                self.emitAvailable()
+
+        except Exception, e:
+            self.emitCheckFailed("%s" %(e,))
+        finally:
+            self.releaseLocks()
+        return True
+
+
+    def updateCheckSetup(self):
+        """Set up the transaction set, rpm database, and prepare to
+        get updates.
+        """
+        # self.doTsSetup()
+        # self.doRpmDBSetup()
+        self.doUpdateSetup()
+
+    def releaseLocks(self):
+        """Close the rpm database, and release the yum lock."""
+        self.closeRpmDB()
+        self.doUnlock()
+
+    def emitAvailable(self):
+        """Emit a notice stating whether updates are available."""
+        map(lambda x: x.updatesAvailable(self.updateInfo), self.emitters)
+
+    def emitDownloading(self):
+        """Emit a notice stating that updates are downloading."""
+        map(lambda x: x.updatesDownloading(self.updateInfo), self.emitters)
+
+    def emitUpdateApplied(self):
+        """Emit a notice stating that automatic updates have been applied."""
+        map(lambda x: x.updatesApplied(self.updateInfo), self.emitters)
+
+    def emitUpdateFailed(self, errmsgs):
+        """Emit a notice stating that automatic updates failed."""
+        map(lambda x: x.updatesFailed(errmsgs), self.emitters)
+
+    def emitCheckFailed(self, error):
+        """Emit a notice stating that checking for updates failed."""
+        map(lambda x: x.checkFailed(error), self.emitters)
+
+    def emitSetupFailed(self, error, translation_domain=""):
+        """Emit a notice stating that checking for updates failed."""
+        map(lambda x: x.setupFailed(error, translation_domain), self.emitters)
+
+
+def main(options = None):
+    """Configure and start the daemon."""
+
+    # Create ConfigParser and UDConfig Objects
+    confparser = ConfigParser()
+    opts = YumCronConfig()
+
+    # Attempt to read the config file.  confparser.read will return a
+    # list of the files that were read successfully, so check that it
+    # contains config_file
+    if config_file not in confparser.read(config_file):
+        print >> sys.stderr, "Error reading config file"
+        sys.exit(1)
+
+    # Populate the values into  the opts object
+    opts.populate(confparser, 'main')
+
+    # Create the base object 
+    base = YumCronBase(opts)
+
+    #Run the update check
+    base.updatesCheck()
+
+if __name__ == "__main__":
+    main()
+
+
-- 
1.7.10.2


More information about the Yum-devel mailing list