source: yum/trunk/yum-updatesd.py@ 6

Last change on this file since 6 was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

  • Property svn:eol-style set to native
File size: 21.8 KB
Line 
1#!/usr/bin/python -tt
2# This program is free software; you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation; either version 2 of the License, or
5# (at your option) any later version.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15
16# (c)2006 Duke University, Red Hat, Inc.
17# Seth Vidal <skvidal@linux.duke.edu>
18# Jeremy Katz <katzj@redhat.com>
19
20#TODO:
21# - clean up config and work on man page for docs
22# - need to be able to cancel downloads. requires some work in urlgrabber
23# - what to do if we're asked to exit while updates are being applied?
24# - what to do with the lock around downloads/updates
25
26# since it takes me time everytime to figure this out again, here's how to
27# queue a check with dbus-send. adjust appropriately for other methods
28# $ dbus-send --system --print-reply --type=method_call \
29# --dest=edu.duke.linux.yum /Updatesd edu.duke.linux.yum.CheckNow
30
31import os
32import sys
33import time
34import gzip
35import dbus
36import dbus.service
37import dbus.glib
38import gobject
39import smtplib
40import threading
41from optparse import OptionParser
42from email.mime.text import MIMEText
43
44
45
46import yum
47import yum.Errors
48import syslog
49from yum.config import BaseConfig, Option, IntOption, ListOption, BoolOption
50from yum.parser import ConfigPreProcessor
51from ConfigParser import ConfigParser, ParsingError
52from yum.constants import *
53from yum.update_md import UpdateMetadata
54
55# FIXME: is it really sane to use this from here?
56sys.path.append('/usr/share/yum-cli')
57import callback
58
59config_file = '/etc/yum/yum-updatesd.conf'
60initial_directory = os.getcwd()
61
62class UpdateEmitter(object):
63 """Abstract object for implementing different types of emitters."""
64 def __init__(self):
65 pass
66 def updatesAvailable(self, updateInfo):
67 """Emitted when there are updates available to be installed.
68 If not doing the download here, then called immediately on finding
69 new updates. If we do the download here, then called after the
70 updates have been downloaded."""
71 pass
72 def updatesDownloading(self, updateInfo):
73 """Emitted to give feedback of update download starting."""
74 pass
75 def updatesApplied(self, updateInfo):
76 """Emitted on successful installation of updates."""
77 pass
78 def updatesFailed(self, errmsgs):
79 """Emitted when an update has failed to install."""
80 pass
81 def checkFailed(self, error):
82 """Emitted when checking for updates failed."""
83 pass
84
85 def setupFailed(self, error, translation_domain):
86 """Emitted when plugin initialization failed."""
87 pass
88
89
90class SyslogUpdateEmitter(UpdateEmitter):
91 def __init__(self, syslog_facility, ident = "yum-updatesd",
92 level = "WARN"):
93 UpdateEmitter.__init__(self)
94 syslog.openlog(ident, 0, self._facilityMap(syslog_facility))
95 self.level = level
96
97 def updatesAvailable(self, updateInfo):
98 num = len(updateInfo)
99 level = self.level
100 if num > 1:
101 msg = "%d updates available" %(num,)
102 elif num == 1:
103 msg = "1 update available"
104 else:
105 msg = "No updates available"
106 level = syslog.LOG_DEBUG
107
108 syslog.syslog(self._levelMap(level), msg)
109
110 def _levelMap(self, lvl):
111 level_map = { "EMERG": syslog.LOG_EMERG,
112 "ALERT": syslog.LOG_ALERT,
113 "CRIT": syslog.LOG_CRIT,
114 "ERR": syslog.LOG_ERR,
115 "WARN": syslog.LOG_WARNING,
116 "NOTICE": syslog.LOG_NOTICE,
117 "INFO": syslog.LOG_INFO,
118 "DEBUG": syslog.LOG_DEBUG }
119 if type(lvl) == int:
120 return lvl
121 if level_map.has_key(lvl.upper()):
122 return level_map[lvl.upper()]
123 return syslog.LOG_INFO
124
125 def _facilityMap(self, facility):
126 facility_map = { "KERN": syslog.LOG_KERN,
127 "USER": syslog.LOG_USER,
128 "MAIL": syslog.LOG_MAIL,
129 "DAEMON": syslog.LOG_DAEMON,
130 "AUTH": syslog.LOG_AUTH,
131 "LPR": syslog.LOG_LPR,
132 "NEWS": syslog.LOG_NEWS,
133 "UUCP": syslog.LOG_UUCP,
134 "CRON": syslog.LOG_CRON,
135 "LOCAL0": syslog.LOG_LOCAL0,
136 "LOCAL1": syslog.LOG_LOCAL1,
137 "LOCAL2": syslog.LOG_LOCAL2,
138 "LOCAL3": syslog.LOG_LOCAL3,
139 "LOCAL4": syslog.LOG_LOCAL4,
140 "LOCAL5": syslog.LOG_LOCAL5,
141 "LOCAL6": syslog.LOG_LOCAL6,
142 "LOCAL7": syslog.LOG_LOCAL7,}
143 if type(facility) == int:
144 return facility
145 elif facility_map.has_key(facility.upper()):
146 return facility_map[facility.upper()]
147 return syslog.LOG_DAEMON
148
149
150class EmailUpdateEmitter(UpdateEmitter):
151 def __init__(self, sender, rcpt):
152 UpdateEmitter.__init__(self)
153 self.sender = sender
154 self.rcpt = rcpt
155
156 def updatesAvailable(self, updateInfo):
157 num = len(updateInfo)
158 if num < 1:
159 return
160
161 output = """
162 Hi,
163 There are %d package updates available. Please run the system
164 updater.
165
166 Thank You,
167 Your Computer
168 """ % num
169
170 msg = MIMEText(output)
171 msg['Subject'] = "%d Updates Available" %(num,)
172 msg['From'] = self.sender
173 msg['To'] = ",".join(self.rcpt)
174 s = smtplib.SMTP()
175 s.connect()
176 s.sendmail(self.sender, self.rcpt, msg.as_string())
177 s.close()
178
179class DbusUpdateEmitter(UpdateEmitter):
180 def __init__(self):
181 UpdateEmitter.__init__(self)
182 bus = dbus.SystemBus()
183 name = dbus.service.BusName('edu.duke.linux.yum', bus = bus)
184 yum_dbus = YumDbusInterface(name)
185 self.dbusintf = yum_dbus
186
187 def updatesAvailable(self, updateInfo):
188 num = len(updateInfo)
189 msg = "%d" %(num,)
190 if num > 0:
191 self.dbusintf.UpdatesAvailableSignal(msg)
192 else:
193 self.dbusintf.NoUpdatesAvailableSignal(msg)
194
195 def updatesFailed(self, errmsgs):
196 self.dbusintf.UpdatesFailedSignal(errmsgs)
197
198 def updatesApplied(self, updinfo):
199 self.dbusintf.UpdatesAppliedSignal(updinfo)
200
201 def checkFailed(self, error):
202 self.dbusintf.CheckFailedSignal(error)
203
204 def setupFailed(self, error, translation_domain):
205 self.dbusintf.SetupFailedSignal(error, translation_domain)
206
207
208class YumDbusInterface(dbus.service.Object):
209 def __init__(self, bus_name, object_path='/UpdatesAvail'):
210 dbus.service.Object.__init__(self, bus_name, object_path)
211
212 @dbus.service.signal('edu.duke.linux.yum')
213 def UpdatesAvailableSignal(self, message):
214 pass
215
216 @dbus.service.signal('edu.duke.linux.yum')
217 def NoUpdatesAvailableSignal(self, message):
218 pass
219
220 @dbus.service.signal('edu.duke.linux.yum')
221 def UpdatesFailedSignal(self, errmsgs):
222 pass
223
224 @dbus.service.signal('edu.duke.linux.yum')
225 def UpdatesAppliedSignal(self, updinfo):
226 pass
227
228 @dbus.service.signal('edu.duke.linux.yum')
229 def CheckFailedSignal(self, message):
230 pass
231
232 @dbus.service.signal('edu.duke.linux.yum')
233 def SetupFailedSignal(self, message, translation_domain=""):
234 pass
235
236
237class UDConfig(BaseConfig):
238 """Config format for the daemon"""
239 run_interval = IntOption(3600)
240 nonroot_workdir = Option("/var/tmp/yum-updatesd")
241 emit_via = ListOption(['dbus', 'email', 'syslog'])
242 email_to = ListOption(["root"])
243 email_from = Option("root")
244 dbus_listener = BoolOption(True)
245 do_update = BoolOption(False)
246 do_download = BoolOption(False)
247 do_download_deps = BoolOption(False)
248 updaterefresh = IntOption(3600)
249 syslog_facility = Option("DAEMON")
250 syslog_level = Option("WARN")
251 syslog_ident = Option("yum-updatesd")
252 yum_config = Option("/etc/yum/yum.conf")
253
254
255class UpdateBuildTransactionThread(threading.Thread):
256 def __init__(self, updd, name):
257 self.updd = updd
258 threading.Thread.__init__(self, name=name)
259
260 def run(self):
261 self.updd.tsInfo.makelists()
262 try:
263 (result, msgs) = self.updd.buildTransaction()
264 except yum.Errors.RepoError, errmsg: # error downloading hdrs
265 msgs = ["Error downloading headers"]
266 self.updd.emitUpdateFailed(msgs)
267 return
268
269 dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
270 txmbr.ts_state in ("i", "u"),
271 self.updd.tsInfo.getMembers()))
272 self.updd.downloadPkgs(dlpkgs)
273 self.processPkgs(dlpkgs)
274
275
276class UpdateDownloadThread(UpdateBuildTransactionThread):
277 def __init__(self, updd):
278 UpdateBuildTransactionThread.__init__(self, updd,
279 name="UpdateDownloadThread")
280
281 def processPkgs(self, dlpkgs):
282 self.updd.emitAvailable()
283 self.updd.releaseLocks()
284
285
286class UpdateInstallThread(UpdateBuildTransactionThread):
287 def __init__(self, updd):
288 UpdateBuildTransactionThread.__init__(self, updd,
289 name="UpdateInstallThread")
290
291 def failed(self, msgs):
292 self.updd.emitUpdateFailed(msgs)
293 self.updd.releaseLocks()
294
295 def success(self):
296 self.updd.emitUpdateApplied()
297 self.updd.releaseLocks()
298
299 self.updd.updateInfo = None
300 self.updd.updateInfoTime = None
301
302 def processPkgs(self, dlpkgs):
303 for po in dlpkgs:
304 result, err = self.updd.sigCheckPkg(po)
305 if result == 0:
306 continue
307 elif result == 1:
308 try:
309 self.updd.getKeyForPackage(po)
310 except yum.Errors.YumBaseError, errmsg:
311 self.failed([str(errmsg)])
312
313 del self.updd.ts
314 self.updd.initActionTs() # make a new, blank ts to populate
315 self.updd.populateTs(keepold=0)
316 self.updd.ts.check() #required for ordering
317 self.updd.ts.order() # order
318 cb = callback.RPMInstallCallback(output = 0)
319 cb.filelog = True
320
321 cb.tsInfo = self.updd.tsInfo
322 try:
323 self.updd.runTransaction(cb=cb)
324 except yum.Errors.YumBaseError, err:
325 self.failed([str(err)])
326
327 self.success()
328
329class UpdatesDaemon(yum.YumBase):
330 def __init__(self, opts):
331 yum.YumBase.__init__(self)
332 self.opts = opts
333 self.didSetup = False
334
335 self.emitters = []
336 if 'dbus' in self.opts.emit_via:
337 self.emitters.append(DbusUpdateEmitter())
338 if 'email' in self.opts.emit_via:
339 self.emitters.append(EmailUpdateEmitter(self.opts.email_from,
340 self.opts.email_to))
341 if 'syslog' in self.opts.emit_via:
342 self.emitters.append(SyslogUpdateEmitter(self.opts.syslog_facility,
343 self.opts.syslog_ident,
344 self.opts.syslog_level))
345
346 self.updateInfo = []
347 self.updateInfoTime = None
348
349 def doSetup(self):
350 # if we are not root do the special subdir thing
351 if os.geteuid() != 0:
352 if not os.path.exists(self.opts.nonroot_workdir):
353 os.makedirs(self.opts.nonroot_workdir)
354 self.repos.setCacheDir(self.opts.nonroot_workdir)
355
356 self.doConfigSetup(fn=self.opts.yum_config)
357
358 def refreshUpdates(self):
359 self.doLock()
360 try:
361 self.doRepoSetup()
362 self.doSackSetup()
363 self.updateCheckSetup()
364 except Exception, e:
365 syslog.syslog(syslog.LOG_WARNING,
366 "error getting update info: %s" %(e,))
367 self.emitCheckFailed("%s" %(e,))
368 self.doUnlock()
369 return False
370 return True
371
372 def populateUpdateMetadata(self):
373 self.updateMetadata = UpdateMetadata()
374 repos = []
375
376 for (new, old) in self.up.getUpdatesTuples():
377 pkg = self.getPackageObject(new)
378 if pkg.repoid not in repos:
379 repo = self.repos.getRepo(pkg.repoid)
380 repos.append(repo.id)
381 try: # grab the updateinfo.xml.gz from the repodata
382 md = repo.retrieveMD('updateinfo')
383 except Exception: # can't find any; silently move on
384 continue
385 md = gzip.open(md)
386 self.updateMetadata.add(md)
387 md.close()
388
389 def populateUpdates(self):
390 def getDbusPackageDict(pkg):
391 """Returns a dictionary corresponding to the package object
392 in the form that we can send over the wire for dbus."""
393 pkgDict = {
394 "name": pkg.name,
395 "version": pkg.version,
396 "release": pkg.release,
397 "epoch": pkg.epoch,
398 "arch": pkg.arch,
399 "sourcerpm": pkg.sourcerpm,
400 "summary": pkg.summary or "",
401 }
402
403 # check if any updateinfo is available
404 md = self.updateMetadata.get_notice((pkg.name, pkg.ver, pkg.rel))
405 if md:
406 # right now we only want to know if it is a security update
407 pkgDict['type'] = md['type']
408
409 return pkgDict
410
411 if self.up is None:
412 # we're _only_ called after updates are setup
413 return
414
415 self.populateUpdateMetadata()
416
417 self.updateInfo = []
418 for (new, old) in self.up.getUpdatesTuples():
419 n = getDbusPackageDict(self.getPackageObject(new))
420 o = getDbusPackageDict(self.rpmdb.searchPkgTuple(old)[0])
421 self.updateInfo.append((n, o))
422
423 if self.conf.obsoletes:
424 for (obs, inst) in self.up.getObsoletesTuples():
425 n = getDbusPackageDict(self.getPackageObject(obs))
426 o = getDbusPackageDict(self.rpmdb.searchPkgTuple(inst)[0])
427 self.updateInfo.append((n, o))
428
429 self.updateInfoTime = time.time()
430
431 def populateTsInfo(self):
432 # figure out the updates
433 for (new, old) in self.up.getUpdatesTuples():
434 updating = self.getPackageObject(new)
435 updated = self.rpmdb.searchPkgTuple(old)[0]
436
437 self.tsInfo.addUpdate(updating, updated)
438
439 # and the obsoletes
440 if self.conf.obsoletes:
441 for (obs, inst) in self.up.getObsoletesTuples():
442 obsoleting = self.getPackageObject(obs)
443 installed = self.rpmdb.searchPkgTuple(inst)[0]
444
445 self.tsInfo.addObsoleting(obsoleting, installed)
446 self.tsInfo.addObsoleted(installed, obsoleting)
447
448 def updatesCheck(self):
449 if not self.didSetup:
450 try:
451 self.doSetup()
452 except Exception, e:
453 syslog.syslog(syslog.LOG_WARNING,
454 "error initializing: %s" % e)
455
456 if isinstance(e, yum.plugins.PluginYumExit):
457 self.emitSetupFailed(e.value, e.translation_domain)
458 else:
459 # if we don't know where the string is from, then assume
460 # it's not marked for translation (versus sending
461 # gettext.textdomain() and assuming it's from the default
462 # domain for this app)
463 self.emitSetupFailed(str(e))
464 # Setup failed, let's restart and try again after the update
465 # interval
466 restart()
467 else:
468 self.didSetup = True
469
470 try:
471 if not self.refreshUpdates():
472 return
473 except yum.Errors.LockError:
474 return True # just pass for now
475
476 try:
477 self.populateTsInfo()
478 self.populateUpdates()
479
480 if self.opts.do_update:
481 uit = UpdateInstallThread(self)
482 uit.start()
483 elif self.opts.do_download:
484 self.emitDownloading()
485 dl = UpdateDownloadThread(self)
486 dl.start()
487 else:
488 # just notify about things being available
489 self.emitAvailable()
490 self.releaseLocks()
491 except Exception, e:
492 self.emitCheckFailed("%s" %(e,))
493 self.doUnlock()
494
495 return True
496
497 def getUpdateInfo(self):
498 # if we have a cached copy, use it
499 if self.updateInfoTime and (time.time() - self.updateInfoTime <
500 self.opts.updaterefresh):
501 return self.updateInfo
502
503 # try to get the lock so we can update the info. fall back to
504 # cached if available or try a few times.
505 for i in range(10):
506 try:
507 self.doLock()
508 break
509 except yum.Errors.LockError:
510 # if we can't get the lock, return what we have if we can
511 if self.updateInfo:
512 return self.updateInfo
513 time.sleep(1)
514 else:
515 return []
516
517 try:
518 self.updateCheckSetup()
519
520 self.populateUpdates()
521
522 self.releaseLocks()
523 except:
524 self.doUnlock()
525
526 return self.updateInfo
527
528 def updateCheckSetup(self):
529 self.doTsSetup()
530 self.doRpmDBSetup()
531 self.doUpdateSetup()
532
533 def releaseLocks(self):
534 self.closeRpmDB()
535 self.doUnlock()
536
537 def emitAvailable(self):
538 """method to emit a notice about updates"""
539 map(lambda x: x.updatesAvailable(self.updateInfo), self.emitters)
540
541 def emitDownloading(self):
542 """method to emit a notice about updates downloading"""
543 map(lambda x: x.updatesDownloading(self.updateInfo), self.emitters)
544
545 def emitUpdateApplied(self):
546 """method to emit a notice when automatic updates applied"""
547 map(lambda x: x.updatesApplied(self.updateInfo), self.emitters)
548
549 def emitUpdateFailed(self, errmsgs):
550 """method to emit a notice when automatic updates failed"""
551 map(lambda x: x.updatesFailed(errmsgs), self.emitters)
552
553 def emitCheckFailed(self, error):
554 """method to emit a notice when checking for updates failed"""
555 map(lambda x: x.checkFailed(error), self.emitters)
556
557 def emitSetupFailed(self, error, translation_domain=""):
558 """method to emit a notice when checking for updates failed"""
559 map(lambda x: x.setupFailed(error, translation_domain), self.emitters)
560
561
562class YumDbusListener(dbus.service.Object):
563 def __init__(self, updd, bus_name, object_path='/Updatesd',
564 allowshutdown = False):
565 dbus.service.Object.__init__(self, bus_name, object_path)
566 self.updd = updd
567 self.allowshutdown = allowshutdown
568
569 def doCheck(self):
570 self.updd.updatesCheck()
571 return False
572
573 @dbus.service.method("edu.duke.linux.yum", in_signature="")
574 def CheckNow(self):
575 # make updating checking asynchronous since we discover whether
576 # or not there are updates via a callback signal anyway
577 gobject.idle_add(self.doCheck)
578 return "check queued"
579
580 @dbus.service.method("edu.duke.linux.yum", in_signature="")
581 def ShutDown(self):
582 if not self.allowshutdown:
583 return False
584
585 # we have to do this in a callback so that it doesn't get
586 # sent back to the caller
587 gobject.idle_add(shutDown)
588 return True
589
590 @dbus.service.method("edu.duke.linux.yum", in_signature="", out_signature="a(a{ss}a{ss})")
591 def GetUpdateInfo(self):
592 # FIXME: should this be async?
593 upds = self.updd.getUpdateInfo()
594 return upds
595
596
597def shutDown():
598 sys.exit(0)
599
600def restart():
601 os.chdir(initial_directory)
602 os.execve(sys.argv[0], sys.argv, os.environ)
603
604def main(options = None):
605 # we'll be threading for downloads/updates
606 gobject.threads_init()
607 dbus.glib.threads_init()
608
609 if options is None:
610 parser = OptionParser()
611 parser.add_option("-f", "--no-fork", action="store_true", default=False, dest="nofork")
612 parser.add_option("-r", "--remote-shutdown", action="store_true", default=False, dest="remoteshutdown")
613 (options, args) = parser.parse_args()
614
615 if not options.nofork:
616 if os.fork():
617 sys.exit()
618 fd = os.open("/dev/null", os.O_RDWR)
619 os.dup2(fd, 0)
620 os.dup2(fd, 1)
621 os.dup2(fd, 2)
622 os.close(fd)
623
624 confparser = ConfigParser()
625 opts = UDConfig()
626
627 if os.path.exists(config_file):
628 confpp_obj = ConfigPreProcessor(config_file)
629 try:
630 confparser.readfp(confpp_obj)
631 except ParsingError, e:
632 print >> sys.stderr, "Error reading config file: %s" % e
633 sys.exit(1)
634
635 syslog.openlog("yum-updatesd", 0, syslog.LOG_DAEMON)
636
637 opts.populate(confparser, 'main')
638 updd = UpdatesDaemon(opts)
639
640 if opts.dbus_listener:
641 bus = dbus.SystemBus()
642 name = dbus.service.BusName("edu.duke.linux.yum", bus=bus)
643 YumDbusListener(updd, name, allowshutdown = options.remoteshutdown)
644
645 run_interval_ms = opts.run_interval * 1000 # needs to be in ms
646 gobject.timeout_add(run_interval_ms, updd.updatesCheck)
647
648 mainloop = gobject.MainLoop()
649 mainloop.run()
650
651
652if __name__ == "__main__":
653 main()
Note: See TracBrowser for help on using the repository browser.