~ubuntu-core-dev/unattended-upgrades/ubuntu

« back to all changes in this revision

Viewing changes to unattended-upgrade

  • Committer: Michael Vogt
  • Date: 2010-08-02 09:04:20 UTC
  • mfrom: (136 ubuntu)
  • mto: This revision was merged to the branch mainline in revision 139.
  • Revision ID: michael.vogt@ubuntu.com-20100802090420-b1vaha9f359q9vto
mergedĀ fromĀ ubuntu

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import datetime
31
31
import ConfigParser
32
32
 
 
33
from StringIO import StringIO
33
34
from optparse import OptionParser
34
35
from subprocess import Popen, PIPE
35
36
 
37
38
warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning)
38
39
import apt
39
40
import logging
 
41
import lsb_release
40
42
import subprocess
41
43
 
42
44
import gettext
43
45
from gettext import gettext as _
44
46
 
 
47
# the reboot required flag file used by packages
 
48
REBOOT_REQUIRED_FILE = "/var/run/reboot-required"
 
49
MAIL_BINARY = "/usr/bin/mail"
 
50
DISTRO_CODENAME = lsb_release.get_distro_information()['CODENAME']
 
51
DISTRO_ID = lsb_release.get_distro_information()['ID']
 
52
 
45
53
class MyCache(apt.Cache):
46
54
    def __init__(self):
47
 
        apt.Cache.__init__(self, apt.progress.base.OpProgress())
 
55
        apt.Cache.__init__(self)
48
56
    def clear(self):
49
57
        self._depcache.init()
50
58
        assert (self._depcache.inst_count == 0 and
51
59
                self._depcache.broken_count == 0 and
52
60
                self._depcache.del_count == 0)
53
 
        
 
61
    
 
62
def substitute(line):
 
63
    """ substitude known mappings and return a new string 
 
64
 
 
65
    Currently supported "${distro-release}
 
66
    """
 
67
    mapping = {"distro_codename" : get_distro_codename(),
 
68
               "distro_id" : get_distro_id(),
 
69
              }
 
70
    return string.Template(line).substitute(mapping)
 
71
    
 
72
def get_distro_codename():
 
73
    return DISTRO_CODENAME
 
74
 
 
75
def get_distro_id():
 
76
    return DISTRO_ID
 
77
 
 
78
def get_allowed_origins():
 
79
    """ return a list of allowed origins from apt.conf
 
80
 
 
81
    This will take substitutions (like distro_id) into account.
 
82
    """
 
83
    allowed_origins = []
 
84
    for s in apt_pkg.config.value_list("Unattended-Upgrade::Allowed-Origins"):
 
85
        (distro_id, distro_codename) = s.split()
 
86
        allowed_origins.append((substitute(distro_id), 
 
87
                                substitute(distro_codename)))
 
88
    return allowed_origins
54
89
 
55
90
def is_allowed_origin(pkg, allowed_origins):
56
91
    if not pkg.candidate:
66
101
        return False
67
102
    for pkg in cache:
68
103
        if pkg.marked_delete:
 
104
            logging.debug("pkg '%s' now marked delete" % pkg.name)
69
105
            return False
70
106
        if pkg.marked_install or pkg.marked_upgrade:
71
107
            if not is_allowed_origin(pkg, allowed_origins):
 
108
                logging.debug("pkg '%s' not in allowed origin" % pkg.name)
72
109
                return False
73
110
            if pkg.name in blacklist:
 
111
                logging.debug("pkg '%s' blacklisted" % pkg.name)
 
112
                return False
 
113
            if pkg._pkg.selected_state == apt_pkg.SELSTATE_HOLD:
 
114
                logging.debug("pkg '%s' is on hold" % pkg.name)
74
115
                return False
75
116
    return True
76
117
 
80
121
        control = apt_inst.DebFile(debfile).control.extractdata("control")
81
122
        sections = apt_pkg.TagSection(control)
82
123
        return sections["Package"]
83
 
    except SystemError, e:
 
124
    except (IOError, SystemError), e:
84
125
        logging.error("failed to read deb file '%s' (%s)" % (debfile, e))
