[yum-cvs] yum yum-updatesd.py,1.10,1.11

Jeremy Katz katzj at linux.duke.edu
Wed Jun 7 03:31:35 UTC 2006


Update of /home/groups/yum/cvs/yum
In directory login1.linux.duke.edu:/tmp/cvs-serv5730

Modified Files:
	yum-updatesd.py 
Log Message:
lots of work to get this to do more of what's needed for puplet.  
high-level summary:
* add support for automatic downloading of updates when available
* add support for automatic installation of updates when available
* make emitters more object-y so that we can emit more types of signals more
  easily.  dbus is probably the most fleshed out right now
* threads for updates/downloading so that we can continue to interact
  with the daemon while these occur
* little bit more command-line fun for ease-of-debugging


Index: yum-updatesd.py
===================================================================
RCS file: /home/groups/yum/cvs/yum/yum-updatesd.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -r1.10 -r1.11
--- yum-updatesd.py	28 May 2006 12:12:01 -0000	1.10
+++ yum-updatesd.py	7 Jun 2006 03:31:33 -0000	1.11
@@ -13,14 +13,18 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
-# (c)2006 Duke University - written by Seth Vidal
+# (c)2006 Duke University, Red Hat, Inc.
+# Seth Vidal <skvidal at linux.duke.edu>
+# Jeremy Katz <katzj at redhat.com>
 
 #TODO:
 # - add logs and errorlogs below a certain number to send out to syslog
-# - thread it so it can download the updated packages while still answering 
-#     dbus calls
 # - clean up config and work on man page for docs
-
+# - need to be able to cancel downloads.  requires some work in urlgrabber
+# - what to do if we're asked to exit while updates are being applied?
+# - what to do with the lock around downloads/updates
+# - need to not hold the rpmdb open.  probably via the changes in yum to
+#   handle the rpmdb lazily
 
 import os
 import sys
@@ -30,6 +34,9 @@
 import dbus.glib
 import gobject
 import smtplib
+import string
+import time
+import threading
 from optparse import OptionParser
 from email.MIMEText import MIMEText
 
@@ -41,10 +48,111 @@
 from yum.config import BaseConfig, Option, IntOption, ListOption, BoolOption, \
                        IncludingConfigParser
 from yum.constants import *
+from yum.packages import YumInstalledPackage
+
+# FIXME: is it really sane to use this from here?
+import callback
+
 YUM_PID_FILE = '/var/run/yum.pid'
 config_file = '/etc/yum/yum-updatesd.conf'
 
 
+# FIXME: this is kind of gross -- hopefully the rpmdb as a sack stuff will
+# make this not really be needed
+def pkgFromInstalledTuple(pkgtup, rpmdb):
+    return YumInstalledPackage(rpmdb.returnHeaderByTuple(pkgtup)[0])
+
+
+class UpdateEmitter(object):
+    """Abstract object 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."""
+        pass
+    def updatesDownloading(self, updateInfo):
+        """Emitted to give feedback of update download starting."""
+        pass
+    def updatesApplied(self, updateInfo):
+        """Emitted on successful installation of updates."""
+        pass
+    def updatesFailed(self, errmsgs):
+        """Emitted when an update has failed to install."""
+        pass
+
+class SyslogUpdateEmitter(UpdateEmitter):
+    def __init__(self, syslog_facility, ident = "yum-updatesd"):
+        UpdateEmitter.__init__(self)
+        syslog_object = SysLogger(threshold = 10, 
+                                      facility=syslog_facility,
+                                      ident='yum-updatesd')
+        self.syslog = LogContainer([syslog_object])
+        
+    def updatesAvailable(self, updateInfo):
+        num = len(updateInfo)
+        if num > 1:
+            msg = "%d updates available" %(num,)
+        elif num == 1:
+            msg = "1 update available"
+        else:
+            msg = "No updates available"
+
+        self.syslog(0, msg)
+
+class EmailUpdateEmitter(UpdateEmitter):
+    def __init__(self, sender, rcpt):
+        UpdateEmitter.__init__(self)        
+        self.sender = sender
+        self.rcpt = rcpt
+
+    def updatesAvailable(self, updateInfo):
+        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'] = string.join(self.rcpt, ",")
+        s = smtplib.SMTP()
+        s.connect()
+        s.sendmail(self.sender, self.rcpt, msg.as_string())
+        s.close()
+
+class DbusUpdateEmitter(UpdateEmitter):
+    def __init__(self):
+        UpdateEmitter.__init__(self)        
+        bus = dbus.SystemBus()
+        name = dbus.service.BusName('edu.duke.linux.yum', bus = bus)
+        yum_dbus = YumDbusInterface(name)
+        self.dbusintf = yum_dbus
+
+    def updatesAvailable(self, updateInfo):
+        num = len(updateInfo)
+        msg = "%d" %(num,)
+        if num > 0:
+            self.dbusintf.UpdatesAvailableSignal(msg)
+        else:
+            self.dbusintf.NoUpdatesAvailableSignal(msg)
+
+    def updatesFailed(self, errmsgs):
+        self.dbusintf.UpdatesFailedSignal(errmsgs)
+
+    def updatesApplied(self, updinfo):
+        self.dbusintf.UpdatesAppliedSignal(updinfo)
+
 class YumDbusInterface(dbus.service.Object):
     def __init__(self, bus_name, object_path='/UpdatesAvail'):
         dbus.service.Object.__init__(self, bus_name, object_path)
