~ubuntu-branches/ubuntu/natty/unattended-upgrades/natty

« back to all changes in this revision

Viewing changes to unattended-upgrade

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt
  • Date: 2011-03-14 11:49:02 UTC
  • mfrom: (4.1.14 sid)
  • Revision ID: james.westby@ubuntu.com-20110314114902-ptzfspz42t7s6287
Tags: 0.70ubuntu1
* merged lp:~mvo/unattended-upgrades/minimal-steps-upgrade
  - This allows performaing the upgrades in minimal chunks so
    that they can be interrupted (relatively) quickly with
    SIGUSR1
  - This feature is not enabled by default yet, in order
    to use it, uncomment the line in 50unattended-upgrades:
     Unattended-Upgrades::MinimalSteps "true";
  LP: #729214

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
import apt_inst
25
25
import apt_pkg
26
26
 
27
 
import sys
 
27
import ConfigParser
 
28
import datetime
 
29
import re
28
30
import os
29
31
import string
30
 
import datetime
31
 
import ConfigParser
 
32
import sys
32
33
 
33
34
from StringIO import StringIO
34
35
from optparse import OptionParser
39
40
import apt
40
41
import logging
41
42
import lsb_release
 
43
import signal
42
44
import subprocess
43
45
 
44
46
import gettext
47
49
# the reboot required flag file used by packages
48
50
REBOOT_REQUIRED_FILE = "/var/run/reboot-required"
49
51
MAIL_BINARY = "/usr/bin/mail"
 
52
SENDMAIL_BINARY = "/usr/sbin/sendmail"
50
53
DISTRO_CODENAME = lsb_release.get_distro_information()['CODENAME']
51
54
DISTRO_ID = lsb_release.get_distro_information()['ID']
52
55
 
53
 
class MyCache(apt.Cache):
54
 
    def __init__(self):
55
 
        apt.Cache.__init__(self)
56
 
    def clear(self):
57
 
        self._depcache.init()
58
 
        assert (self._depcache.inst_count == 0 and
59
 
                self._depcache.broken_count == 0 and
60
 
                self._depcache.del_count == 0)
61
 
    
 
56
# set from the sigint signal handler
 
57
SIGNAL_STOP_REQUEST=False
 
58
 
 
59
 
 
60
class Unlocked:
 
61
    """ context manager for unlocking the apt lock while cache.commit()
 
62
        is run 
 
63
    """
 
64
    def __enter__(self):
 
65
        try:
 
66
            apt_pkg.pkgsystem_unlock()
 
67
        except:
 
68
            pass
 
69
    def __exit__(self, exc_type, exc_value, exc_tb):
 
70
        try:
 
71
            apt_pkg.pkgsystem_unlock()
 
72
        except:
 
73
            pass
 
74
 
 
75
 
 
76
def signal_handler(signal, frame):
 
77
    logging.warn("SIGUSR1 recieved, will stop")
 
78
    global SIGNAL_STOP_REQUEST
 
79
    SIGNAL_STOP_REQUEST=True
 
80
 
62
81
def substitute(line):
63
82
    """ substitude known mappings and return a new string 
64
83
 
65
 
    Currently supported "${distro-release}
 
84
    Currently supported ${distro-release}
66
85
    """