85
126
        # dumb fallback
86
127
        return debfile.split("_")[0]
87
128
 
88
 
def conffile_prompt(destFile):
 
129
# prefix is *only* needed for the build-in tests
 
130
def conffile_prompt(destFile, prefix=""):
89
131
    logging.debug("check_conffile_prompt('%s')" % destFile)
90
132
    pkgname = pkgname_from_deb(destFile)
91
133
    status_file = apt_pkg.config.find("Dir::State::status")
100
142
                for line in string.split(conffiles,"\n"):
101
143
                    logging.debug("conffile line: %s", line)
102
144
                    l = string.split(string.strip(line))
103
 
                    file = l[0]
 
145
                    conf_file = l[0]
104
146
                    md5 = l[1]
105
147
                    if len(l) > 2:
106
148
                        obs = l[2]
107
149
                    else:
108
150
                        obs = None
109
 
                    if os.path.exists(file) and obs != "obsolete":
110
 
                        current_md5 = apt_pkg.md5sum(open(file).read())
111
 
                        if current_md5 != md5:
112
 
                            return True
 
151
                    # ignore if conffile is obsolete or does not exist
 
152
                    if obs == "obsolete" or not os.path.exists(prefix+conf_file):
 
153
                        continue
 
154
                    # get conffile value from pkg
 
155
                    pkg_conffiles = apt_inst.debExtractControl(
 
156
                        open(destFile), "conffiles").split("\n")
 
157
                    if not conf_file in pkg_conffiles:
 
158
                        logging.debug("'%s' not in package conffiles '%s'" % (conf_file, pkg_conffiles))
 
159
                        continue
 
160
                    # test against the installed file
 
161
                    current_md5 = apt_pkg.md5sum(open(prefix+conf_file).read())
 
162
                    logging.debug("current md5: %s" % current_md5)
 
163
                    # hashes are the same, no conffile prompt
 
164
                    if current_md5 == md5:
 
165
                        return False
 
166
                    # calculate md5sum from the deb (may take a bit)
 
167
                    dpkg_cmd = ["dpkg-deb","--fsys-tarfile",destFile]
 
168
                    tar_cmd = ["tar","-x","-O", "-f","-", "."+conf_file]
 
169
                    md5_cmd = ["md5sum"]
 
170
                    dpkg_p = Popen(dpkg_cmd, stdout=PIPE)
 
171
                    tar_p = Popen(tar_cmd, stdin=dpkg_p.stdout, stdout=PIPE)
 
172
                    md5_p = Popen(md5_cmd, stdin=tar_p.stdout, stdout=PIPE)
 
173
                    pkg_md5sum = md5_p.communicate()[0].split()[0]
 
174
                    logging.debug("pkg_md5sum: %s" % pkg_md5sum)
 
175
                    # the md5sum in the deb is unchanged, this will not 
 
176
                    # trigger a conffile prompt
 
177
                    if pkg_md5sum == md5:
 
178
                        continue
 
179
                    # if we made it to this point:
 
180
                    #  current_md5 != pkg_md5sum != md5
 
181
                    # and that will trigger a conffile prompt, we can
 
182
                    # stop processing at this point and just return True
 
183
                    return True
113
184
    return False
114
185
 
115
186
 
116
187
def dpkg_conffile_prompt():
117
 
    if "DPkg::Options" not in apt_pkg.config:
 
188
    if not "DPkg::Options" in apt_pkg.config:
118
189
        return True
119
190
    options = apt_pkg.config.value_list("DPkg::Options")
120
191
    for option in map(string.strip, options):
157
228
                return
158
229
    # setup env (to play it safe) and return
159
230
    os.putenv("APT_LISTCHANGES_FRONTEND","none");
 
231
 
 
232
def send_summary_mail(pkgs, res, pkgs_kept_back, mem_log, logfile_dpkg):
 
233
    " send mail (if configured in Unattended-Upgrades::Mail) "
 
234
    email = apt_pkg.config.find("Unattended-Upgrade::Mail", "")
 
235
    if not email:
 
236
        return
 