@@ -57,6 +165,13 @@
     def NoUpdatesAvailableSignal(self, message):
         pass
         
+    @dbus.service.signal('edu.duke.linux.yum')
+    def UpdatesFailedSignal(self, errmsgs):
+        pass
+
+    @dbus.service.signal('edu.duke.linux.yum')
+    def UpdatesAppliedSignal(self, updinfo):
+        pass
 
 
 class UDConfig(yum.config.BaseConfig):
@@ -64,22 +179,94 @@
     run_interval = IntOption(3600)
     nonroot_workdir = Option("/var/tmp/yum-updatesd")
     emit_via = ListOption(['dbus', 'email', 'syslog'])
-    email_to = Option("root at localhost")
+    email_to = ListOption(["root at localhost"])
     email_from = Option("yum-updatesd at localhost")
+    dbus_listener = BoolOption(True)
     do_update = BoolOption(False)
+    do_download = BoolOption(False)
+    do_download_deps = BoolOption(False)
+    updaterefresh = IntOption(3600)
     syslog_facility = Option("DAEMON")
     syslog_level = Option("WARN")
     yum_config = Option("/etc/yum.conf")
-    
+
+class UpdateDownloadThread(threading.Thread):
+    def __init__(self, updd, dlpkgs):
+        self.updd = updd
+        self.dlpkgs = dlpkgs
+        threading.Thread.__init__(self, name="UpdateDownloadThread")
+
+    def run(self):
+        self.updd.downloadPkgs(self.dlpkgs)
+        self.updd.emitAvailable()
+        self.updd.closeRpmDB()
+        self.updd.doUnlock(YUM_PID_FILE)
+
+class UpdateInstallThread(threading.Thread):
+    def __init__(self, updd, dlpkgs):
+        self.updd = updd
+        self.dlpkgs = dlpkgs
+        threading.Thread.__init__(self, name="UpdateInstallThread")
+
+    def failed(self, msgs):
+        self.updd.emitUpdateFailed(msgs)
+        self.updd.closeRpmDB()
+        self.updd.doUnlock(YUM_PID_FILE)
+
+    def success(self):
+        self.updd.emitUpdateApplied()
+        self.updd.closeRpmDB()
+        self.updd.doUnlock(YUM_PID_FILE)
+
+        self.updd.updateInfo = None
+        self.updd.updateInfoTime = None        
+        
+    def run(self):
+        self.updd.downloadPkgs(dlpkgs)
+        for po in dlpkgs:
+            rc, err = self.updd.sigCheckPkg(po)
+            if result == 0:
+                continue
+            elif result == 1:
+                try:
+                    self.updd.getKeyForPackage(po)
+                except yum.Errors.YumBaseError, errmsg:
+                    self.failed([errmsg])
+
+        del self.updd.ts
+        self.updd.initActionTs() # make a new, blank ts to populate
+        self.updd.populateTs(keepold=0)
+        self.updd.ts.check() #required for ordering
+        self.updd.ts.order() # order
+        cb = callback.RPMInstallCallback(output = 0)
+            
+        # FIXME: need to do filelog
+        cb.tsInfo = self.updd.tsInfo
+        try:
+            tserrors = self.updd.runTransaction(cb=cb)
+        except yum.Errors.YumBaseError, err:
+            self.failed([err])
+
+        self.success()
 
 class UpdatesDaemon(yum.YumBase):