67
86
    mapping = {"distro_codename" : get_distro_codename(),
68
87
               "distro_id" : get_distro_id(),
75
94
def get_distro_id():
76
95
    return DISTRO_ID
77
96
 
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
 
    """
 
97
def get_allowed_origins_legacy():
 
98
    """ legacy support for old Allowed-Origins var """
83
99
    allowed_origins = []
84
100
    for s in apt_pkg.config.value_list("Unattended-Upgrade::Allowed-Origins"):
85
101
        # if there is a ":" use that as seperator, else use spaces
87
103
            (distro_id, distro_codename) = s.split(':')
88
104
        else:
89
105
            (distro_id, distro_codename) = s.split()
90
 
        allowed_origins.append((substitute(distro_id), 
91
 
                                substitute(distro_codename)))
92
 
    return allowed_origins
 
106
        # convert to new format
 
107
        allowed_origins.append("o=%s,a=%s" % (substitute(distro_id), 
 
108
                                              substitute(distro_codename)))
 
109
    return allowed_origins
 
110
 
 
111
def get_allowed_origins():
 
112
    """ return a list of allowed origins from apt.conf
 
113
 
 
114
    This will take substitutions (like distro_id) into account.
 
115
    """
 
116
    allowed_origins = get_allowed_origins_legacy()
 
117
    for s in apt_pkg.config.value_list("Unattended-Upgrade::Origins-Pattern"):
 
118
        allowed_origins.append(substitute(s))
 
119
    return allowed_origins
 
120
 
 
121
def match_whitelist_string(whitelist, origin):
 
122
    """
 
123
    take a whitelist string in the form "origin=Debian,label=Debian-Security"
 
124
    and match against the given python-apt origin. A empty whitelist string
 
125
    never matches anything.
 
126
    """
 
127
    whitelist = whitelist.strip()
 
128
    if whitelist == "":
 
129
        logging.warn("empty match string matches nothing")
 
130
        return False
 
131
    res = True
 
132
    # make "\," the html quote equivalent
 
133
    whitelist = whitelist.replace("\,", "%2C")
 
134
    for token in whitelist.split(","):
 
135
        # strip and unquote the "," back
 
136
        (what, value) = [s.strip().replace("%2C",",")
 
137
                         for s in token.split("=")]
 
138
        #logging.debug("matching '%s'='%s' against '%s'" % (what, value, origin))
 
139
        # first char is apt-cache policy output, send is the name
 
140
        # in the Release file
 
141
        if what in ("o", "origin"):
 
142
            res &= (value == origin.origin)
 
143
        elif what in ("l", "label"):
 
144
            res &= (value == origin.label)
 
145
        elif what in ("a", "suite", "archive"):
 
146
            res &= (value == origin.archive)
 
147
        elif what in ("c", "component"):
 
148
            res &= (value == origin.component)
 
149
        elif what in ("site",):
 
150
            res &= (value == origin.site)
 
151
    return res
 
152
 
 
153
def upgrade_normal(cache, pkgs_to_upgrade, logfile_dpkg):
 
154
    error = None
 
155
    res = False
 
156
    try:
 
157
        with Unlocked():
 
158
            res = cache.commit()
 
159
    except SystemError,e:
 
160
        error = e
 
161
    if res:
 
162
        logging.info(_("All upgrades installed"))
 
163
    else:
 
164
        logging.error(_("Installing the upgrades failed!"))
 
165
        logging.error(_("error message: '%s'") % error)
 
166
        logging.error(_("dpkg returned a error! See '%s' for details") % \
 
167
                          logfile_dpkg)
 
168
    return res
 
169
 
 
170
def upgrade_in_minimal_steps(cache, pkgs_to_upgrade, logfile_dpkg=""):
 
171
    # setup signal handler
 
172
    signal.signal(signal.SIGUSR1, signal_handler)
 
173
 
 
174
    # to upgrade contains the package names
 
175
    to_upgrade = set(pkgs_to_upgrade)
 
176
    while True:
 
177
        # find smallest set
 
178
        smallest_partition = to_upgrade
 
179
        for pkgname in to_upgrade:
 
180
            if SIGNAL_STOP_REQUEST:
 
181
                logging.warn("SIGNAL recieved, stopping")
 
182
                return True
 
183
            pkg = cache[pkgname]
 
184
            if pkg.is_upgradable:
 
185
                pkg.mark_upgrade()
 
186
            elif not pkg.is_installed:
 
187
                pkg.mark_install()
 
188
            else:
 
189
                continue
 
190
            changes = [pkg.name for pkg in cache.get_changes()]
 
191
            if len(changes) == 1:
 
192
                logging.debug("found leaf package %s" % pkg.name)
 
193
                smallest_partition = changes
 
194
                break
 
195
            if len(changes) < len(smallest_partition):
 
196
                logging.debug("found  partition of size %s (%s)" % (len(changes), changes))
 
197
                smallest_partition = changes
 
198
            cache.clear()
 
199
        # apply changes
 
200
        logging.debug("applying set %s" % smallest_partition)
 
201
        rewind_cache(cache, [cache[name] for name in smallest_partition])
 
202
        try:
 
203
            with Unlocked():
 
204
                res = cache.commit()
 
205
                if not res:
 
206
                    raise Exception("cache.commit() returned false")
 
207
            cache.open()
 
208
        except Exception, e:
 
209
            logging.error(_("Installing the upgrades failed!"))
 
210
            logging.error(_("error message: '%s'") % e)
 
211
            logging.error(_("dpkg returned a error! See '%s' for details") % \
 
212
                              logfile_dpkg)
 
213
            return False
 
214
        to_upgrade = to_upgrade-set(smallest_partition)
 
215
        logging.debug("left to upgrade %s" % to_upgrade)
 
216
        if len(to_upgrade) == 0:
 
217
            logging.info(_("All upgrades installed"))
 
218
            break
 
219
    return True
93
220
 
94
221
def is_allowed_origin(pkg, allowed_origins):
95
222
    if not pkg.candidate:
96
223
        return False
97
224
    for origin in pkg.candidate.origins:
98
225
        for allowed in allowed_origins:
99
 
            if origin.origin == allowed[0] and origin.archive == allowed[1]:
 
226
            if match_whitelist_string(allowed, origin):
100
227
                return True
101
228
    return False
102
229
 
111
238
            if not is_allowed_origin(pkg, allowed_origins):
112
239
                logging.debug("pkg '%s' not in allowed origin" % pkg.name)
113
240
                return False
114
 
            if pkg.name in blacklist:
115
 
                logging.debug("pkg '%s' blacklisted" % pkg.name)
116
 
                return False
 
241
            for blacklist_regexp in blacklist:
 
242
                if re.match(blacklist_regexp, pkg.name):
 
243
                    logging.debug("pkg '%s' blacklisted" % pkg.name)
 
244
                    return False
117
245
            if pkg._pkg.selected_state == apt_pkg.SELSTATE_HOLD:
118
246
                logging.debug("pkg '%s' is on hold" % pkg.name)
119
247
                return False
218
346
        out += s + " "
219
347
    return out
220
348
 
221
 
def setup_apt_listchanges():
222
 
    " deal with apt-listchanges "
223
 
    conf = "/etc/apt/listchanges.conf"
 
349
def setup_apt_listchanges(conf="/etc/apt/listchanges.conf"):
 
350
    """ deal with apt-listchanges """
224
351
    if os.path.exists(conf):
225
352
        # check if mail is used by apt-listchanges
226
353
        cf = ConfigParser.ConfigParser()
227
354
        cf.read(conf)
228
 
        if cf.has_section("apt") and cf.has_option("apt","frontend"):
 
355
        if cf.has_section("apt") and cf.has_option("apt", "frontend"):
229
356
            frontend = cf.get("apt","frontend")
230
 
            if frontend == "mail" and os.path.exists("/usr/sbin/sendmail"):
 
357
            if frontend == "mail" and os.path.exists(SENDMAIL_BINARY):
231
358
                # mail frontend and sendmail, we are fine
232
359
                logging.debug("apt-listchanges is set to mail frontend, ignoring")
233
360
                return
234
361
    # setup env (to play it safe) and return
235
 
    os.putenv("APT_LISTCHANGES_FRONTEND","none");
 
362
    os.environ["APT_LISTCHANGES_FRONTEND"] = "none"
236
363
 
237
364
def send_summary_mail(pkgs, res, pkgs_kept_back, mem_log, logfile_dpkg):
238
365
    " send mail (if configured in Unattended-Upgrades::Mail) "
278
405
    logging.debug("mail returned: %s" % ret)
279
406
    
280
407
 
281
 
def main():
282
 
    # init the options
283
 
    parser = OptionParser()
284
 
    parser.add_option("-d", "--debug",
285
 
                      action="store_true", dest="debug", default=False,
286
 
                      help=_("print debug messages"))
287
 
    parser.add_option("", "--dry-run",
288
 
                      action="store_true", default=False,
289
 
                      help=_("Simulation, download but do not install"))
290
 
    (options, args) = parser.parse_args()
 
408
def _setup_alternative_rootdir(rootdir):
 
409
    # clear system unattended-upgrade stuff
 
410
    apt_pkg.config.clear("Unattended-Upgrade")
 
411
    # read rootdir (taken from apt.Cache, but we need to run it
 
412
    # here before the cache gets initialized
 
413
    if os.path.exists(rootdir+"/etc/apt/apt.conf"):
 
414
        apt_pkg.read_config_file(apt_pkg.config,
 
415
                                 rootdir + "/etc/apt/apt.conf")
 
416
    if os.path.isdir(rootdir+"/etc/apt/apt.conf.d"):
 
417
        apt_pkg.read_config_dir(apt_pkg.config,
 
418
                                rootdir + "/etc/apt/apt.conf.d")
 
419
 
 
420
def _get_logdir():
 
421
    logdir= apt_pkg.config.find_dir(
 
422
        "Unattended-Upgrade::LogDir",
 
423
        # COMPAT only
 
424
        apt_pkg.config.find_dir("APT::UnattendedUpgrades::LogDir",
 
425
                                "/var/log/unattended-upgrades/"))
 
426
    return logdir
 
427
 
 
428
def _setup_logging(options):
 
429
    # init the logging
 
430
    logdir = _get_logdir()
 
431
    logfile = os.path.join(
 
432
        logdir,
 
433
        apt_pkg.config.find(
 
434
            "Unattended-Upgrade::LogFile",
 
435
            # COMPAT only
 
436
            apt_pkg.config.find("APT::UnattendedUpgrades::LogFile",
 
437
                                "unattended-upgrades.log")))
 
438
 
 
439
    if not options.dry_run and not os.path.exists(os.path.dirname(logfile)):
 
440
        os.makedirs(os.path.dirname(logfile))
 
441
 
 
442
    logging.basicConfig(level=logging.INFO,
 
443
                        format='%(asctime)s %(levelname)s %(message)s',
 
444
                        filename=logfile)
 
445
 
 
446
 
 
447
def main(options, rootdir=""):
 
448
 
 
449
    # useful for testing
 
450
    if rootdir:
 
451
        _setup_alternative_rootdir(rootdir)
 
452
 
 
453
    _setup_logging(options)
291
454
 
292
455
    # setup logging
293
456
    logger = logging.getLogger()
321
484
        sys.exit(1)
322
485
 
323
486
    # get a cache
324
 
    cache = MyCache()
 
487
    cache = apt.Cache(rootdir=rootdir)
325
488
    if cache._depcache.broken_count > 0:
326
489
        print _("Cache has broken packages, exiting")
327
490
        logging.error(_("Cache has broken packages, exiting"))
439
602
    # exit if there is nothing to do and nothing to report
440
603
    if (len(pkgs_to_upgrade) == 0) and (len(pkgs_kept_back) == 0):
441
604
        logging.info(_("No packages found that can be upgraded unattended"))
442
 
        sys.exit(0)    
 
605
        return
443
606
 
444
607
    # check if we are in dry-run mode
445
608
    if options.dry_run:
460
623
    os.dup2(fd,0)
461
624
 
462
625
    now = datetime.datetime.now()
463
 
    logfile_dpkg = logdir+'unattended-upgrades-dpkg_%s.log' % now.isoformat('_')
 
626
    logfile_dpkg = os.path.join(
 
627
        _get_logdir(), 'unattended-upgrades-dpkg_%s.log' % now.isoformat('_'))
464
628
    logging.info(_("Writing dpkg log to '%s'") % logfile_dpkg)
465
629
    fd = os.open(logfile_dpkg, os.O_RDWR|os.O_CREAT, 0644)
466
630
    old_stdout = os.dup(1)
467
631
    old_stderr = os.dup(2)
468
632
    os.dup2(fd,1)
469
633
    os.dup2(fd,2)
470
 
    
471
 
    # create a new package-manager. the blacklist may have changed
472
 
    # the markings in the depcache
473
 
    pm = apt_pkg.PackageManager(cache._depcache)
474
 
    if not pm.get_archives(fetcher,list,recs):
475
 
        logging.error(_("pm.GetArchives() failed"))
476
 
    # run the fetcher again (otherwise local file:// 
477
 
    # URIs are unhappy (see LP: #56832)
478
 
    res = fetcher.run()
479
 
    # unlock the cache
480
 
    try:
481
 
        apt_pkg.pkgsystem_unlock()
482
 
    except SystemError, e:
483
 
        pass
484
 
    # lock for the shutdown check - its fine if the system
485
 
    # is shutdown while downloading but not so much while installing
486
 
    apt_pkg.get_lock("/var/run/unattended-upgrades.lock")
487
 
    # now do the actual install
488
 
    error = None
489
 
    try:
490
 
        res = pm.do_install()
491
 
    except SystemError,e:
492
 
        error = e
493
 
        res = pm.RESULT_FAILED
494
 
    finally:
495
 
        os.dup2(old_stdout, 1)
496
 
        os.dup2(old_stderr, 2)
497
 
 
498
 
    if res == pm.RESULT_FAILED:
499
 
        logging.error(_("Installing the upgrades failed!"))
500
 
        logging.error(_("error message: '%s'") % e)
501
 
        logging.error(_("dpkg returned a error! See '%s' for details") % logfile_dpkg)
502
 
    else:
503
 
        logging.info(_("All upgrades installed"))
 
634
 
 
635
    try:
 
636
        # lock for the shutdown check - its fine if the system
 
637
        # is shutdown while downloading but not so much while installing
 
638
        apt_pkg.get_lock("/var/run/unattended-upgrades.lock")
 
639
 
 
640
        if (options.minimal_upgrade_steps or 
 
641
            apt_pkg.config.find_b("Unattended-Upgrades::MinimalSteps", False)):
 
642
            open("/var/run/unattended-upgrades.pid", "w").write("%s" % os.getpid())
 
643
            # try upgrade all "pkgs" in minimal steps
 
644
            pkg_install_success = upgrade_in_minimal_steps(
 
645
                cache, [pkg.name for pkg in pkgs_to_upgrade], logfile_dpkg)
 
646
        else:
 
647
            pkg_install_success = upgrade_normal(
 
648
                cache, [pkg.name for pkg in pkgs_to_upgrade], logfile_dpkg)
 
649
    except Exception, e:
 
650
        # print unhandled exceptions here this way, while stderr is redirected
 
651
        os.write(old_stderr, "Exception: %s" % e)
 
652
 
 
653
    # restore
 
654
    os.dup2(old_stdout, 1)
 
655
    os.dup2(old_stderr, 2)
504
656
 
505
657
    # send a mail (if needed)
506
 
    pkg_install_success = (res != pm.RESULT_FAILED)
507
 
    send_summary_mail(pkgs, pkg_install_success, pkgs_kept_back, mem_log, logfile_dpkg)
 
658
    if not options.dry_run:
 
659
        send_summary_mail(
 
660
            pkgs, pkg_install_success, pkgs_kept_back, mem_log, logfile_dpkg)
508
661
 
509
662
    # auto-reboot (if required and the config for this is set
510
663
    if (apt_pkg.config.find_b("Unattended-Upgrade::Automatic-Reboot", False) and
519
672
    gettext.bindtextdomain(localesApp, localesDir)
520
673
    gettext.textdomain(localesApp)
521
674
 
 
675
    # init the options
 
676
    parser = OptionParser()
 
677
    parser.add_option("-d", "--debug",
 
678
                      action="store_true", dest="debug", default=False,
 
679
                      help=_("print debug messages"))
 
680
    parser.add_option("", "--dry-run",
 
681
                      action="store_true", default=False,
 
682
                      help=_("Simulation, download but do not install"))
 
683
    parser.add_option("", "--minimal_upgrade_steps",
 
684
                      action="store_true", default=False,
 
685
                      help=_("Upgrade in minimal steps (and allow interrupting with SIGINT"))
 
686
    (options, args) = parser.parse_args()
 
687
 
522
688
    if os.getuid() != 0:
523
689
        print _("You need to be root to run this application")
524
690
        sys.exit(1)
525
 
    
526
 
    if not os.path.exists("/var/log/unattended-upgrades"):
527
 
        os.makedirs("/var/log/unattended-upgrades")
528
 
 
529
 
    # init the logging
530
 
    logdir = apt_pkg.config.find_dir("APT::UnattendedUpgrades::LogDir",
531
 
                                    "/var/log/unattended-upgrades/")
532
 
    logfile = logdir+apt_pkg.config.find("APT::UnattendedUpgrades::LogFile",
533
 
                                         "unattended-upgrades.log")
534
 
    logging.basicConfig(level=logging.INFO,
535
 
                        format='%(asctime)s %(levelname)s %(message)s',
536
 
                        filename=logfile)
 
691
 
 
692
    # nice & ionce
 
693
    os.nice(19)
 
694
    subprocess.call(["ionice","-c3", "-p",str(os.getpid())])
 
695
 
537
696
    # run the main code
538
 
    main()
 
697
    main(options)