[PATCH] Adds the new version of yum-cron.

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This adds three new files: yum-cron.conf, the configuration file,
yum-cron.py, then actual program, and emitters.py the emitters module.
---
 new-yum-cron/emitters.py   |  670 ++++++++++++++++++++++++++++++++++++++++++++
 new-yum-cron/yum-cron.conf |   24 ++
 new-yum-cron/yum-cron.py   |  390 ++++++++++++++++++++++++++
 3 files changed, 1084 insertions(+)
 create mode 100644 new-yum-cron/emitters.py
 create mode 100644 new-yum-cron/yum-cron.conf
 create mode 100755 new-yum-cron/yum-cron.py

diff --git a/new-yum-cron/emitters.py b/new-yum-cron/emitters.py
new file mode 100644
index 0000000..3555982
--- /dev/null
+++ b/new-yum-cron/emitters.py
@@ -0,0 +1,670 @@
+from email.mime.text import MIMEText
+from yum.i18n import to_str, to_utf8, to_unicode, utf8_width, utf8_width_fill, utf8_text_fill
+from yum import  _, P_
+import smtplib
+
+
+class UpdateEmitter(object):
+    """Abstract class for implementing different types of
+       emitters.
+    """
+
+    def __init__(self, opts):
+        self.opts  = opts
+        self.output = []
+
+    def updatesAvailable(self, tsInfo):
+        """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
+        """
+        self.output.append('The following updates are available on %s:' % self.opts.system_name)
+        self.output.append(self._formatTransaction(tsInfo))
+
+    def updatesDownloading(self, tsInfo):
+        """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
+        """
+        self.output.append('The following updates will be downloaded on %s:' % self.opts.system_name)
+        self.output.append(self._formatTransaction(tsInfo))
+
+    def updatesDownloaded(self):
+        """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
+        """
+        self.output.append("Updates downloaded successfully.")
+
+    def updatesInstalling(self, tsInfo):
+        """Emitted to give feedback of update download starting."""
+        self.output.append('The following updates will be applied on %s:' % self.opts.system_name)
+        self.output.append(self._formatTransaction(tsInfo))
+
+    def updatesInstalled(self):
+        """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
+        """
+        self.output.append('The updates were sucessfully applied')
+
+    def setupFailed(self, errmsg):
+        """Emitted when plugin initialization failed.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.output.append("Plugins failed to initialize with the following error message: \n%s" 
+                           % errmsg)
+        self.sendMessages()
+
+    def lockFailed(self, errmsg):
+        """Emitted when plugin initialization failed.
+
+        :param errmsg: a string that contains the error message
+        """
+        self.output.append("Failed to acquire the yum lock with the following error message: \n%s"
+                           % errmsg)
+        self.sendMessages()
+
+    def checkFailed(self, errmsg):
+        """Emitted when checking for updates failed.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.output.append("Failed to check for updates with the following error message: \n%s" 
+                           % errmsg)
+        self.sendMessages()
+
+    def downloadFailed(self, errmsg):
+        """Emitted when an update has failed to install.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.output.append("Updates failed to download with the following error message: \n%s"
+                      % errmsg)
+        self.sendMessages()
+        
+    def updatesFailed(self, errmsg):
+        """Emitted when an update has failed to install.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.output.append("Updates failed to install with the following error message: \n%s"
+                      % errmsg)
+        self.sendMessages()
+
+    def sendMessages(self):
+        """Send the messages that have been stored.  This should be
+        overridden by inheriting classes to emit the messages
+        according to their individual methods.
+        """
+        pass
+
+    def format_number(self, number, SI=0, space=' '):
+        """Return a human-readable metric-like string representation
+        of a number.
+
+        :param number: the number to be converted to a human-readable form
+        :param SI: If is 0, this function will use the convention
+           that 1 kilobyte = 1024 bytes, otherwise, the convention
+           that 1 kilobyte = 1000 bytes will be used
+        :param space: string that will be placed between the number
+           and the SI prefix
+        :return: a human-readable metric-like string representation of
+           *number*
+        """
+        symbols = [ ' ', # (none)
+                    'k', # kilo
+                    'M', # mega
+                    'G', # giga
+                    'T', # tera
+                    'P', # peta
+                    'E', # exa
+                    'Z', # zetta
+                    'Y'] # yotta
+    
+        if SI: step = 1000.0
+        else: step = 1024.0
+    
+        thresh = 999
+        depth = 0
+        max_depth = len(symbols) - 1
+    
+        # we want numbers between 0 and thresh, but don't exceed the length
+        # of our list.  In that event, the formatting will be screwed up,
+        # but it'll still show the right number.
+        while number > thresh and depth < max_depth:
+            depth  = depth + 1
+            number = number / step
+    
+        if type(number) == type(1) or type(number) == type(1L):
+            format = '%i%s%s'
+        elif number < 9.95:
+            # must use 9.95 for proper sizing.  For example, 9.99 will be
+            # rounded to 10.0 with the .1f format string (which is too long)
+            format = '%.1f%s%s'
+        else:
+            format = '%.0f%s%s'
+    
+        return(format % (float(number or 0), space, symbols[depth]))
+
+    def fmtColumns(self, columns, msg=u'', end=u'', text_width=utf8_width):
+        """Return a row of data formatted into a string for output.
+        Items can overflow their columns. 
+
+        :param columns: a list of tuples containing the data to
+           output.  Each tuple contains first the item to be output,
+           then the amount of space allocated for the column, and then
+           optionally a type of highlighting for the item
+        :param msg: a string to begin the line of output with
+        :param end: a string to end the line of output with
+        :param text_width: a function to find the width of the items
+           in the columns.  This defaults to utf8 but can be changed
+           to len() if you know it'll be fine
+        :return: a row of data formatted into a string for output
+        """
+        total_width = len(msg)
+        data = []
+        for col_data in columns[:-1]:
+            (val, width) = col_data
+
+            if not width: # Don't count this column, invisible text
+                msg += u"%s"
+                data.append(val)
+                continue
+
+            (align, width) = self._fmt_column_align_width(width)
+            val_width = text_width(val)
+            if val_width <= width:
+                #  Don't use utf8_width_fill() because it sucks performance
+                # wise for 1,000s of rows. Also allows us to use len(), when
+                # we can.
+                msg += u"%s%s "
+                if (align == u'-'):
+                    data.extend([val, " " * (width - val_width)])
+                else:
+                    data.extend([" " * (width - val_width), val])
+            else:
+                msg += u"%s\n" + " " * (total_width + width + 1)
+                data.append(val)
+            total_width += width
+            total_width += 1
+        (val, width) = columns[-1]
+        (align, width) = self._fmt_column_align_width(width)
+        val = utf8_width_fill(val, width, left=(align == u'-'))
+        msg += u"%%s%s" % end
+        data.append(val)
+        return msg % tuple(data)
+
+    def _calcColumns(self, data, total_width, columns=None, remainder_column=0, indent=''):
+        """Dynamically calculate the widths of the columns that the
+        fields in data should be placed into for output.
+        
+        :param data: a list of dictionaries that represent the data to
+           be output.  Each dictionary in the list corresponds to annn
+           column of output. The keys of the dictionary are the
+           lengths of the items to be output, and the value associated
+           with a key is the number of items of that length.
+        :param total_width: the total width of the output.
+        :param columns: a list containing the minimum amount of space
+           that must be allocated for each row. This can be used to
+           ensure that there is space available in a column if, for
+           example, the actual lengths of the items being output
+           cannot be given in *data*
+        :param remainder_column: number of the column to receive a few
+           extra spaces that may remain after other allocation has
+           taken place
+        :param indent: string that will be prefixed to a line of
+           output to create e.g. an indent
+        :return: a list of the widths of the columns that the fields
+           in data should be placed into for output
+        """
+
+        if total_width is None:
+            total_width = self.term.columns
+
+        cols = len(data)
+        # Convert the data to ascending list of tuples, (field_length, pkgs)
+        pdata = data
+        data  = [None] * cols # Don't modify the passed in data
+        for d in range(0, cols):
+            data[d] = sorted(pdata[d].items())
+
+        #  We start allocating 1 char to everything but the last column, and a
+        # space between each (again, except for the last column). Because
+        # at worst we are better with:
+        # |one two three|
+        # | four        |
+        # ...than:
+        # |one two three|
+        # |            f|
+        # |our          |
+        # ...the later being what we get if we pre-allocate the last column, and
+        # thus. the space, due to "three" overflowing it's column by 2 chars.
+        if columns is None:
+            columns = [1] * (cols - 1)
+            columns.append(0)
+
+        total_width -= (sum(columns) + (cols - 1) +
+                        utf8_width(indent))
+        if not columns[-1]:
+            total_width += 1
+        while total_width > 0:
+            # Find which field all the spaces left will help best
+            helps = 0
+            val   = 0
+            for d in xrange(0, cols):
+                thelps = self._calc_columns_spaces_helps(columns[d], data[d],
+                                                         total_width)
+                if not thelps:
+                    continue
+                #  We prefer to overflow: the last column, and then earlier
+                # columns. This is so that in the best case (just overflow the
+                # last) ... grep still "works", and then we make it prettier.
+                if helps and (d == (cols - 1)) and (thelps / 2) < helps:
+                    continue
+                if thelps < helps:
+                    continue
+                helps = thelps
+                val   = d
+
+            #  If we found a column to expand, move up to the next level with
+            # that column and start again with any remaining space.
+            if helps:
+                diff = data[val].pop(0)[0] - columns[val]
+                if not columns[val] and (val == (cols - 1)):
+                    #  If we are going from 0 => N on the last column, take 1
+                    # for the space before the column.
+                    total_width  -= 1
+                columns[val] += diff
+                total_width  -= diff
+                continue
+
+            overflowed_columns = 0
+            for d in xrange(0, cols):
+                if not data[d]:
+                    continue
+                overflowed_columns += 1
+            if overflowed_columns:
+                #  Split the remaining spaces among each overflowed column
+                # equally
+                norm = total_width / overflowed_columns
+                for d in xrange(0, cols):
+                    if not data[d]:
+                        continue
+                    columns[d] += norm
+                    total_width -= norm
+
+            #  Split the remaining spaces among each column equally, except the
+            # last one. And put the rest into the remainder column
+            cols -= 1
+            norm = total_width / cols
+            for d in xrange(0, cols):
+                columns[d] += norm
+            columns[remainder_column] += total_width - (cols * norm)
+            total_width = 0
+
+        return columns
+
+    @staticmethod
+    def _fmt_column_align_width(width):
+        if width < 0:
+            return (u"-", -width)
+        return (u"", width)
+
+    @staticmethod
+    def _calc_columns_spaces_helps(current, data_tups, left):
+        """ Spaces left on the current field will help how many pkgs? """
+        ret = 0
+        for tup in data_tups:
+            if left < (tup[0] - current):
+                break
+            ret += tup[1]
+        return ret
+
+
+    def _formatTransaction(self, tsInfo):
+        """Return a string containing a human-readable formatted
+        summary of the transaction.
+        
+        :param tsInfo: :class:`yum.transactioninfo.TransactionData`
+           instance that contains information about the transaction
+        :return: a string that contains a formatted summary of the
+           transaction
+           """
+
+        # Sort the packages in the transaction into different lists,
+        # e.g. installed, updated etc
+        tsInfo.makelists(True, True)
+
+        # For each package list, pkglist_lines will contain a tuple
+        # that contains the name of the list, and a list of tuples
+        # with information about each package in the list
+        pkglist_lines = []
+        data  = {'n' : {}, 'v' : {}, 'r' : {}}
+        a_wid = 0 # Arch can't get "that big" ... so always use the max.
+
+
+        def _add_line(lines, data, a_wid, po, obsoletes=[]):
+            # Create a tuple of strings that contain the name, arch,
+            # version, repository, size, and obsoletes of the package
+            # given in po.  Then, append this tuple to lines.  The
+            # strings are formatted so that the tuple can be easily
+            # joined together for output.
+
+            
+            (n,a,e,v,r) = po.pkgtup
+            
+            # Retrieve the version, repo id, and size of the package
+            # in human-readable form
+            evr = po.printVer()
+            repoid = po.ui_from_repo
+            size = self.format_number(float(po.size))
+
+            if a is None: # gpgkeys are weird
+                a = 'noarch'
+
+            lines.append((n, a, evr, repoid, size, obsoletes))
+            #  Create a dict of field_length => number of packages, for
+            # each field.
+            for (d, v) in (("n",len(n)), ("v",len(evr)), ("r",len(repoid))):
+                data[d].setdefault(v, 0)
+                data[d][v] += 1
+            a_wid = max(a_wid, len(a))
+
+            return a_wid
+
+        
+
+        # Iterate through the different groups of packages
+        for (action, pkglist) in [(_('Installing'), tsInfo.installed),
+                            (_('Updating'), tsInfo.updated),
+                            (_('Removing'), tsInfo.removed),
+                            (_('Reinstalling'), tsInfo.reinstalled),
+                            (_('Downgrading'), tsInfo.downgraded),
+                            (_('Installing for dependencies'), tsInfo.depinstalled),
+                            (_('Updating for dependencies'), tsInfo.depupdated),
+                            (_('Removing for dependencies'), tsInfo.depremoved)]:
+            # Create a list to hold the tuples of strings for each package
+            lines = []
+
+            # Append the tuple for each package to lines, and update a_wid
+            for txmbr in pkglist:
+                a_wid = _add_line(lines, data, a_wid, txmbr.po, txmbr.obsoletes)
+
+            # Append the lines instance for this package list to pkglist_lines
+            pkglist_lines.append((action, lines))
+
+        # # Iterate through other package lists
+        # for (action, pkglist) in [(_('Skipped (dependency problems)'),
+        #                            self.skipped_packages),
+        #                           (_('Not installed'), self._not_found_i.values()),
+        #                           (_('Not available'), self._not_found_a.values())]:
+        #     lines = []
+        #     for po in pkglist:
+        #         a_wid = _add_line(lines, data, a_wid, po)
+
+        #     pkglist_lines.append((action, lines))
+
+        if not data['n']:
+            return u''
+        else:
+            # Change data to a list with the correct number of
+            # columns, in the correct order
+            data    = [data['n'],    {}, data['v'], data['r'], {}]
+
+            
+             
+            # Calculate the space needed for each column
+            columns = [1,         a_wid,         1,         1,  5]
+
+            columns = self._calcColumns(data, self.opts.output_width,
+                                        columns, remainder_column = 2, indent="  ")
+
+            (n_wid, a_wid, v_wid, r_wid, s_wid) = columns
+            assert s_wid == 5
+
+            # out will contain the output as a list of strings, that
+            # can be later joined together
+            out = [u"""
+%s
+%s
+%s
+""" % ('=' * self.opts.output_width,
+       self.fmtColumns(((_('Package'), -n_wid), (_('Arch'), -a_wid),
+                        (_('Version'), -v_wid), (_('Repository'), -r_wid),
+                        (_('Size'), s_wid)), u" "),
+       '=' * self.opts.output_width)]
+
+        # Add output for each package list in pkglist_lines
+        for (action, lines) in pkglist_lines:
+            #If the package list is empty, skip it
+            if not lines:
+                continue
+
+            # Add the name of the package list
+            totalmsg = u"%s:\n" % action
+            # Add a line of output about an individual package
+            for (n, a, evr, repoid, size, obsoletes) in lines:
+                columns = ((n,   -n_wid), (a,      -a_wid),
+                           (evr, -v_wid), (repoid, -r_wid), (size, s_wid))
+                msg = self.fmtColumns(columns, u" ", u"\n")
+                for obspo in sorted(obsoletes):
+                    appended = _('     replacing  %s.%s %s\n')
+                    appended %= (obspo.name,
+                                 obspo.arch, obspo.printVer())
+                    msg = msg+appended
+                totalmsg = totalmsg + msg
+
+            # Append the line about the individual package to out
+            out.append(totalmsg)
+
+        # Add a summary of the transaction
+        out.append(_("""
+Transaction Summary
+%s
+""") % ('=' * self.opts.output_width))
+        summary_data =  (
+            (_('Install'), len(tsInfo.installed),
+             len(tsInfo.depinstalled)),
+            (_('Upgrade'), len(tsInfo.updated),
+             len(tsInfo.depupdated)),
+            (_('Remove'), len(tsInfo.removed),
+             len(tsInfo.depremoved)),
+            (_('Reinstall'), len(tsInfo.reinstalled), 0),
+            (_('Downgrade'), len(tsInfo.downgraded), 0),
+            # (_('Skipped (dependency problems)'), len(self.skipped_packages), 0),
+            # (_('Not installed'), len(self._not_found_i.values()), 0),
+            # (_('Not available'), len(self._not_found_a.values()), 0),
+        )
+        max_msg_action   = 0
+        max_msg_count    = 0
+        max_msg_pkgs     = 0
+        max_msg_depcount = 0
+        for action, count, depcount in summary_data:
+            if not count and not depcount:
+                continue
+
+            msg_pkgs = P_('Package', 'Packages', count)
+            len_msg_action   = utf8_width(action)
+            len_msg_count    = utf8_width(str(count))
+            len_msg_pkgs     = utf8_width(msg_pkgs)
+
+            if depcount:
+                len_msg_depcount = utf8_width(str(depcount))
+            else:
+                len_msg_depcount = 0
+
+            max_msg_action   = max(len_msg_action,   max_msg_action)
+            max_msg_count    = max(len_msg_count,    max_msg_count)
+            max_msg_pkgs     = max(len_msg_pkgs,     max_msg_pkgs)
+            max_msg_depcount = max(len_msg_depcount, max_msg_depcount)
+
+        for action, count, depcount in summary_data:
+            msg_pkgs = P_('Package', 'Packages', count)
+            if depcount:
+                msg_deppkgs = P_('Dependent package', 'Dependent packages',
+                                 depcount)
+                if count:
+                    msg = '%s  %*d %s (+%*d %s)\n'
+                    out.append(msg % (utf8_width_fill(action, max_msg_action),
+                                      max_msg_count, count,
+                                      utf8_width_fill(msg_pkgs, max_msg_pkgs),
+                                      max_msg_depcount, depcount, msg_deppkgs))
+                else:
+                    msg = '%s  %*s %s ( %*d %s)\n'
+                    out.append(msg % (utf8_width_fill(action, max_msg_action),
+                                      max_msg_count, '',
+                                      utf8_width_fill('', max_msg_pkgs),
+                                      max_msg_depcount, depcount, msg_deppkgs))
+            elif count:
+                msg = '%s  %*d %s\n'
+                out.append(msg % (utf8_width_fill(action, max_msg_action),
+                                  max_msg_count, count, msg_pkgs))
+
+        return ''.join(out)
+
+
+class EmailEmitter(UpdateEmitter):
+    """Emitter class to send messages via email."""
+
+    def __init__(self, opts):
+        super(EmailEmitter, self).__init__(opts)        
+        self.subject = ""
+
+    def updatesAvailable(self, tsInfo):
+        """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
+        """
+        super(EmailEmitter, self).updatesAvailable(tsInfo)
+        self.subject = "Yum: Updates Available on %s" % self.opts.system_name
+
+    def updatesDownloaded(self):
+        """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
+        """
+        self.subject = "Yum: Updates downloaded on %s" % self.opts.system_name
+        super(EmailEmitter, self).updatesDownloaded()
+
+    def updatesInstalled(self):
+        """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
+        """
+        self.subject = "Yum: Updates installed on %s" % self.opts.system_name
+        super(EmailEmitter, self).updatesInstalled()
+
+    def setupFailed(self, errmsg):
+        """Emitted when plugin initialization failed.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.subject = "Yum: Failed to perform setup on %s" % self.opts.system_name
+        super(EmailEmitter, self).setupFailed(errmsg)
+
+    def lockFailed(self, errmsg):
+        """Emitted when plugin initialization failed.
+
+        :param errmsg: a string that contains the error message
+        """
+        self.subject = "Yum: Failed to  acquire the yum lock on %s" % self.opts.system_name
+        super(EmailEmitter, self).lockFailed(errmsg)
+
+    def checkFailed(self, errmsg):
+        """Emitted when checking for updates failed.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.subject = "Yum: Failed to check for updates on %s" % self.opts.system_name
+        super(EmailEmitter, self).checkFailed(errmsg)
+
+    def downloadFailed(self, errmsg):
+        """Emitted when plugin initialization failed.
+
+        :param errmsg: a string that contains the error message
+        """
+        self.subject = "Yum: Failed to download updates on %s" % self.opts.system_name
+        super(EmailEmitter, self).lockFailed(errmsg)
+
+    def updatesFailed(self, errmsg):
+        """Emitted when an update has failed to install.
+
+        :param errmsgs: a string that contains the error message
+        """
+        self.subject = "Yum: Failed to install updates on %s" % self.opts.system_name
+        super(EmailEmitter, self).updatesFailed(errmsg)
+        
+    def sendMessages(self):
+        """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
+        """
+        # Build up the email to be sent
+        msg = MIMEText(''.join(self.output))
+        msg['Subject'] = self.subject
+        msg['From'] = self.opts.email_from
+        msg['To'] = ",".join(self.opts.email_to)
+
+        # Send the email
+        s = smtplib.SMTP()
+        s.connect(self.opts.email_host)
+        s.sendmail(self.opts.email_from, self.opts.email_to, msg.as_string())
+        s.close()
+
+
+class StdIOEmitter(UpdateEmitter):
+    """Emitter class to send messages to syslog."""
+
+    def __init__(self, opts):
+        super(StdIOEmitter, self).__init__(opts)
+        
+    def sendMessages(self) :
+        print "".join(self.output)
diff --git a/new-yum-cron/yum-cron.conf b/new-yum-cron/yum-cron.conf
new file mode 100644
index 0000000..93f80bb
--- /dev/null
+++ b/new-yum-cron/yum-cron.conf
@@ -0,0 +1,24 @@
+[main]
+# how to send notifications (valid: email, stdio)
+emit_via = stdio
+
+# Name to use for this computer.  If system_name is None, the hostname
+# will be used
+system_name = None
+
+# How wide the output should be formatted to, in characters
+ouput_width = 80
+
+# send messages that updates are available
+update_messages = yes
+# automatically download updates
+download_updates = no
+# automatically install updates
+install_updates = no
+
+# address to send email messages from
+email_from = root
+# List of addresses to send messages to
+email_to = root
+# Name of the host to connect to to send email messages
+email_host = localhost
diff --git a/new-yum-cron/yum-cron.py b/new-yum-cron/yum-cron.py
new file mode 100755
index 0000000..4f74776
--- /dev/null
+++ b/new-yum-cron/yum-cron.py
@@ -0,0 +1,390 @@
+#!/usr/bin/python -tt
+import os
+import sys
+import time
+import gzip
+from socket import gethostname
+
+import yum
+import yum.Errors
+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
+import emitters
+
+
+# FIXME: is it really sane to use this from here?
+sys.path.append('/usr/share/yum-cli')
+import callback
+
+
+config_file = '/home/nick/yum/new-yum-cron/yum-cron.conf'
+initial_directory = os.getcwd()
+
+
+class YumCronConfig(BaseConfig):
+    """Class to parse configuration information from the config file, and
+    to store this information.
+    """
+    nonroot_workdir = Option("/var/tmp/yum-cron")
+    system_name = Option(gethostname())
+    output_width = IntOption(80)
+    emit_via = ListOption(['email','stdio'])
+    email_to = ListOption(["root"])
+    email_from = Option("root")
+    email_host = Option("localhost")
+    email_port = IntOption(25)
+    update_messages = BoolOption(False)
+    install_updates = BoolOption(False)
+    download_updates = BoolOption(False)
+    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(emitters.EmailEmitter(self.opts))
+        if 'stdio' in self.opts.emit_via:
+            self.emitters.append(emitters.StdIOEmitter(self.opts))
+
+        self.updateInfo = []
+        self.updateInfoTime = None
+
+    def doSetup(self):
+        """Perform set up, including setting up directories and
+        parsing options.
+
+        :return: boolean that indicates whether setup completes
+           successfully
+        """
+        try :
+            # Set the configuration file
+            self.preconf.fn = self.opts.yum_config_file
+
+            # 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)
+
+            # Create the configuration
+            self.conf
+
+        except Exception, e:
+            # If there are any exceptions, send a message about them,
+            # and return False
+            self.emitSetupFailed('%s' % e)
+            return False
+
+        else:
+            # If there are no errors, return True
+            return True
+
+    def acquireLock(self):
+        """ Wrapper method around doLock to emit errors correctly.
+        
+        :return: whether the lock was acquired successfully
+        """
+        try:
+            self.doLock()
+        except yum.Errors.LockError, e:
+            self.emitLockFailed("%s" % e)
+            return False
+        else:
+            return True
+
+
+    def findDeps(self):
+        try:
+            (res, resmsg) = self.buildTransaction()
+        except yum.Errors.RepoError, e:
+            self.emitCheckFailed("%s" %(e,))
+            return False
+        if res != 2:
+            self.emitCheckFailed("Failed to build transaction: %s" %(str.join("\n", resmsg),))
+            return False
+        return True
+
+    def downloadUpdates(self, emit):
+        # Emit a message that that updates will be downloaded
+        if emit :
+            self.emitDownloading()
+        dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
+                                            txmbr.ts_state in ("i", "u"),
+                                            self.tsInfo.getMembers()))
+        try:
+            # Download the updates
+            self.downloadPkgs(dlpkgs)
+        except Exception, e:
+            self.emitDownloadFailed("%s" % e)
+            return False
+        else :
+            # Emit a message that the packages have been downloaded
+            # successfully
+            if emit :
+                self.emitDownloaded()
+                self.emitMessages()
+        return True
+
+    def installUpdates(self, emit):
+        """Apply the available updates.
+
+        """
+        # Emit a message  that 
+        if emit :
+            self.emitInstalling()
+
+        dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
+                                            txmbr.ts_state in ("i", "u"),
+                                            self.tsInfo.getMembers()))
+
+        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)])
+                    return False
+
+        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)])
+            return False
+
+        self.emitInstalled()
+        self.emitMessages()
+        return True
+
+    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()
+
+        # 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 refreshUpdates(self):
+        try:
+            self.populateUpdates()
+        except Exception, e:
+            self.emitCheckFailed("%s" %(e,))
+            return False
+        return True
+
+
+    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
+        if not self.doSetup():
+            sys.exit(1)
+
+        # Acquire the yum lock
+        if not self.acquireLock():
+            sys.exit(1)
+
+        # Find what packages should be updated
+        if not self.refreshUpdates():
+            sys.exit(1)
+
+        # Build the transaction to find the additional dependencies
+        if not self.findDeps():
+            sys.exit(1)
+
+        # download if set up to do so, else tell about the updates and exit
+        if not self.opts.download_updates:
+            self.emitAvailable()
+            self.emitMessages()
+            self.releaseLocks()
+            sys.exit(0)
+
+        if not self.downloadUpdates(not self.opts.install_updates):
+            sys.exit(1)
+
+        # now apply if we're set up to do so; else just tell that things are
+        # available
+        if not self.opts.install_updates:
+            self.releaseLocks()
+            sys.exit(0)
+
+        if not self.installUpdates(True):
+            sys.exit(1)
+
+        self.releaseLocks()
+        sys.exit(0)
+
+
+    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.tsInfo), self.emitters)
+
+    def emitDownloading(self):
+        """Emit a notice stating that updates are downloading."""
+        print "Emit Downloading"
+        map(lambda x: x.updatesDownloading(self.tsInfo), self.emitters)
+
+    def emitDownloaded(self):
+        """Emit a notice stating that updates have downloaded."""
+        map(lambda x: x.updatesDownloaded(), self.emitters)
+
+    def emitInstalling(self):
+        """Emit a notice stating that automatic updates are about to
+        be applied.
+        """
+        map(lambda x: x.updatesInstalling(self.tsInfo), self.emitters)
+
+    def emitInstalled(self):
+        """Emit a notice stating that automatic updates have been applied."""
+        map(lambda x: x.updatesInstalled(), self.emitters)
+
+    def emitSetupFailed(self, error):
+        """Emit a notice stating that checking for updates failed."""
+        map(lambda x: x.setupFailed(error), self.emitters)
+
+    def emitLockFailed(self, errmsg):
+        """Emit a notice that we failed to acquire the yum lock."""
+        map(lambda x: x.lockFailed(errmsg), self.emitters)
+
+    def emitCheckFailed(self, error):
+        """Emit a notice stating that checking for updates failed."""
+        map(lambda x: x.checkFailed(error), self.emitters)
+
+    def emitDownloadFailed(self, error):
+        """Emit a notice stating that downloading the updates failed."""
+        map(lambda x: x.downloadFailed(error), self.emitters)
+
+    def emitUpdateFailed(self, errmsgs):
+        """Emit a notice stating that automatic updates failed."""
+        map(lambda x: x.updatesFailed(errmsgs), self.emitters)
+
+    def emitMessages(self):
+        """Emit the messages from the emitters."""
+        map(lambda x: x.sendMessages(), 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')
+
+    #If the system name is not given, set it by getting the hostname
+    if opts.system_name == 'None' :
+        opts.system_name = gethostname()
+
+    # Create the base object 
+    base = YumCronBase(opts)
+
+    #Run the update check
+    base.updatesCheck()
+
+if __name__ == "__main__":
+    main()
-- 
1.7.10.4

_______________________________________________
Yum mailing list
Yum@xxxxxxxxxxxxxxxxx
http://lists.baseurl.org/mailman/listinfo/yum


[Index of Archives]     [Fedora Users]     [Fedora Legacy List]     [Fedora Maintainers]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]

  Powered by Linux