autoupdater.py revision fb2b9f2c2a93db2093f5363a6650dd6b7a833e33
1# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import httplib
6import logging
7import re
8import socket
9import urlparse
10
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.cros import constants as chromeos_constants
13
14STATEFULDEV_UPDATER = '/usr/local/bin/stateful_update'
15UPDATER_BIN = '/usr/bin/update_engine_client'
16UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
17UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
18UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed'
19
20
21class ChromiumOSError(error.InstallError):
22    """Generic error for ChromiumOS-specific exceptions."""
23    pass
24
25
26def url_to_version(update_url):
27    # The ChromiumOS updater respects the last element in the path as
28    # the requested version. Parse it out.
29    return urlparse.urlparse(update_url).path.split('/')[-1]
30
31
32class ChromiumOSUpdater():
33    def __init__(self, host=None, update_url=None):
34        self.host = host
35        self.update_url = update_url
36        self.update_version = url_to_version(update_url)
37
38
39    def check_update_status(self):
40        update_status_cmd = ' '.join([UPDATER_BIN, '-status', '2>&1',
41                                      '| grep CURRENT_OP'])
42        update_status = self._run(update_status_cmd)
43        return update_status.stdout.strip().split('=')[-1]
44
45
46    def reset_update_engine(self):
47        logging.info('Resetting update-engine.')
48        self._run('rm -f %s' % UPDATED_MARKER)
49        try:
50            self._run('initctl stop update-engine')
51        except error.AutoservRunError, e:
52            logging.warn('Stopping update-engine service failed. Already dead?')
53        self._run('initctl start update-engine')
54        # May need to wait if service becomes slow to restart.
55        if self.check_update_status() != UPDATER_IDLE:
56            raise ChromiumOSError('%s is not in an installable state' %
57                                  self.host.hostname)
58
59
60    def _run(self, cmd, *args, **kwargs):
61        return self.host.run(cmd, *args, **kwargs)
62
63
64    def rootdev(self):
65        return self._run('rootdev').stdout.strip()
66
67
68    def revert_boot_partition(self):
69        part = self.rootdev()
70        logging.warn('Reverting update; Boot partition will be %s', part)
71        return self._run('/postinst %s 2>&1' % part)
72
73
74    def run_update(self):
75        if not self.update_url:
76            return False
77
78        # Check that devserver is accepting connections (from autoserv's host)
79        # If we can't talk to it, the machine host probably can't either.
80        auserver_host = urlparse.urlparse(self.update_url)[1]
81        try:
82            httplib.HTTPConnection(auserver_host).connect()
83        except socket.error:
84            raise ChromiumOSError('Update server at %s not available' %
85                                  auserver_host)
86
87        logging.info('Installing from %s to: %s' % (self.update_url,
88                                                    self.host.hostname))
89        # Reset update_engine's state & check that update_engine is idle.
90        self.reset_update_engine()
91
92        # Run autoupdate command. This tells the autoupdate process on
93        # the host to look for an update at a specific URL and version
94        # string.
95        autoupdate_cmd = ' '.join([UPDATER_BIN,
96                                   '--update',
97                                   '--omaha_url=%s' % self.update_url,
98                                   ' 2>&1'])
99        logging.info(autoupdate_cmd)
100        try:
101            self._run(autoupdate_cmd, timeout=900)
102        except error.AutoservRunError, e:
103            # Either a runtime error occurred on the host, or
104            # update_engine_client exited with > 0.
105            raise ChromiumOSError('update_engine failed on %s' %
106                                  self.host.hostname)
107
108        # Check that the installer completed as expected.
109        status = self.check_update_status()
110        if status != UPDATER_NEED_REBOOT:
111            raise ChromiumOSError('update-engine error on %s: '
112                                  '"%s" from update-engine' %
113                                  (self.host.hostname, status))
114
115        # Attempt dev & test tools update (which don't live on the
116        # rootfs). This must succeed so that the newly installed host
117        # is testable after we run the autoupdater.
118        statefuldev_url = self.update_url.replace('update', 'static/archive')
119
120        statefuldev_cmd = ' '.join([STATEFULDEV_UPDATER, statefuldev_url,
121                                    '2>&1'])
122        logging.info(statefuldev_cmd)
123        try:
124            self._run(statefuldev_cmd, timeout=600)
125        except error.AutoservRunError, e:
126            # TODO(seano): If statefuldev update failed, we must mark
127            # the update as failed, and keep the same rootfs after
128            # reboot.
129            self.revert_boot_partition()
130            raise ChromiumOSError('stateful_update failed on %s.' %
131                                  self.host.hostname)
132        return True
133
134
135    def check_version(self):
136        booted_version = self.get_build_id()
137        if not booted_version:
138            booted_version = self.get_dev_build_id()
139        if not booted_version in self.update_version:
140            logging.error('Expected Chromium OS version: %s.'
141                          'Found Chromium OS %s',
142                          self.update_version, booted_version)
143            raise ChromiumOSError('Updater failed on host %s' %
144                                  self.host.hostname)
145        else:
146            return True
147
148
149    def get_build_id(self):
150        """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that
151        matches the build ID."""
152        version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION'
153                            ' /etc/lsb-release').stdout
154        build_re = (r'CHROMEOS_RELEASE_DESCRIPTION='
155                    '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)')
156        version_match = re.match(build_re, version)
157        if version_match:
158            version, build_id, builder = version_match.groups()
159            build_match = re.match(r'.*: (\d+)', builder)
160            if build_match:
161                builder_num = '-b%s' % build_match.group(1)
162            else:
163                builder_num = ''
164            return '%s-r%s%s' % (version, build_id, builder_num)
165
166
167    def get_dev_build_id(self):
168        """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release."""
169        return self._run('grep CHROMEOS_RELEASE_VERSION'
170                         ' /etc/lsb-release').stdout.split('=')[1].strip()
171