-    def __init__(self, opts, dbusintf):
+    def __init__(self, opts):
         yum.YumBase.__init__(self)
         self.opts = opts
         self.doSetup()
 
-        self.dbusintf = dbusintf
-        
+        self.emitters = []
+        if 'dbus' in self.opts.emit_via:
+            self.emitters.append(DbusUpdateEmitter())
+        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.conf.syslog_facility))
+
+        self.updateInfo = []
+        self.updateInfoTime = None
+
     def log(self, num, msg):
     #TODO - this should probably syslog
         pass
@@ -95,7 +282,8 @@
                 os.makedirs(self.opts.nonroot_workdir)
             self.repos.setCacheDir(self.opts.nonroot_workdir)
 
-        self.doConfigSetup(fn=self.opts.yum_config)
+        self.doStartupConfig(fn=self.opts.yum_config)
+        self.doConfigSetup()
 
     def refreshUpdates(self):
         self.doLock(YUM_PID_FILE)
@@ -104,30 +292,111 @@
         self.doTsSetup()
         self.doRpmDBSetup()
         self.doUpdateSetup()
+
+    def populateUpdates(self):
+        def getDbusPackageDict(pkg):
+            """Returns a dictionary corresponding to the package object
+            in the form that we can send over the wire for dbus."""
+            return { "name": pkg.returnSimple("name"),
+                     "version": pkg.returnSimple("version"),
+                     "release": pkg.returnSimple("release"),
+                     "epoch": pkg.returnSimple("epoch"),
+                     "arch": pkg.returnSimple("arch"),
+                     "sourcerpm": pkg.returnSimple("sourcerpm"),
+                     "summary": pkg.returnSimple("summary") or "",
+                     }            
         
+        if not hasattr(self, 'up'):
+            # we're _only_ called after updates are setup
+            return
+
+        self.updateInfo = []
+        for (new, old) in self.up.getUpdatesTuples():
+            n = getDbusPackageDict(self.getPackageObject(new))
+            o = getDbusPackageDict(pkgFromInstalledTuple(old, self.rpmdb))
+            self.updateInfo.append((n, o))
+
+        if self.conf.obsoletes:
+            for (obs, inst) in self.up.getObsoletesTuples():
+                n = getDbusPackageDict(self.getPackageObject(obs))
+                o = getDbusPackageDict(pkgFromInstalledTuple(inst, self.rpmdb))
+                self.updateInfo.append((n, o))
+
+        self.updateInfoTime = time.time()
+
+    def populateTsInfo(self):
+        # figure out the updates
+        for (new, old) in self.up.getUpdatesTuples():
+            updating = self.getPackageObject(new)
+            updated = pkgFromInstalledTuple(old, self.rpmdb)
+                
+            self.tsInfo.addUpdate(updating, updated)
+
+        # and the obsoletes
+        if self.conf.obsoletes:
+            for (obs, inst) in self.up.getObsoletesTuples():
+                obsoleting = self.getPackageObject(obs)
+                installed = pkgFromInstalledTuple(inst, self.rpmdb)
+                
+                self.tsInfo.addObsoleting(obsoleting, installed)
+                self.tsInfo.addObsoleted(installed, obsoleting)
+
     def updatesCheck(self):
         try:
             self.refreshUpdates()
         except yum.Errors.LockError:
             return True # just pass for now
 
