[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