autoupdater.py revision a5b75c741c5d0fd13d7255373acd8a89e8eed7fb
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 time
10import urlparse
11
12from autotest_lib.client.common_lib import error
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    KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
38    KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
39
40
41    def __init__(self, update_url, host=None):
42        self.host = host
43        self.update_url = update_url
44        self.update_version = url_to_version(update_url)
45
46
47    def check_update_status(self):
48        update_status_cmd = ' '.join([UPDATER_BIN, '-status', '2>&1',
49                                      '| grep CURRENT_OP'])
50        update_status = self._run(update_status_cmd)
51        return update_status.stdout.strip().split('=')[-1]
52
53
54    def reset_update_engine(self):
55        logging.info('Resetting update-engine.')
56        self._run('rm -f %s' % UPDATED_MARKER)
57        try:
58            self._run('initctl stop update-engine')
59        except error.AutoservRunError, e:
60            logging.warn('Stopping update-engine service failed. Already dead?')
61        self._run('initctl start update-engine')
62        # May need to wait if service becomes slow to restart.
63        if self.check_update_status() != UPDATER_IDLE:
64            raise ChromiumOSError('%s is not in an installable state' %
65                                  self.host.hostname)
66
67
68    def _run(self, cmd, *args, **kwargs):
69        return self.host.run(cmd, *args, **kwargs)
70
71
72    def rootdev(self, options=''):
73        return self._run('rootdev %s' % options).stdout.strip()
74
75
76    def get_kernel_state(self):
77        """Returns the (<active>, <inactive>) kernel state as a pair."""
78        active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0])
79        if active_root == self.KERNEL_A['root']:
80            return self.KERNEL_A, self.KERNEL_B
81        elif active_root == self.KERNEL_B['root']:
82            return self.KERNEL_B, self.KERNEL_A
83        else:
84            raise ChromiumOSError('Encountered unknown root partition: %s',
85                                  active_root)
86
87
88    def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
89        """Return numeric cgpt value for the specified flag, kernel, device. """
90        return int(self._run('cgpt show -n -i %d %s %s' % (
91            kernel['kernel'], flag, dev)).stdout.strip())
92
93
94    def get_kernel_priority(self, kernel):
95        """Return numeric priority for the specified kernel."""
96        return self._cgpt('-P', kernel)
97
98
99    def get_kernel_success(self, kernel):
100        """Return boolean success flag for the specified kernel."""
101        return self._cgpt('-S', kernel) != 0
102
103
104    def get_kernel_tries(self, kernel):
105        """Return tries count for the specified kernel."""
106        return self._cgpt('-T', kernel)
107
108
109    def revert_boot_partition(self):
110        part = self.rootdev()
111        logging.warn('Reverting update; Boot partition will be %s', part)
112        return self._run('/postinst %s 2>&1' % part)
113
114
115    def run_update(self, force_update):
116        booted_version = self.get_booted_version()
117        if booted_version in self.update_version and not force_update:
118            logging.info('System is already up to date. Skipping update.')
119            return False
120
121        # Check that devserver is accepting connections (from autoserv's host)
122        # If we can't talk to it, the machine host probably can't either.
123        auserver_host = urlparse.urlparse(self.update_url)[1]
124        try:
125            httplib.HTTPConnection(auserver_host).connect()
126        except socket.error:
127            raise ChromiumOSError('Update server at %s not available' %
128                                  auserver_host)
129
130        logging.info('Installing from %s to: %s' % (self.update_url,
131                                                    self.host.hostname))
132        # Reset update_engine's state & check that update_engine is idle.
133        self.reset_update_engine()
134        # TODO(dalecurtis): Hack! Remove once http://crosbug.com/14954 fixed.
135        time.sleep(120)
136
137        # Run autoupdate command. This tells the autoupdate process on
138        # the host to look for an update at a specific URL and version
139        # string.
140        autoupdate_cmd = ' '.join([UPDATER_BIN,
141                                   '--update',
142                                   '--omaha_url=%s' % self.update_url,
143                                   ' 2>&1'])
144        logging.info(autoupdate_cmd)
145        try:
146            self._run(autoupdate_cmd, timeout=900)
147        except error.AutoservRunError, e:
148            # Either a runtime error occurred on the host, or
149            # update_engine_client exited with > 0.
150            raise ChromiumOSError('update_engine failed on %s' %
151                                  self.host.hostname)
152
153        # Check that the installer completed as expected.
154        status = self.check_update_status()
155        if status != UPDATER_NEED_REBOOT:
156            raise ChromiumOSError('update-engine error on %s: '
157                                  '"%s" from update-engine' %
158                                  (self.host.hostname, status))
159
160        # Attempt dev & test tools update (which don't live on the
161        # rootfs). This must succeed so that the newly installed host
162        # is testable after we run the autoupdater.
163        statefuldev_url = self.update_url.replace('update', 'static/archive')
164
165        # TODO(dalecurtis): HACK to bootstrap stateful updater until
166        # crosbug.com/8960 is fixed.
167        self.host.send_file(LOCAL_STATEFULDEV_UPDATER, STATEFULDEV_UPDATER,
168                            delete_dest=True)
169        statefuldev_cmd = [STATEFULDEV_UPDATER, statefuldev_url]
170
171        # TODO(dalecurtis): HACK necessary until R10 builds are out of testing.
172        if int(self.update_version.split('.')[1]) > 10:
173            statefuldev_cmd.append('--stateful_change=clean')
174
175        statefuldev_cmd.append('2>&1')
176        statefuldev_cmd = ' '.join(statefuldev_cmd)
177
178        logging.info(statefuldev_cmd)
179        try:
180            self._run(statefuldev_cmd, timeout=600)
181        except error.AutoservRunError, e:
182            # TODO(seano): If statefuldev update failed, we must mark
183            # the update as failed, and keep the same rootfs after
184            # reboot.
185            self.revert_boot_partition()
186            raise ChromiumOSError('stateful_update failed on %s.' %
187                                  self.host.hostname)
188        return True
189
190
191    def get_booted_version(self):
192        booted_version = self.get_build_id()
193        if not booted_version:
194            booted_version = self.get_dev_build_id()
195        return booted_version
196
197
198    def check_version(self):
199        booted_version = self.get_booted_version()
200        if not booted_version in self.update_version:
201            logging.error('Expected Chromium OS version: %s.'
202                          'Found Chromium OS %s',
203                          self.update_version, booted_version)
204            raise ChromiumOSError('Updater failed on host %s' %
205                                  self.host.hostname)
206        else:
207            return True
208
209
210    def get_build_id(self):
211        """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that
212        matches the build ID."""
213        version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION'
214                            ' /etc/lsb-release').stdout
215        build_re = (r'CHROMEOS_RELEASE_DESCRIPTION='
216                    '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)')
217        version_match = re.match(build_re, version)
218        if version_match:
219            version, build_id, builder = version_match.groups()
220            build_match = re.match(r'.*: (\d+)', builder)
221            if build_match:
222                builder_num = '-b%s' % build_match.group(1)
223            else:
224                builder_num = ''
225            return '%s-r%s%s' % (version, build_id, builder_num)
226
227
228    def get_dev_build_id(self):
229        """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release."""
230        return self._run('grep CHROMEOS_RELEASE_VERSION'
231                         ' /etc/lsb-release').stdout.split('=')[1].strip()
232