-        updates = len(self.up.getUpdatesList())
-        obsoletes = len(self.up.getObsoletesTuples())
+        self.populateTsInfo()
+        self.populateUpdates()
 
-        # this should check to see if opts.do_update is true or false
-        # right now just notify something/someone
-        if not self.opts.do_update:
-            num_updates = updates+obsoletes
-            self.emitAvailable(num_updates)
+        # FIXME: this needs to be done in the download/install threads
+        if self.opts.do_update or self.opts.do_download_deps:
+            self.tsInfo.makelists()
+            try:
+                (result, msgs) = self.buildTransaction()
+            except yum.Errors.RepoError, errmsg: # error downloading hdrs
+                (result, msgs) = (1, ["Error downloading headers"])
+
+        dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
+                                            txmbr.ts_state in ("i", "u"),
+                                            self.tsInfo.getMembers()))
+
+        close = True
+        if self.opts.do_update:
+            # we already resolved deps above
+            if result == 1: 
+                self.emitUpdateFailed(msgs)
+            else:
+                uit = UpdateInstallThread(self, dlpkgs)
+                uit.start()
+                close = False
+        elif self.opts.do_download:
+            self.emitDownloading()
+            dl = UpdateDownloadThread(self, dlpkgs)
+            dl.start()
+            close = False
+        else:
+            # just notify about things being available
+            self.emitAvailable()
 
-        self.closeRpmDB()
-        self.doUnlock(YUM_PID_FILE)
+        # FIXME: this is kind of ugly in that I want to do it sometimes
+        # and yet not others and it's from threads that it matters.  aiyee!
+        if close:
+            self.closeRpmDB()
+            self.doUnlock(YUM_PID_FILE)
 
         return True
 
     def getUpdateInfo(self):
-        # try to get the lock up to 10 times to get the explicitly
-        # asked for info
+        # if we have a cached copy, use it
+        if self.updateInfoTime and (time.time() - self.updateInfoTime <
+                                    self.opts.updaterefresh):
+            print "returning cached"
+            return self.updateInfo
+            
+        # try to get the lock so we can update the info.  fall back to
+        # cached if available or try a few times.
         tries = 0
         while tries < 10:
             try:
@@ -135,82 +404,46 @@
                 break
             except yum.Errors.LockError:
                 pass
+            # if we can't get the lock, return what we have if we can
+            if self.updateInfo: return self.updateInfo
             time.sleep(1)
+        if tries == 10:
+            return []
         
         self.doTsSetup()
         self.doRpmDBSetup()
         self.doUpdateSetup()
-        self.doUnlock(YUM_PID_FILE)        
-        return self.up.getUpdatesTuples()
 
-    def emitAvailable(self, num_updates):
-        """method to emit a notice about updates"""
-        if 'dbus' in self.opts.emit_via:
-            self.emit_dbus(num_updates)
-        
-        if 'syslog' in self.opts.emit_via:
-            self.emit_syslog(num_updates)
-        
-        if 'email' in self.opts.emit_via:
-            self.emit_email(num_updates)
+        self.populateUpdates()
 
+        self.closeRpmDB()        
+        self.doUnlock(YUM_PID_FILE)
 
-    def emit_email(self, num_updates):
-        """method to send email for notice of updates"""
-        
-        if num_updates > 0:
-            output = """
-               Hi,
-                There are %d package updates available. Please run the system
-                updater.
-                
-                Thank You,
-                Your Computer
-                
-                """ % num_updates
-                
-            msg = MIMEText(output)
-            subject = 'Updates Available'
-            msg['Subject'] = subject
-            msg['From'] = self.opts.email_from
-            msg['To'] = self.opts.email_to
-            s = smtplib.SMTP()
-            s.connect()
-            s.sendmail(self.opts.email_from, [self.opts.email_to], msg.as_string())
-            s.close()        
-        
-    
-    def emit_syslog(self, num_updates):
-        """method to write to syslog for notice of updates"""
-        syslog_object = SysLogger(threshold = 10, 
-                                      facility=self.conf.syslog_facility,
-                                      ident='yum-updatesd')
-        syslog = LogContainer([syslog_object])
-        
-        if num_updates > 0:
-            msg = "%d update(s) available" % num_updates
-        else:
-            msg = "No Updates Available"
-            
-        syslog(0, msg)
+        return self.updateInfo
 