237
    if not os.path.exists(MAIL_BINARY):
 
238
        logging.error(_("No '/usr/bin/mail', can not send mail. "
 
239
                        "You probably want to install the 'mailx' package."))
 
240
        return
 
241
    # Check if reboot-required flag is present
 
242
    logging.debug("Sending mail with '%s' to '%s'" % (logfile_dpkg, email))
 
243
    if os.path.isfile(REBOOT_REQUIRED_FILE):
 
244
        subject = _("[reboot required] unattended-upgrades result for '%s'") % host()
 
245
    else:
 
246
        subject = _("unattended-upgrades result for '%s'") % host()
 
247
    mail = subprocess.Popen([MAIL_BINARY, "-s", subject,
 
248
                             email], stdin=subprocess.PIPE)
 
249
    s = _("Unattended upgrade returned: %s\n\n") % res
 
250
    if os.path.isfile(REBOOT_REQUIRED_FILE):
 
251
        s += _("Warning: A reboot is required to complete this upgrade.\n\n")
 
252
    s += _("Packages that are upgraded:\n")
 
253
    s += " " + wrap(pkgs, 70, " ")
 
254
    s += "\n"
 
255
    if pkgs_kept_back:
 
256
        s += _("Packages with upgradable origin but kept back:\n")
 
257
        s += " " + wrap(" ".join(pkgs_kept_back), 70, " ")
 
258
        s += "\n"
 
259
    s += "\n"
 
260
    s += _("Package installation log:")+"\n"
 
261
    s += open(logfile_dpkg).read()
 
262
    s += "\n\n"
 
263
    s += _("Unattended-upgrades log:\n")
 
264
    s += mem_log.getvalue()
 
265
    mail.stdin.write(s)
 
266
    mail.stdin.close()
 
267
    ret = mail.wait()
 
268
    logging.debug("mail returned: %s" % ret)
160
269
    
161
270
 
162
271
def main():
165
274
    parser.add_option("-d", "--debug",
166
275
                      action="store_true", dest="debug", default=False,
167
276
                      help=_("print debug messages"))
 
277
    parser.add_option("", "--dry-run",
 
278
                      action="store_true", default=False,
 
279
                      help=_("Simulation, download but do not install"))
168
280
    (options, args) = parser.parse_args()
 
281
 
 
282
    # setup logging
 
283
    logger = logging.getLogger()
 
284
    mem_log = StringIO()
169
285
    if options.debug:
170
 
        logging.getLogger().setLevel(logging.DEBUG)
171
 
        pass
172
 
 
173
 
    #dldir = "/tmp/pyapt-test"
174
 
    #try:
175
 
    #    os.mkdir(dldir)
176
 
    #    os.mkdir(dldir+"/partial")
177
 
    #except OSError:
178
 
    #    pass
179
 
    #apt_pkg.config.set("Dir::Cache::archives",dldir)
 
286
        logger.setLevel(logging.DEBUG)
 
287
        stderr_handler = logging.StreamHandler()
 
288
        logger.addHandler(stderr_handler)
 
289
    if apt_pkg.config.find("Unattended-Upgrade::Mail", ""):
 
290
        mem_log_handler = logging.StreamHandler(mem_log)
 
291
        logger.addHandler(mem_log_handler)
180
292
 
181
293
    # format (origin, archive), e.g. ("Ubuntu","dapper-security")
182
 
    allowed_origins = map(string.split, apt_pkg.config.value_list("Unattended-Upgrade::Allowed-Origins"))
 
294
    allowed_origins = get_allowed_origins()
183
295
 
184
296
    # pkgs that are (for some reason) not save to install
185
297
    blacklisted_pkgs = apt_pkg.config.value_list("Unattended-Upgrade::Package-Blacklist")
189
301
    # display available origin
190
302
    logging.info(_("Allowed origins are: %s") % map(str,allowed_origins))
191
303
    
 
304
    # check and get lock
 
305
    try:
 
306
        apt_pkg.pkgsystem_lock()
 
307
    except SystemError, e:
 
308
        logging.error(_("Lock could not be acquired (another package "
 
309
                        "manager running?)"))
 
