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