-    def emit_dbus(self, num_updates):
-        """method to emit a dbus event for notice of updates"""
-        if not self.dbusintf:
-            # FIXME: assert here ?
-            return
-        if num_updates > 0:
-            msg = "%d updates available" % num_updates
-            self.dbusintf.UpdatesAvailableSignal(msg)
-        else:
-            msg = "No Updates Available"
-            self.dbusintf.NoUpdatesAvailableSignal(msg)
+    def emitAvailable(self):
+        """method to emit a notice about updates"""
+        map(lambda x: x.updatesAvailable(self.updateInfo), self.emitters)
+
+    def emitDownloading(self):
+        """method to emit a notice about updates downloading"""
+        print "downloading some updates"
+        map(lambda x: x.updatesDownloading(self.updateInfo), self.emitters)
+
+    def emitUpdateApplied(self):
+        """method to emit a notice when automatic updates applied"""
+        map(lambda x: x.updatesApplied(self.updateInfo), self.emitters)
+
+    def emitUpdateFailed(self, errmsgs):
+        """method to emit a notice when automatic updates failed"""
+        map(lambda x: x.updatesFailed(errmsgs), self.emitters)
 
 class YumDbusListener(dbus.service.Object):
-    def __init__(self, updd, bus_name, object_path='/Updatesd'):
+    def __init__(self, updd, bus_name, object_path='/Updatesd',
+                 allowshutdown = False):
         dbus.service.Object.__init__(self, bus_name, object_path)
         self.updd = updd
-        self.allowshutdown = False
+        self.allowshutdown = allowshutdown
 
     def doCheck(self):
         self.updd.updatesCheck()
@@ -235,16 +468,23 @@
 
     @dbus.service.method("edu.duke.linux.yum")
     def GetUpdateInfo(self):
+        print "GetUpdateInfo"
         # FIXME: should this be async?
         upds = self.updd.getUpdateInfo()
         return upds
+        
 
 def quit(*args):
     sys.exit(0)
 
 def main():
+    # we'll be threading for downloads/updates
+    gobject.threads_init()
+    dbus.glib.threads_init()
+    
     parser = OptionParser()
     parser.add_option("-f", "--no-fork", action="store_true", default=False, dest="nofork")
+    parser.add_option("-r", "--remote-shutdown", action="store_true", default=False, dest="remoteshutdown")    
     (options, args) = parser.parse_args()
 
     if not options.nofork:
@@ -263,23 +503,15 @@
         confparser.read(config_file)
     
     opts.populate(confparser, 'main')
+    updd = UpdatesDaemon(opts)
 
-    if "dbus" in opts.emit_via:
-        # setup the dbus interfaces
+    if opts.dbus_listener:
         bus = dbus.SystemBus()
-
-        name = dbus.service.BusName('edu.duke.linux.yum', bus=bus)
-        yum_dbus = YumDbusInterface(name)
-
-        updd = UpdatesDaemon(opts, yum_dbus)
-        
         name = dbus.service.BusName("edu.duke.linux.yum", bus=bus)
-        object = YumDbusListener(updd, name)
-    else:
-        updd = UpdatesDaemon(opts, None)
+        object = YumDbusListener(updd, name,
+                                 allowshutdown = options.remoteshutdown)
     
     run_interval_ms = opts.run_interval * 1000 # needs to be in ms
-    
     gobject.timeout_add(run_interval_ms, updd.updatesCheck)
 
     mainloop = gobject.MainLoop()




More information about the Yum-cvs-commits mailing list