310
        print _("Cache lock can not be acquired, exiting")
 
311
        sys.exit(1)
 
312
 
192
313
    # get a cache
193
314
    cache = MyCache()
194
315
    if cache._depcache.broken_count > 0:
201
322
    # find out about the packages that are upgradable (in a allowed_origin)
202
323
    pkgs_to_upgrade = []
203
324
    pkgs_kept_back = []
 
325
    pkgs_auto_removable = set([pkg.name for pkg in cache 
 
326
                               if pkg.is_auto_removable])
204
327
    for pkg in cache:
205
328
        if options.debug and pkg.is_upgradable:
206
329
            logging.debug("Checking: %s (%s)" % (pkg.name, map(str, pkg.candidate.origins)))
217
340
                    pkgs_kept_back.append(pkg.name)
218
341
            except SystemError, e:
219
342
                # can't upgrade
220
 
                logging.warning(_("package '%s' upgradable but fails to be marked for upgrade (%s)") % e)
221
 
                rewind_cache(cache, pkgs_to_ugprade)
 
343
                logging.warning(_("package '%s' upgradable but fails to be marked for upgrade (%s)") % (pkg.name, e))
 
344
                rewind_cache(cache, pkgs_to_upgrade)
222
345
                pkgs_kept_back.append(pkg.name)
223
346
                
224
347
 
252
375
                print _("The URI '%s' failed to download, aborting") % item.desc_uri
253
376
                logging.error(_("The URI '%s' failed to download, aborting") % item.desc_uri)
254
377
                sys.exit(1)
 
378
            if not os.path.exists(item.destfile):
 
379
                print _("Download finished, but file '%s' not there?!?" % item.destfile)
 
380
                logging.error("Download finished, but file '%s' not there?!?" % item.destfile)
 
381
                sys.exit(1)
255
382
            if not item.is_trusted:
256
383
                blacklisted_pkgs.append(pkgname_from_deb(item.destfile))
257
384
            if conffile_prompt(item.destfile):
287
414
    else:
288
415
        logging.debug("dpkg is configured not to cause conffile prompts")
289
416
 
 
417
    # do auto-remove
 
418
    if apt_pkg.config.find_b("Unattended-Upgrade::Remove-Unused-Dependencies", False):
 
419
        now_auto_removable = set([pkg.name for pkg in cache 
 
420
                               if pkg.is_auto_removable])
 
421
        for pkgname in now_auto_removable-pkgs_auto_removable:
 
422
            logging.debug("marking %s for remove" % pkgname)
 
423
            cache[pkgname].mark_delete()
 
424
        logging.info(_("Packages that are auto removed: '%s'") %
 
425
                     " ".join(now_auto_removable-pkgs_auto_removable))
 
426
 
290
427
    logging.debug("InstCount=%i DelCount=%i BrokenCout=%i" % (cache._depcache.inst_count, cache._depcache.del_count, cache._depcache.broken_count))
291
428
 
292
429
    # exit if there is nothing to do and nothing to report
294
431
        logging.info(_("No packages found that can be upgraded unattended"))
295
432
        sys.exit(0)    
296
433
 
 
434
    # check if we are in dry-run mode
 
435
    if options.dry_run:
 
436
        logging.info("Option --dry-run given, *not* performing real actions")
 
437
        apt_pkg.config.set("Debug::pkgDPkgPM","1")
 
438
 
297
439
    # do the install based on the new list of pkgs
298
440
    pkgs = " ".join([pkg.name for pkg in pkgs_to_upgrade])
299
441
    logging.info(_("Packages that are upgraded: %s" % pkgs))
311
453
    logfile_dpkg = logdir+'unattended-upgrades-dpkg_%s.log' % now.isoformat('_')
312
454
    logging.info(_("Writing dpkg log to '%s'") % logfile_dpkg)
313
455
    fd = os.open(logfile_dpkg, os.O_RDWR|os.O_CREAT, 0644)
 
456
    old_stdout = os.dup(1)
 
457
    old_stderr = os.dup(2)
314
458
    os.dup2(fd,1)
