autoupdater.py revision 5c32c72beab5784abff82d07bf2618946c2445ec
1# Copyright (c) 2011 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 os
8import re
9import urlparse
10
11from autotest_lib.client.common_lib import error, global_config
12
13# Local stateful update path is relative to the CrOS source directory.
14LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update'
15REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update'
16STATEFUL_UPDATE = '/tmp/stateful_update'
17UPDATER_BIN = '/usr/bin/update_engine_client'
18UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
19UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
20UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed'
21
22
23class ChromiumOSError(error.InstallError):
24    """Generic error for ChromiumOS-specific exceptions."""
25    pass
26
27
28def url_to_version(update_url):
29    # The ChromiumOS updater respects the last element in the path as
30    # the requested version. Parse it out.
31    return urlparse.urlparse(update_url).path.split('/')[-1]
32
33
34class ChromiumOSUpdater():
35    KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
36    KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
37
38
39    def __init__(self, update_url, host=None):
40        self.host = host
41        self.update_url = update_url
42        self.update_version = url_to_version(update_url)
43
44
45    def check_update_status(self):
46        """Return current status from update-engine."""
47        update_status = self._run(
48            '%s -status 2>&1 | grep CURRENT_OP' % UPDATER_BIN)
49        return update_status.stdout.strip().split('=')[-1]
50
51
52    def reset_update_engine(self):
53        """Restarts the update-engine service."""
54        self._run('rm -f %s' % UPDATED_MARKER)
55        try:
56            self._run('initctl stop update-engine')
57        except error.AutoservRunError:
58            logging.warn('Stopping update-engine service failed. Already dead?')
59        self._run('initctl start update-engine')
60
61        if self.check_update_status() != UPDATER_IDLE:
62            raise ChromiumOSError('%s is not in an installable state' %
63                                  self.host.hostname)
64
65
66    def _run(self, cmd, *args, **kwargs):
67        """Abbreviated form of self.host.run(...)"""
68        return self.host.run(cmd, *args, **kwargs)
69
70
71    def rootdev(self, options=''):
72        """Returns the stripped output of rootdev <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 _update_root(self):
116        # Reset update_engine's state & check that update_engine is idle.
117        self.reset_update_engine()
118
119        # Run update_engine using the specified URL.
120        try:
121            autoupdate_cmd = '%s --update --omaha_url=%s 2>&1' % (
122                UPDATER_BIN, self.update_url)
123            self._run(autoupdate_cmd, timeout=900)
124        except error.AutoservRunError:
125            raise ChromiumOSError('update-engine failed on %s' %
126                                  self.host.hostname)
127
128        # Check that the installer completed as expected.
129        status = self.check_update_status()
130        if status != UPDATER_NEED_REBOOT:
131            raise ChromiumOSError('update-engine error on %s: %s' %
132                                  (self.host.hostname, status))
133
134
135    def _update_stateful(self):
136        # Attempt stateful partition update; this must succeed so that the newly
137        # installed host is testable after update.
138        statefuldev_url = self.update_url.replace('update', 'static/archive')
139
140        # Load the Chrome OS source tree location.
141        stateful_update_path = os.path.join(
142            global_config.global_config.get_config_value(
143                'CROS', 'source_tree', default=''),
144            LOCAL_STATEFUL_UPDATE_PATH)
145
146        if os.path.exists(stateful_update_path):
147            self.host.send_file(
148                stateful_update_path, STATEFUL_UPDATE, delete_dest=True)
149            statefuldev_cmd = [STATEFUL_UPDATE]
150        else:
151            logging.warn('Could not find local stateful_update script, falling'
152                         ' back on client copy.')
153            statefuldev_cmd = [REMOTE_STATEUL_UPDATE_PATH]
154
155        statefuldev_cmd += [statefuldev_url, '--stateful_change=clean', '2>&1']
156        try:
157            self._run(' '.join(statefuldev_cmd), timeout=600)
158        except error.AutoservRunError:
159            self.revert_boot_partition()
160            raise ChromiumOSError('stateful_update failed on %s' %
161                                  self.host.hostname)
162
163
164    def run_update(self, force_update):
165        booted_version = self.get_booted_version()
166        if booted_version in self.update_version and not force_update:
167            logging.info('System is already up to date. Skipping update.')
168            return False
169
170        # Check that Dev Server is accepting connections (from autoserv's host).
171        # If we can't talk to it, the machine host probably can't either.
172        auserver_host = urlparse.urlparse(self.update_url)[1]
173        try:
174            httplib.HTTPConnection(auserver_host).connect()
175        except IOError:
176            raise ChromiumOSError(
177                'Update server at %s not available' % auserver_host)
178
179        logging.info(
180            'Installing from %s to: %s', self.update_url, self.host.hostname)
181
182        logging.info('Updating root partition...')
183        self._update_root()
184
185        logging.info('Updating stateful partition...')
186        self._update_stateful()
187
188        logging.info('Update complete.')
189        return True
190
191
192    def get_booted_version(self):
193        booted_version = self.get_build_id()
194        if not booted_version:
195            booted_version = self.get_dev_build_id()
196        return booted_version
197
198
199    def check_version(self):
200        booted_version = self.get_booted_version()
201        if not booted_version in self.update_version:
202            logging.error('Expected Chromium OS version: %s.'
203                          'Found Chromium OS %s',
204                          self.update_version, booted_version)
205            raise ChromiumOSError('Updater failed on host %s' %
206                                  self.host.hostname)
207        else:
208            return True
209
210
211    def get_build_id(self):
212        """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that
213        matches the build ID."""
214        version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION'
215                            ' /etc/lsb-release').stdout
216        build_re = (r'CHROMEOS_RELEASE_DESCRIPTION='
217                    '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)')
218        version_match = re.match(build_re, version)
219        if version_match:
220            version, build_id, builder = version_match.groups()
221            build_match = re.match(r'.*: (\d+)', builder)
222            if build_match:
223                builder_num = '-b%s' % build_match.group(1)
224            else:
225                builder_num = ''
226            return '%s-r%s%s' % (version, build_id, builder_num)
227
228
229    def get_dev_build_id(self):
230        """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release."""
231        return self._run('grep CHROMEOS_RELEASE_VERSION'
232                         ' /etc/lsb-release').stdout.split('=')[1].strip()
233