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

Last change on this file since 1569 was 516, checked in by Yuri Dario, 11 years ago

yum: update trunk to 3.4.3.

  • Property svn:eol-style set to native
File size: 21.7 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('/@unixroot/usr/share/yum-cli')
57import callback
58
59config_file = '/@unixroot/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 return level_map.get(lvl.upper(), syslog.LOG_INFO)
122
123 def _facilityMap(self, facility):
124 facility_map = { "KERN": syslog.LOG_KERN,
125 "USER": syslog.LOG_USER,
126 "MAIL": syslog.LOG_MAIL,
127 "DAEMON": syslog.LOG_DAEMON,
128 "AUTH": syslog.LOG_AUTH,
129 "LPR": syslog.LOG_LPR,
130 "NEWS": syslog.LOG_NEWS,
131 "UUCP": syslog.LOG_UUCP,
132 "CRON": syslog.LOG_CRON,
133 "LOCAL0": syslog.LOG_LOCAL0,
134 "LOCAL1": syslog.LOG_LOCAL1,
135 "LOCAL2": syslog.LOG_LOCAL2,
136 "LOCAL3": syslog.LOG_LOCAL3,
137 "LOCAL4": syslog.LOG_LOCAL4,
138 "LOCAL5": syslog.LOG_LOCAL5,
139 "LOCAL6": syslog.LOG_LOCAL6,
140 "LOCAL7": syslog.LOG_LOCAL7,}
141 if type(facility) == int:
142 return facility
143 return facility_map.get(facility.upper(), syslog.LOG_DAEMON)
144
145
146class EmailUpdateEmitter(UpdateEmitter):
147 def __init__(self, sender, rcpt):
148 UpdateEmitter.__init__(self)
149 self.sender = sender
150 self.rcpt = rcpt
151
152 def updatesAvailable(self, updateInfo):
153 num = len(updateInfo)
154 if num < 1:
155 return
156
157 output = """
158 Hi,
159 There are %d package updates available. Please run the system
160 updater.
161
162 Thank You,
163 Your Computer
164 """ % num
165
166 msg = MIMEText(output)
167 msg['Subject'] = "%d Updates Available" %(num,)
168 msg['From'] = self.sender
169 msg['To'] = ",".join(self.rcpt)
170 s = smtplib.SMTP()
171 s.connect()
172 s.sendmail(self.sender, self.rcpt, msg.as_string())
173 s.close()
174
175class DbusUpdateEmitter(UpdateEmitter):
176 def __init__(self):
177 UpdateEmitter.__init__(self)
178 bus = dbus.SystemBus()
179 name = dbus.service.BusName('edu.duke.linux.yum', bus = bus)
180 yum_dbus = YumDbusInterface(name)
181 self.dbusintf = yum_dbus
182
183 def updatesAvailable(self, updateInfo):
184 num = len(updateInfo)
185 msg = "%d" %(num,)
186 if num > 0:
187 self.dbusintf.UpdatesAvailableSignal(msg)
188 else:
189 self.dbusintf.NoUpdatesAvailableSignal(msg)
190
191 def updatesFailed(self, errmsgs):
192 self.dbusintf.UpdatesFailedSignal(errmsgs)
193
194 def updatesApplied(self, updinfo):
195 self.dbusintf.UpdatesAppliedSignal(updinfo)
196
197 def checkFailed(self, error):
198 self.dbusintf.CheckFailedSignal(error)
199
200 def setupFailed(self, error, translation_domain):
201 self.dbusintf.SetupFailedSignal(error, translation_domain)
202
203
204class YumDbusInterface(dbus.service.Object):
205 def __init__(self, bus_name, object_path='/UpdatesAvail'):
206 dbus.service.Object.__init__(self, bus_name, object_path)
207
208 @dbus.service.signal('edu.duke.linux.yum')
209 def UpdatesAvailableSignal(self, message):
210 pass
211
212 @dbus.service.signal('edu.duke.linux.yum')
213 def NoUpdatesAvailableSignal(self, message):
214 pass
215
216 @dbus.service.signal('edu.duke.linux.yum')
217 def UpdatesFailedSignal(self, errmsgs):
218 pass
219
220 @dbus.service.signal('edu.duke.linux.yum')
221 def UpdatesAppliedSignal(self, updinfo):
222 pass
223
224 @dbus.service.signal('edu.duke.linux.yum')
225 def CheckFailedSignal(self, message):
226 pass
227
228 @dbus.service.signal('edu.duke.linux.yum')
229 def SetupFailedSignal(self, message, translation_domain=""):
230 pass
231
232
233class UDConfig(BaseConfig):
234 """Config format for the daemon"""
235 run_interval = IntOption(3600)
236 nonroot_workdir = Option("/var/tmp/yum-updatesd")
237 emit_via = ListOption(['dbus', 'email', 'syslog'])
238 email_to = ListOption(["root"])
239 email_from = Option("root")
240 dbus_listener = BoolOption(True)
241 do_update = BoolOption(False)
242 do_download = BoolOption(False)
243 do_download_deps = BoolOption(False)
244 updaterefresh = IntOption(3600)
245 syslog_facility = Option("DAEMON")
246 syslog_level = Option("WARN")
247 syslog_ident = Option("yum-updatesd")
248 yum_config = Option("/etc/yum/yum.conf")
249
250
251class UpdateBuildTransactionThread(threading.Thread):
252 def __init__(self, updd, name):
253 self.updd = updd
254 threading.Thread.__init__(self, name=name)
255
256 def run(self):
257 self.updd.tsInfo.makelists()
258 try:
259 (result, msgs) = self.updd.buildTransaction()
260 except yum.Errors.RepoError, errmsg: # error downloading hdrs
261 msgs = ["Error downloading headers"]
262 self.updd.emitUpdateFailed(msgs)
263 return
264
265 dlpkgs = map(lambda x: x.po, filter(lambda txmbr:
266 txmbr.ts_state in ("i", "u"),
267 self.updd.tsInfo.getMembers()))
268 self.updd.downloadPkgs(dlpkgs)
269 self.processPkgs(dlpkgs)
270
271
272class UpdateDownloadThread(UpdateBuildTransactionThread):
273 def __init__(self, updd):
274 UpdateBuildTransactionThread.__init__(self, updd,
275 name="UpdateDownloadThread")
276
277 def processPkgs(self, dlpkgs):
278 self.updd.emitAvailable()
279 self.updd.releaseLocks()
280
281
282class UpdateInstallThread(UpdateBuildTransactionThread):
283 def __init__(self, updd):
284 UpdateBuildTransactionThread.__init__(self, updd,
285 name="UpdateInstallThread")
286
287 def failed(self, msgs):
288 self.updd.emitUpdateFailed(msgs)
289 self.updd.releaseLocks()
290
291 def success(self):
292 self.updd.emitUpdateApplied()
293 self.updd.releaseLocks()
294
295 self.updd.updateInfo = None
296 self.updd.updateInfoTime = None
297
298 def processPkgs(self, dlpkgs):
299 for po in dlpkgs:
300 result, err = self.updd.sigCheckPkg(po)
301 if result == 0:
302 continue
303 elif result == 1:
304 try:
305 self.updd.getKeyForPackage(po)
306 except yum.Errors.YumBaseError, errmsg:
307 self.failed([str(errmsg)])
308
309 del self.updd.ts
310 self.updd.initActionTs() # make a new, blank ts to populate
311 self.updd.populateTs(keepold=0)
312 self.updd.ts.check() #required for ordering
313 self.updd.ts.order() # order
314 cb = callback.RPMInstallCallback(output = 0)
315 cb.filelog = True
316
317 cb.tsInfo = self.updd.tsInfo
318 try:
319 self.updd.runTransaction(cb=cb)
320 except yum.Errors.YumBaseError, err:
321 self.failed([str(err)])
322
323 self.success()
324
325class UpdatesDaemon(yum.YumBase):
326 def __init__(self, opts):
327 yum.YumBase.__init__(self)
328 self.opts = opts
329 self.didSetup = False
330
331 self.emitters = []
332 if 'dbus' in self.opts.emit_via:
333 self.emitters.append(DbusUpdateEmitter())
334 if 'email' in self.opts.emit_via:
335 self.emitters.append(EmailUpdateEmitter(self.opts.email_from,
336 self.opts.email_to))
337 if 'syslog' in self.opts.emit_via:
338 self.emitters.append(SyslogUpdateEmitter(self.opts.syslog_facility,
339 self.opts.syslog_ident,
340 self.opts.syslog_level))
341
342 self.updateInfo = []
343 self.updateInfoTime = None
344
345 def doSetup(self):
346 # if we are not root do the special subdir thing
347 if os.geteuid() != 0:
348 if not os.path.exists(self.opts.nonroot_workdir):
349 os.makedirs(self.opts.nonroot_workdir)
350 self.repos.setCacheDir(self.opts.nonroot_workdir)
351
352 self.doConfigSetup(fn=self.opts.yum_config)
353
354 def refreshUpdates(self):
355 self.doLock()
356 try:
357 self.doRepoSetup()
358 self.doSackSetup()
359 self.updateCheckSetup()
360 except Exception, e:
361 syslog.syslog(syslog.LOG_WARNING,
362 "error getting update info: %s" %(e,))
363 self.emitCheckFailed("%s" %(e,))
364 self.doUnlock()
365 return False
366 return True
367
368 def populateUpdateMetadata(self):
369 self.updateMetadata = UpdateMetadata()
370 repos = []
371
372 for (new, old) in self.up.getUpdatesTuples():
373 pkg = self.getPackageObject(new)
374 if pkg.repoid not in repos:
375 repo = self.repos.getRepo(pkg.repoid)
376 repos.append(repo.id)
377 try: # grab the updateinfo.xml.gz from the repodata
378 md = repo.retrieveMD('updateinfo')
379 except Exception: # can't find any; silently move on
380 continue
381 md = gzip.open(md)
382 self.updateMetadata.add(md)
383 md.close()
384
385 def populateUpdates(self):
386 def getDbusPackageDict(pkg):
387 """Returns a dictionary corresponding to the package object
388 in the form that we can send over the wire for dbus."""
389 pkgDict = {
390 "name": pkg.name,
391 "version": pkg.version,
392 "release": pkg.release,
393 "epoch": pkg.epoch,
394 "arch": pkg.arch,
395 "sourcerpm": pkg.sourcerpm,
396 "summary": pkg.summary or "",
397 }
398
399 # check if any updateinfo is available
400 md = self.updateMetadata.get_notice((pkg.name, pkg.ver, pkg.rel))
401 if md:
402 # right now we only want to know if it is a security update
403 pkgDict['type'] = md['type']
404
405 return pkgDict
406
407 if self.up is None:
408 # we're _only_ called after updates are setup
409 return
410
411 self.populateUpdateMetadata()
412
413 self.updateInfo = []
414 for (new, old) in self.up.getUpdatesTuples():
415 n = getDbusPackageDict(self.getPackageObject(new))
416 o = getDbusPackageDict(self.rpmdb.searchPkgTuple(old)[0])
417 self.updateInfo.append((n, o))
418
419 if self.conf.obsoletes:
420 for (obs, inst) in self.up.getObsoletesTuples():
421 n = getDbusPackageDict(self.getPackageObject(obs))
422 o = getDbusPackageDict(self.rpmdb.searchPkgTuple(inst)[0])
423 self.updateInfo.append((n, o))
424
425 self.updateInfoTime = time.time()
426
427 def populateTsInfo(self):
428 # figure out the updates
429 for (new, old) in self.up.getUpdatesTuples():
430 updating = self.getPackageObject(new)
431 updated = self.rpmdb.searchPkgTuple(old)[0]
432
433 self.tsInfo.addUpdate(updating, updated)
434
435 # and the obsoletes
436 if self.conf.obsoletes:
437 for (obs, inst) in self.up.getObsoletesTuples():
438 obsoleting = self.getPackageObject(obs)
439 installed = self.rpmdb.searchPkgTuple(inst)[0]
440
441 self.tsInfo.addObsoleting(obsoleting, installed)
442 self.tsInfo.addObsoleted(installed, obsoleting)
443
444 def updatesCheck(self):
445 if not self.didSetup:
446 try:
447 self.doSetup()
448 except Exception, e:
449 syslog.syslog(syslog.LOG_WARNING,
450 "error initializing: %s" % e)
451
452 if isinstance(e, yum.plugins.PluginYumExit):
453 self.emitSetupFailed(e.value, e.translation_domain)
454 else:
455 # if we don't know where the string is from, then assume
456 # it's not marked for translation (versus sending
457 # gettext.textdomain() and assuming it's from the default
458 # domain for this app)
459 self.emitSetupFailed(str(e))
460 # Setup failed, let's restart and try again after the update
461 # interval
462 restart()
463 else:
464 self.didSetup = True
465
466 try:
467 if not self.refreshUpdates():
468 return
469 except yum.Errors.LockError:
470 return True # just pass for now
471
472 try:
473 self.populateTsInfo()
474 self.populateUpdates()
475
476 if self.opts.do_update:
477 uit = UpdateInstallThread(self)
478 uit.start()
479 elif self.opts.do_download:
480 self.emitDownloading()
481 dl = UpdateDownloadThread(self)
482 dl.start()
483 else:
484 # just notify about things being available
485 self.emitAvailable()
486 self.releaseLocks()
487 except Exception, e:
488 self.emitCheckFailed("%s" %(e,))
489 self.doUnlock()
490
491 return True
492
493 def getUpdateInfo(self):
494 # if we have a cached copy, use it
495 if self.updateInfoTime and (time.time() - self.updateInfoTime <
496 self.opts.updaterefresh):
497 return self.updateInfo
498
499 # try to get the lock so we can update the info. fall back to
500 # cached if available or try a few times.
501 for i in range(10):
502 try:
503 self.doLock()
504 break
505 except yum.Errors.LockError:
506 # if we can't get the lock, return what we have if we can
507 if self.updateInfo:
508 return self.updateInfo
509 time.sleep(1)
510 else:
511 return []
512
513 try:
514 self.updateCheckSetup()
515
516 self.populateUpdates()
517
518 self.releaseLocks()
519 except:
520 self.doUnlock()
521
522 return self.updateInfo
523
524 def updateCheckSetup(self):
525 self.doTsSetup()
526 self.doRpmDBSetup()
527 self.doUpdateSetup()
528
529 def releaseLocks(self):
530 self.closeRpmDB()
531 self.doUnlock()
532
533 def emitAvailable(self):
534 """method to emit a notice about updates"""
535 map(lambda x: x.updatesAvailable(self.updateInfo), self.emitters)
536
537 def emitDownloading(self):
538 """method to emit a notice about updates downloading"""
539 map(lambda x: x.updatesDownloading(self.updateInfo), self.emitters)
540
541 def emitUpdateApplied(self):
542 """method to emit a notice when automatic updates applied"""
543 map(lambda x: x.updatesApplied(self.updateInfo), self.emitters)
544
545 def emitUpdateFailed(self, errmsgs):
546 """method to emit a notice when automatic updates failed"""
547 map(lambda x: x.updatesFailed(errmsgs), self.emitters)
548
549 def emitCheckFailed(self, error):
550 """method to emit a notice when checking for updates failed"""
551 map(lambda x: x.checkFailed(error), self.emitters)
552
553 def emitSetupFailed(self, error, translation_domain=""):
554 """method to emit a notice when checking for updates failed"""
555 map(lambda x: x.setupFailed(error, translation_domain), self.emitters)
556
557
558class YumDbusListener(dbus.service.Object):
559 def __init__(self, updd, bus_name, object_path='/Updatesd',
560 allowshutdown = False):
561 dbus.service.Object.__init__(self, bus_name, object_path)
562 self.updd = updd
563 self.allowshutdown = allowshutdown
564
565 def doCheck(self):
566 self.updd.updatesCheck()
567 return False
568
569 @dbus.service.method("edu.duke.linux.yum", in_signature="")
570 def CheckNow(self):
571 # make updating checking asynchronous since we discover whether
572 # or not there are updates via a callback signal anyway
573 gobject.idle_add(self.doCheck)
574 return "check queued"
575
576 @dbus.service.method("edu.duke.linux.yum", in_signature="")
577 def ShutDown(self):
578 if not self.allowshutdown:
579 return False
580
581 # we have to do this in a callback so that it doesn't get
582 # sent back to the caller
583 gobject.idle_add(shutDown)
584 return True
585
586 @dbus.service.method("edu.duke.linux.yum", in_signature="", out_signature="a(a{ss}a{ss})")
587 def GetUpdateInfo(self):
588 # FIXME: should this be async?
589 upds = self.updd.getUpdateInfo()
590 return upds
591
592
593def shutDown():
594 sys.exit(0)
595
596def restart():
597 os.chdir(initial_directory)
598 os.execve(sys.argv[0], sys.argv, os.environ)
599
600def main(options = None):
601 # we'll be threading for downloads/updates
602 gobject.threads_init()
603 dbus.glib.threads_init()
604
605 if options is None:
606 parser = OptionParser()
607 parser.add_option("-f", "--no-fork", action="store_true", default=False, dest="nofork")
608 parser.add_option("-r", "--remote-shutdown", action="store_true", default=False, dest="remoteshutdown")
609 (options, args) = parser.parse_args()
610
611 if not options.nofork:
612 if os.fork():
613 sys.exit()
614 fd = os.open("/dev/null", os.O_RDWR)
615 os.dup2(fd, 0)
616 os.dup2(fd, 1)
617 os.dup2(fd, 2)
618 os.close(fd)
619
620 confparser = ConfigParser()
621 opts = UDConfig()
622
623 if os.path.exists(config_file):
624 confpp_obj = ConfigPreProcessor(config_file)
625 try:
626 confparser.readfp(confpp_obj)
627 except ParsingError, e:
628 print >> sys.stderr, "Error reading config file: %s" % e
629 sys.exit(1)
630
631 syslog.openlog("yum-updatesd", 0, syslog.LOG_DAEMON)
632
633 opts.populate(confparser, 'main')
634 updd = UpdatesDaemon(opts)
635
636 if opts.dbus_listener:
637 bus = dbus.SystemBus()
638 name = dbus.service.BusName("edu.duke.linux.yum", bus=bus)
639 YumDbusListener(updd, name, allowshutdown = options.remoteshutdown)
640
641 run_interval_ms = opts.run_interval * 1000 # needs to be in ms
642 gobject.timeout_add(run_interval_ms, updd.updatesCheck)
643
644 mainloop = gobject.MainLoop()
645 mainloop.run()
646
647
648if __name__ == "__main__":
649 main()
Note: See TracBrowser for help on using the repository browser.