315
459
    os.dup2(fd,2)
316
460
    
317
 
    # enable debugging
318
 
    if options.debug:
319
 
        apt_pkg.config.set("Debug::pkgDPkgPM","1")
320
461
    # create a new package-manager. the blacklist may have changed
321
462
    # the markings in the depcache
322
463
    pm = apt_pkg.PackageManager(cache._depcache)
325
466
    # run the fetcher again (otherwise local file:// 
326
467
    # URIs are unhappy (see LP: #56832)
327
468
    res = fetcher.run()
 
469
    # unlock the cache
 
470
    try:
 
471
        apt_pkg.pkgsystem_unlock()
 
472
    except SystemError, e:
 
473
        pass
 
474
    # lock for the shutdown check - its fine if the system
 
475
    # is shutdown while downloading but not so much while installing
 
476
    apt_pkg.get_lock("/var/run/unattended-upgrades.lock")
328
477
    # now do the actual install
 
478
    error = None
329
479
    try:
330
480
        res = pm.do_install()
331
481
    except SystemError,e:
 
482
        error = e
 
483
        res = pm.RESULT_FAILED
 
484
    finally:
 
485
        os.dup2(old_stdout, 1)
 
486
        os.dup2(old_stderr, 2)
 
487
 
 
488
    if res == pm.RESULT_FAILED:
332
489
        logging.error(_("Installing the upgrades failed!"))
333
490
        logging.error(_("error message: '%s'") % e)
334
 
        res = False
335
 
                
336
 
    if res == pm.RESULT_FAILED:
337
491
        logging.error(_("dpkg returned a error! See '%s' for details") % logfile_dpkg)
338
492
    else:
339
493
        logging.info(_("All upgrades installed"))
340
494
 
341
 
    # check if we need to send a mail
342
 
    email = apt_pkg.config.find("Unattended-Upgrade::Mail", "")
343
 
    if email != "":
344
 
        if not os.path.exists("/usr/bin/mail"):
345
 
            logging.error(_("No '/usr/bin/mail', can not send mail. "
346
 
                            "You probably want to install the 'mailx' package."))
347
 
            return
348
 
        logging.debug("Sending mail with '%s' to '%s'" % (logfile_dpkg, email))
349
 
        mail = subprocess.Popen(["/usr/bin/mail",
350
 
                                 "-s", _("unattended-upgrades result "
351
 
                                         "for '%s'") % host(), 
352
 
                                 email],
353
 
                                stdin=subprocess.PIPE)
354
 
        s = _("Unattended upgrade returned: %s\n\n") % (res != pm.ResultFailed)
355
 
        s += _("Packages that are upgraded:\n")
356
 
        s += " " + wrap(pkgs, 70, " ")
357
 
        s += "\n"
358
 
        if pkgs_kept_back:
359
 
            s += _("Packages with upgradable origin but kept back:\n")
360
 
            s += " " + wrap(" ".join(pkgs_kept_back), 70, " ")
361
 
            s += "\n"
362
 
        s += "\n"
363
 
        s += _("Package installation log:")
364
 
        s += open(logfile_dpkg).read()
365
 
        mail.stdin.write(s)
366
 
        mail.stdin.close()
367
 
        ret = mail.wait()
368
 
        logging.debug("mail returned: %s" % ret)
 
495
    # send a mail (if needed)
 
496
    pkg_install_success = (res != pm.RESULT_FAILED)
 
497
    send_summary_mail(pkgs, pkg_install_success, pkgs_kept_back, mem_log, logfile_dpkg)
369
498
 
 
499
    # auto-reboot (if required and the config for this is set
 
500
    if (apt_pkg.config.find_b("Unattended-Upgrade::Automatic-Reboot", False) and
 
501
        os.path.exists(REBOOT_REQUIRED_FILE)):
 
502
        logging.warning("Found %s, rebooting" % REBOOT_REQUIRED_FILE)
 
503
        subprocess.call(["/sbin/reboot"])
 
504
        
370
505
 
371
506
if __name__ == "__main__":
372
507
    localesApp="unattended-upgrades"