blob: 2a806267739e0c631bc1c2f6309e2be077b51cfa [file] [log] [blame]
Chris Sosa5e4246b2012-05-22 18:05:22 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Sean O'Connor5346e4e2010-08-12 18:49:24 +02002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Don Garrett56b1cc82013-12-06 17:49:20 -08005import glob
Sean O'Connor5346e4e2010-08-12 18:49:24 +02006import logging
Dale Curtis5c32c722011-05-04 19:24:23 -07007import os
Sean O'Connor5346e4e2010-08-12 18:49:24 +02008import re
Richard Barnette0beb14b2018-05-15 18:07:52 +00009import time
Prashanth B32baa9b2014-03-13 13:23:01 -070010import urllib2
Richard Barnette0beb14b2018-05-15 18:07:52 +000011import urlparse
Sean O'Connor5346e4e2010-08-12 18:49:24 +020012
Chris Sosa65425082013-10-16 13:26:22 -070013from autotest_lib.client.bin import utils
Dale Curtis5c32c722011-05-04 19:24:23 -070014from autotest_lib.client.common_lib import error, global_config
Prashanth B32baa9b2014-03-13 13:23:01 -070015from autotest_lib.client.common_lib.cros import dev_server
Richard Barnette0beb14b2018-05-15 18:07:52 +000016from autotest_lib.server import autotest
Shelley Chen61d28982016-10-28 09:40:20 -070017from autotest_lib.server import utils as server_utils
Richard Barnette0beb14b2018-05-15 18:07:52 +000018from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
19from autotest_lib.server.cros.dynamic_suite import tools
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -080020from chromite.lib import retry_util
Dan Shif3a35f72016-01-25 11:18:14 -080021
Shelley Chen16b8df32016-10-27 16:24:21 -070022try:
23 from chromite.lib import metrics
Dan Shi5e2efb72017-02-07 11:40:23 -080024except ImportError:
25 metrics = utils.metrics_mock
Sean O'Connor5346e4e2010-08-12 18:49:24 +020026
Gwendal Grignou3e96cc22017-06-07 16:22:51 -070027try:
28 import devserver
29 STATEFUL_UPDATE_PATH = devserver.__path__[0]
30except ImportError:
31 STATEFUL_UPDATE_PATH = '/usr/bin'
32
Dale Curtis5c32c722011-05-04 19:24:23 -070033# Local stateful update path is relative to the CrOS source directory.
Gwendal Grignou3e96cc22017-06-07 16:22:51 -070034STATEFUL_UPDATE_SCRIPT = 'stateful_update'
Sean O'Connor5346e4e2010-08-12 18:49:24 +020035UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020036UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
beeps5e8c45a2013-12-17 22:05:11 -080037# A list of update engine client states that occur after an update is triggered.
38UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE',
39 'UPDATE_STATUS_UPDATE_AVAILABLE',
40 'UPDATE_STATUS_DOWNLOADING',
41 'UPDATE_STATUS_FINALIZING']
Sean O'Connor5346e4e2010-08-12 18:49:24 +020042
Richard Barnette0beb14b2018-05-15 18:07:52 +000043
44# PROVISION_FAILED - A flag file to indicate provision failures. The
45# file is created at the start of any AU procedure (see
46# `ChromiumOSUpdater.run_full_update()`). The file's location in
47# stateful means that on successul update it will be removed. Thus, if
48# this file exists, it indicates that we've tried and failed in a
49# previous attempt to update.
50PROVISION_FAILED = '/var/tmp/provision_failed'
51
52
Sean O'Connor5346e4e2010-08-12 18:49:24 +020053class ChromiumOSError(error.InstallError):
54 """Generic error for ChromiumOS-specific exceptions."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -070055
56
Chris Sosa77556d82012-04-05 15:23:14 -070057class RootFSUpdateError(ChromiumOSError):
58 """Raised when the RootFS fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070059
60
61class StatefulUpdateError(ChromiumOSError):
62 """Raised when the stateful partition fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070063
64
Sean O'Connor5346e4e2010-08-12 18:49:24 +020065def url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -080066 """Return the version based on update_url.
67
68 @param update_url: url to the image to update to.
69
70 """
Dale Curtisddfdb942011-07-14 13:59:24 -070071 # The Chrome OS version is generally the last element in the URL. The only
72 # exception is delta update URLs, which are rooted under the version; e.g.,
73 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
74 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -070075 return re.sub('/au/.*', '',
76 urlparse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +020077
78
Scott Zawalskieadbf702013-03-14 09:23:06 -040079def url_to_image_name(update_url):
80 """Return the image name based on update_url.
81
82 From a URL like:
83 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
84 return lumpy-release/R27-3837.0.0
85
86 @param update_url: url to the image to update to.
87 @returns a string representing the image name in the update_url.
88
89 """
90 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
91
92
Prashanth B32baa9b2014-03-13 13:23:01 -070093def _get_devserver_build_from_update_url(update_url):
94 """Get the devserver and build from the update url.
95
96 @param update_url: The url for update.
97 Eg: http://devserver:port/update/build.
98
99 @return: A tuple of (devserver url, build) or None if the update_url
100 doesn't match the expected pattern.
101
102 @raises ValueError: If the update_url doesn't match the expected pattern.
103 @raises ValueError: If no global_config was found, or it doesn't contain an
104 image_url_pattern.
105 """
106 pattern = global_config.global_config.get_config_value(
107 'CROS', 'image_url_pattern', type=str, default='')
108 if not pattern:
109 raise ValueError('Cannot parse update_url, the global config needs '
110 'an image_url_pattern.')
111 re_pattern = pattern.replace('%s', '(\S+)')
112 parts = re.search(re_pattern, update_url)
113 if not parts or len(parts.groups()) < 2:
114 raise ValueError('%s is not an update url' % update_url)
115 return parts.groups()
116
117
118def list_image_dir_contents(update_url):
119 """Lists the contents of the devserver for a given build/update_url.
120
121 @param update_url: An update url. Eg: http://devserver:port/update/build.
122 """
123 if not update_url:
124 logging.warning('Need update_url to list contents of the devserver.')
125 return
126 error_msg = 'Cannot check contents of devserver, update url %s' % update_url
127 try:
128 devserver_url, build = _get_devserver_build_from_update_url(update_url)
129 except ValueError as e:
130 logging.warning('%s: %s', error_msg, e)
131 return
132 devserver = dev_server.ImageServer(devserver_url)
133 try:
134 devserver.list_image_dir(build)
135 # The devserver will retry on URLError to avoid flaky connections, but will
136 # eventually raise the URLError if it persists. All HTTPErrors get
137 # converted to DevServerExceptions.
138 except (dev_server.DevServerException, urllib2.URLError) as e:
139 logging.warning('%s: %s', error_msg, e)
140
141
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700142# TODO(garnold) This implements shared updater functionality needed for
143# supporting the autoupdate_EndToEnd server-side test. We should probably
144# migrate more of the existing ChromiumOSUpdater functionality to it as we
145# expand non-CrOS support in other tests.
146class BaseUpdater(object):
147 """Platform-agnostic DUT update functionality."""
148
David Haddock76a4c882017-12-13 18:50:09 -0800149 def __init__(self, updater_ctrl_bin, update_url, host, interactive=True):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700150 """Initializes the object.
151
152 @param updater_ctrl_bin: Path to update_engine_client.
153 @param update_url: The URL we want the update to use.
154 @param host: A client.common_lib.hosts.Host implementation.
David Haddock76a4c882017-12-13 18:50:09 -0800155 @param interactive: Bool whether we are doing an interactive update.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700156 """
157 self.updater_ctrl_bin = updater_ctrl_bin
158 self.update_url = update_url
159 self.host = host
David Haddock76a4c882017-12-13 18:50:09 -0800160 self.interactive = interactive
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700161
162
163 def check_update_status(self):
164 """Returns the current update engine state.
165
166 We use the `update_engine_client -status' command and parse the line
167 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE".
168 """
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800169 update_status = self.host.run(command='%s -status | grep CURRENT_OP' %
170 self.updater_ctrl_bin)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700171 return update_status.stdout.strip().split('=')[-1]
172
173
Shuqian Zhaod9992722016-02-29 12:26:38 -0800174 def get_last_update_error(self):
175 """Get the last autoupdate error code."""
176 error_msg = self.host.run(
177 '%s --last_attempt_error' % self.updater_ctrl_bin)
178 error_msg = (error_msg.stdout.strip()).replace('\n', ', ')
179 return error_msg
180
181
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800182 def _base_update_handler_no_retry(self, run_args):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800183 """Base function to handle a remote update ssh call.
184
185 @param run_args: Dictionary of args passed to ssh_host.run function.
Shuqian Zhaod9992722016-02-29 12:26:38 -0800186
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800187 @throws: intercepts and re-throws all exceptions
Shuqian Zhaod9992722016-02-29 12:26:38 -0800188 """
Shuqian Zhaod9992722016-02-29 12:26:38 -0800189 try:
190 self.host.run(**run_args)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800191 except Exception as e:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800192 logging.debug('exception in update handler: %s', e)
193 raise e
Shuqian Zhaod9992722016-02-29 12:26:38 -0800194
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800195
196 def _base_update_handler(self, run_args, err_msg_prefix=None):
197 """Handle a remote update ssh call, possibly with retries.
198
199 @param run_args: Dictionary of args passed to ssh_host.run function.
200 @param err_msg_prefix: Prefix of the exception error message.
201 """
202 def exception_handler(e):
203 """Examines exceptions and returns True if the update handler
204 should be retried.
205
206 @param e: the exception intercepted by the retry util.
207 """
208 return (isinstance(e, error.AutoservSSHTimeout) or
209 (isinstance(e, error.GenericHostRunError) and
210 hasattr(e, 'description') and
211 (re.search('ERROR_CODE=37', e.description) or
212 re.search('generic error .255.', e.description))))
213
214 try:
215 # Try the update twice (arg 2 is max_retry, not including the first
216 # call). Some exceptions may be caught by the retry handler.
217 retry_util.GenericRetry(exception_handler, 1,
218 self._base_update_handler_no_retry,
219 run_args)
220 except Exception as e:
221 message = err_msg_prefix + ': ' + str(e)
222 raise RootFSUpdateError(message)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800223
224
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800225 def _wait_for_update_service(self):
226 """Ensure that the update engine daemon is running, possibly
227 by waiting for it a bit in case the DUT just rebooted and the
228 service hasn't started yet.
229 """
230 def handler(e):
231 """Retry exception handler.
232
233 Assumes that the error is due to the update service not having
234 started yet.
235
236 @param e: the exception intercepted by the retry util.
237 """
238 if isinstance(e, error.AutoservRunError):
239 logging.debug('update service check exception: %s\n'
240 'retrying...', e)
241 return True
242 else:
243 return False
244
245 # Retry at most three times, every 5s.
246 status = retry_util.GenericRetry(handler, 3,
247 self.check_update_status,
248 sleep=5)
249
250 # Expect the update engine to be idle.
251 if status != UPDATER_IDLE:
252 raise ChromiumOSError('%s is not in an installable state' %
253 self.host.hostname)
254
255
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700256 def trigger_update(self):
257 """Triggers a background update.
258
Shuqian Zhaod9992722016-02-29 12:26:38 -0800259 @raise RootFSUpdateError or unknown Exception if anything went wrong.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700260 """
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800261 # If this function is called immediately after reboot (which it is at
262 # this time), there is no guarantee that the update service is up and
263 # running yet, so wait for it.
264 self._wait_for_update_service()
265
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700266 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' %
267 (self.updater_ctrl_bin, self.update_url))
Shuqian Zhaod9992722016-02-29 12:26:38 -0800268 run_args = {'command': autoupdate_cmd}
269 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700270 logging.info('Triggering update via: %s', autoupdate_cmd)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700271 metric_fields = {'success': False}
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800272 try:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800273 self._base_update_handler(run_args, err_prefix)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700274 metric_fields['success'] = True
275 finally:
Allen Li1a5cc0a2017-06-20 14:08:59 -0700276 c = metrics.Counter('chromeos/autotest/autoupdater/trigger')
Allen Lib5420a72017-06-20 14:14:07 -0700277 metric_fields.update(self._get_metric_fields())
Allen Li1a5cc0a2017-06-20 14:08:59 -0700278 c.increment(fields=metric_fields)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700279
280
Allen Lib5420a72017-06-20 14:14:07 -0700281 def _get_metric_fields(self):
282 """Return a dict of metric fields.
283
284 This is used for sending autoupdate metrics for this instance.
285 """
286 build_name = url_to_image_name(self.update_url)
287 try:
288 board, build_type, milestone, _ = server_utils.ParseBuildName(
289 build_name)
290 except server_utils.ParseBuildNameException:
291 logging.warning('Unable to parse build name %s for metrics. '
292 'Continuing anyway.', build_name)
293 board, build_type, milestone = ('', '', '')
294 return {
295 'dev_server': dev_server.get_hostname(self.update_url),
296 'board': board,
297 'build_type': build_type,
298 'milestone': milestone,
299 }
300
301
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700302 def _verify_update_completed(self):
303 """Verifies that an update has completed.
304
305 @raise RootFSUpdateError: if verification fails.
306 """
307 status = self.check_update_status()
308 if status != UPDATER_NEED_REBOOT:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800309 error_msg = ''
310 if status == UPDATER_IDLE:
311 error_msg = 'Update error: %s' % self.get_last_update_error()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700312 raise RootFSUpdateError('Update did not complete with correct '
Shuqian Zhaod9992722016-02-29 12:26:38 -0800313 'status. Expecting %s, actual %s. %s' %
314 (UPDATER_NEED_REBOOT, status, error_msg))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700315
316
317 def update_image(self):
318 """Updates the device image and verifies success."""
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700319 autoupdate_cmd = ('%s --update --omaha_url=%s' %
Shuqian Zhaod9992722016-02-29 12:26:38 -0800320 (self.updater_ctrl_bin, self.update_url))
David Haddock76a4c882017-12-13 18:50:09 -0800321 if not self.interactive:
322 autoupdate_cmd = '%s --interactive=false' % autoupdate_cmd
Shuqian Zhaod9992722016-02-29 12:26:38 -0800323 run_args = {'command': autoupdate_cmd, 'timeout': 3600}
324 err_prefix = ('Failed to install device image using payload at %s '
325 'on %s. ' % (self.update_url, self.host.hostname))
326 logging.info('Updating image via: %s', autoupdate_cmd)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700327 metric_fields = {'success': False}
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800328 try:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800329 self._base_update_handler(run_args, err_prefix)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700330 metric_fields['success'] = True
331 finally:
Allen Li1a5cc0a2017-06-20 14:08:59 -0700332 c = metrics.Counter('chromeos/autotest/autoupdater/update')
Allen Lib5420a72017-06-20 14:14:07 -0700333 metric_fields.update(self._get_metric_fields())
Allen Li1a5cc0a2017-06-20 14:08:59 -0700334 c.increment(fields=metric_fields)
Dan Shi5e2efb72017-02-07 11:40:23 -0800335
Aviv Keshetf37b2d72016-06-01 19:27:59 -0700336 self._verify_update_completed()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700337
338
339class ChromiumOSUpdater(BaseUpdater):
Dan Shi0f466e82013-02-22 15:44:58 -0800340 """Helper class used to update DUT with image of desired version."""
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700341 REMOTE_STATEFUL_UPDATE_PATH = os.path.join(
342 '/usr/local/bin', STATEFUL_UPDATE_SCRIPT)
343 REMOTE_TMP_STATEFUL_UPDATE = os.path.join(
344 '/tmp', STATEFUL_UPDATE_SCRIPT)
Gilad Arnold0c0df732015-09-21 06:37:59 -0700345 UPDATER_BIN = '/usr/bin/update_engine_client'
Mike Frysinger898bd552017-04-10 23:56:36 -0400346 UPDATED_MARKER = '/run/update_engine_autoupdate_completed'
Gilad Arnold0c0df732015-09-21 06:37:59 -0700347 UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine']
348
Dale Curtisa94c19c2011-05-02 15:05:17 -0700349 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
350 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
Chris Sosa65425082013-10-16 13:26:22 -0700351 # Time to wait for new kernel to be marked successful after
352 # auto update.
353 KERNEL_UPDATE_TIMEOUT = 120
Dale Curtisa94c19c2011-05-02 15:05:17 -0700354
Richard Barnette0beb14b2018-05-15 18:07:52 +0000355 # A flag file used to enable special handling in lab DUTs. Some
356 # parts of the system in Chromium OS test images will behave in ways
357 # convenient to the test lab when this file is present. Generally,
358 # we create this immediately after any update completes.
359 _LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
360
Richard Barnette0173ea82018-05-04 21:13:57 +0000361 def __init__(self, update_url, host=None, interactive=True):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700362 super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url,
David Haddock76a4c882017-12-13 18:50:09 -0800363 host, interactive=interactive)
Richard Barnette0173ea82018-05-04 21:13:57 +0000364 self.update_version = url_to_version(update_url)
Sean Oc053dfe2010-08-23 18:22:26 +0200365
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700366
Sean Oc053dfe2010-08-23 18:22:26 +0200367 def reset_update_engine(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700368 """Resets the host to prepare for a clean update regardless of state."""
Gilad Arnold0c0df732015-09-21 06:37:59 -0700369 self._run('rm -f %s' % self.UPDATED_MARKER)
Chris Sosae92399e2015-04-24 11:32:59 -0700370 self._run('stop ui || true')
371 self._run('stop update-engine || true')
372 self._run('start update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -0700373
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700374 # Wait for update engine to be ready.
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800375 self._wait_for_update_service()
Sean Oc053dfe2010-08-23 18:22:26 +0200376
377
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200378 def _run(self, cmd, *args, **kwargs):
Dale Curtis5c32c722011-05-04 19:24:23 -0700379 """Abbreviated form of self.host.run(...)"""
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200380 return self.host.run(cmd, *args, **kwargs)
381
Sean Oc053dfe2010-08-23 18:22:26 +0200382
Dale Curtisa94c19c2011-05-02 15:05:17 -0700383 def rootdev(self, options=''):
Dan Shi0f466e82013-02-22 15:44:58 -0800384 """Returns the stripped output of rootdev <options>.
385
386 @param options: options to run rootdev.
387
388 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700389 return self._run('rootdev %s' % options).stdout.strip()
390
391
392 def get_kernel_state(self):
393 """Returns the (<active>, <inactive>) kernel state as a pair."""
394 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0])
395 if active_root == self.KERNEL_A['root']:
396 return self.KERNEL_A, self.KERNEL_B
397 elif active_root == self.KERNEL_B['root']:
398 return self.KERNEL_B, self.KERNEL_A
399 else:
Dale Curtis5c32c722011-05-04 19:24:23 -0700400 raise ChromiumOSError('Encountered unknown root partition: %s' %
Dale Curtisa94c19c2011-05-02 15:05:17 -0700401 active_root)
402
403
404 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
405 """Return numeric cgpt value for the specified flag, kernel, device. """
406 return int(self._run('cgpt show -n -i %d %s %s' % (
407 kernel['kernel'], flag, dev)).stdout.strip())
408
409
410 def get_kernel_priority(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800411 """Return numeric priority for the specified kernel.
412
413 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
414
415 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700416 return self._cgpt('-P', kernel)
417
418
419 def get_kernel_success(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800420 """Return boolean success flag for the specified kernel.
421
422 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
423
424 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700425 return self._cgpt('-S', kernel) != 0
426
427
428 def get_kernel_tries(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800429 """Return tries count for the specified kernel.
430
431 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
432
433 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700434 return self._cgpt('-T', kernel)
Sean O267c00b2010-08-31 15:54:55 +0200435
436
Chris Sosa5e4246b2012-05-22 18:05:22 -0700437 def get_stateful_update_script(self):
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700438 """Returns the path to the stateful update script on the target.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700439
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700440 When runnning test_that, stateful_update is in chroot /usr/sbin,
441 as installed by chromeos-base/devserver packages.
442 In the lab, it is installed with the python module devserver, by
443 build_externals.py command.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700444
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700445 If we can find it, we hope it exists already on the DUT, we assert
446 otherwise.
447 """
448 stateful_update_file = os.path.join(STATEFUL_UPDATE_PATH,
449 STATEFUL_UPDATE_SCRIPT)
450 if os.path.exists(stateful_update_file):
Chris Sosa5e4246b2012-05-22 18:05:22 -0700451 self.host.send_file(
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700452 stateful_update_file, self.REMOTE_TMP_STATEFUL_UPDATE,
Gilad Arnold0c0df732015-09-21 06:37:59 -0700453 delete_dest=True)
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700454 return self.REMOTE_TMP_STATEFUL_UPDATE
Chris Sosa5e4246b2012-05-22 18:05:22 -0700455
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700456 if self.host.path_exists(self.REMOTE_STATEFUL_UPDATE_PATH):
457 logging.warning('Could not chroot %s script, falling back on %s',
458 STATEFUL_UPDATE_SCRIPT, self.REMOTE_STATEFUL_UPDATE_PATH)
459 return self.REMOTE_STATEFUL_UPDATE_PATH
460 else:
461 raise ChromiumOSError('Could not locate %s',
462 STATEFUL_UPDATE_SCRIPT)
Chris Sosa5e4246b2012-05-22 18:05:22 -0700463
464
465 def reset_stateful_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800466 """Clear any pending stateful update request."""
Chris Sosa5e4246b2012-05-22 18:05:22 -0700467 statefuldev_cmd = [self.get_stateful_update_script()]
468 statefuldev_cmd += ['--stateful_change=reset', '2>&1']
Chris Sosa66d74072013-09-19 11:21:29 -0700469 self._run(' '.join(statefuldev_cmd))
Chris Sosa5e4246b2012-05-22 18:05:22 -0700470
471
Sean O267c00b2010-08-31 15:54:55 +0200472 def revert_boot_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800473 """Revert the boot partition."""
Dale Curtisd9b26b92011-10-24 13:34:46 -0700474 part = self.rootdev('-s')
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700475 logging.warning('Reverting update; Boot partition will be %s', part)
Sean O267c00b2010-08-31 15:54:55 +0200476 return self._run('/postinst %s 2>&1' % part)
477
478
Chris Sosac1932172013-10-16 13:28:53 -0700479 def rollback_rootfs(self, powerwash):
480 """Triggers rollback and waits for it to complete.
481
482 @param powerwash: If true, powerwash as part of rollback.
483
484 @raise RootFSUpdateError if anything went wrong.
485
486 """
Dan Shi549fb822015-03-24 18:01:11 -0700487 version = self.host.get_release_version()
Chris Sosac8617522014-06-09 23:22:26 +0000488 # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches
489 # X.Y.Z. This version split just pulls the first part out.
490 try:
491 build_number = int(version.split('.')[0])
492 except ValueError:
493 logging.error('Could not parse build number.')
494 build_number = 0
495
496 if build_number >= 5772:
Gilad Arnold0c0df732015-09-21 06:37:59 -0700497 can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN
Chris Sosac8617522014-06-09 23:22:26 +0000498 logging.info('Checking for rollback.')
499 try:
500 self._run(can_rollback_cmd)
501 except error.AutoservRunError as e:
502 raise RootFSUpdateError("Rollback isn't possible on %s: %s" %
503 (self.host.hostname, str(e)))
504
Gilad Arnold0c0df732015-09-21 06:37:59 -0700505 rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN
Chris Sosac1932172013-10-16 13:28:53 -0700506 if not powerwash:
Dan Shif3a35f72016-01-25 11:18:14 -0800507 rollback_cmd += ' --nopowerwash'
Chris Sosac1932172013-10-16 13:28:53 -0700508
Chris Sosac8617522014-06-09 23:22:26 +0000509 logging.info('Performing rollback.')
Chris Sosac1932172013-10-16 13:28:53 -0700510 try:
511 self._run(rollback_cmd)
Chris Sosac1932172013-10-16 13:28:53 -0700512 except error.AutoservRunError as e:
513 raise RootFSUpdateError('Rollback failed on %s: %s' %
514 (self.host.hostname, str(e)))
515
516 self._verify_update_completed()
517
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800518
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700519 # TODO(garnold) This is here for backward compatibility and should be
520 # deprecated once we shift to using update_image() everywhere.
Chris Sosa2f1ae9f2013-08-13 10:00:15 -0700521 def update_rootfs(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700522 """Run the standard command to force an update."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700523 return self.update_image()
Dale Curtis5c32c722011-05-04 19:24:23 -0700524
525
Chris Sosa72312602013-04-16 15:01:56 -0700526 def update_stateful(self, clobber=True):
527 """Updates the stateful partition.
528
529 @param clobber: If True, a clean stateful installation.
530 """
Chris Sosa77556d82012-04-05 15:23:14 -0700531 logging.info('Updating stateful partition...')
joychen03eaad92013-06-26 09:55:21 -0700532 statefuldev_url = self.update_url.replace('update',
533 'static')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700534
Dale Curtis5c32c722011-05-04 19:24:23 -0700535 # Attempt stateful partition update; this must succeed so that the newly
536 # installed host is testable after update.
Chris Sosa72312602013-04-16 15:01:56 -0700537 statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url]
538 if clobber:
539 statefuldev_cmd.append('--stateful_change=clean')
540
541 statefuldev_cmd.append('2>&1')
Dale Curtis5c32c722011-05-04 19:24:23 -0700542 try:
Dan Shi205b8732016-01-25 10:56:22 -0800543 self._run(' '.join(statefuldev_cmd), timeout=1200)
Dale Curtis5c32c722011-05-04 19:24:23 -0700544 except error.AutoservRunError:
Gilad Arnold62cf3a42015-10-01 09:15:25 -0700545 update_error = StatefulUpdateError(
546 'Failed to perform stateful update on %s' %
547 self.host.hostname)
Chris Sosa77556d82012-04-05 15:23:14 -0700548 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700549
Chris Sosaa3ac2152012-05-23 22:23:13 -0700550
Richard Barnette0beb14b2018-05-15 18:07:52 +0000551 def _install_update(self, update_root=True):
552 """Install the requested image on the DUT, but don't start it.
553
554 This downloads all content needed for the requested update, and
555 installs it in place on the DUT. This does not reboot the DUT,
556 so the update is merely pending when the function returns.
557
558 @param update_root: When true, force a rootfs update; otherwise
559 update the stateful partition only.
Dan Shi0f466e82013-02-22 15:44:58 -0800560 """
Dan Shi549fb822015-03-24 18:01:11 -0700561 booted_version = self.host.get_release_version()
Richard Barnette0173ea82018-05-04 21:13:57 +0000562 logging.info('Updating from version %s to %s.',
563 booted_version, self.update_version)
Dale Curtis53d55862011-05-16 12:17:59 -0700564
Dale Curtis5c32c722011-05-04 19:24:23 -0700565 # Check that Dev Server is accepting connections (from autoserv's host).
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200566 # If we can't talk to it, the machine host probably can't either.
xixuanccf2e722016-06-10 16:42:38 -0700567 auserver_host = 'http://%s' % urlparse.urlparse(self.update_url)[1]
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200568 try:
xixuanccf2e722016-06-10 16:42:38 -0700569 if not dev_server.ImageServer.devserver_healthy(auserver_host):
570 raise ChromiumOSError(
571 'Update server at %s not healthy' % auserver_host)
572 except Exception as e:
573 logging.debug('Error happens in connection to devserver: %r', e)
Dale Curtis5c32c722011-05-04 19:24:23 -0700574 raise ChromiumOSError(
575 'Update server at %s not available' % auserver_host)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200576
Chris Sosaa3ac2152012-05-23 22:23:13 -0700577 logging.info('Installing from %s to %s', self.update_url,
Chris Sosa77556d82012-04-05 15:23:14 -0700578 self.host.hostname)
579
Chris Sosa5e4246b2012-05-22 18:05:22 -0700580 # Reset update state.
Chris Sosa77556d82012-04-05 15:23:14 -0700581 self.reset_update_engine()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700582 self.reset_stateful_partition()
Sean Oc053dfe2010-08-23 18:22:26 +0200583
Dale Curtis1e973182011-07-12 18:21:36 -0700584 try:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700585 try:
586 if not update_root:
587 logging.info('Root update is skipped.')
588 else:
589 self.update_rootfs()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200590
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700591 self.update_stateful()
592 except:
Chris Sosa77556d82012-04-05 15:23:14 -0700593 self.revert_boot_partition()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700594 self.reset_stateful_partition()
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700595 raise
Sean Oc053dfe2010-08-23 18:22:26 +0200596
Dale Curtis1e973182011-07-12 18:21:36 -0700597 logging.info('Update complete.')
Dale Curtis1e973182011-07-12 18:21:36 -0700598 except:
599 # Collect update engine logs in the event of failure.
600 if self.host.job:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700601 logging.info('Collecting update engine logs due to failure...')
Dale Curtis1e973182011-07-12 18:21:36 -0700602 self.host.get_file(
Gilad Arnold0c0df732015-09-21 06:37:59 -0700603 self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
604 preserve_perm=False)
Prashanth B32baa9b2014-03-13 13:23:01 -0700605 list_image_dir_contents(self.update_url)
Dale Curtis1e973182011-07-12 18:21:36 -0700606 raise
Dan Shi10e992b2013-08-30 11:02:59 -0700607 finally:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800608 logging.info('Update engine log has downloaded in '
609 'sysinfo/update_engine dir. Check the lastest.')
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200610
611
Richard Barnette0beb14b2018-05-15 18:07:52 +0000612 def _try_stateful_update(self):
613 """Try to use stateful update to initialize DUT.
614
615 When DUT is already running the same version that machine_install
616 tries to install, stateful update is a much faster way to clean up
617 the DUT for testing, compared to a full reimage. It is implemeted
618 by calling autoupdater._run_full_update, but skipping updating root,
619 as updating the kernel is time consuming and not necessary.
620
621 @param update_url: url of the image.
622 @param updater: ChromiumOSUpdater instance used to update the DUT.
623 @returns: True if the DUT was updated with stateful update.
624
625 """
626 self.host.prepare_for_update()
627
628 # TODO(jrbarnette): Yes, I hate this re.match() test case.
629 # It's better than the alternative: see crbug.com/360944.
630 image_name = url_to_image_name(self.update_url)
631 release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$'
632 if not re.match(release_pattern, image_name):
633 return False
634 if not self.check_version():
635 return False
636 # Following folders should be rebuilt after stateful update.
637 # A test file is used to confirm each folder gets rebuilt after
638 # the stateful update.
639 folders_to_check = ['/var', '/home', '/mnt/stateful_partition']
640 test_file = '.test_file_to_be_deleted'
641 paths = [os.path.join(folder, test_file) for folder in folders_to_check]
642 self._run('touch %s' % ' '.join(paths))
643
644 self._install_update(update_root=False)
645
646 # Reboot to complete stateful update.
647 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
648
649 # After stateful update and a reboot, all of the test_files shouldn't
650 # exist any more. Otherwise the stateful update is failed.
651 return not any(
652 self.host.path_exists(os.path.join(folder, test_file))
653 for folder in folders_to_check)
654
655
656 def _post_update_processing(self, expected_kernel):
657 """After the DUT is updated, confirm machine_install succeeded.
658
659 @param updater: ChromiumOSUpdater instance used to update the DUT.
660 @param expected_kernel: kernel expected to be active after reboot,
661 or `None` to skip rollback checking.
662
663 """
664 # Touch the lab machine file to leave a marker that
665 # distinguishes this image from other test images.
666 # Afterwards, we must re-run the autoreboot script because
667 # it depends on the _LAB_MACHINE_FILE.
668 autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
669 '( touch "$FILE" ; start autoreboot )')
670 self._run(autoreboot_cmd % self._LAB_MACHINE_FILE)
671 self.verify_boot_expectations(
672 expected_kernel, rollback_message=
673 'Build %s failed to boot on %s; system rolled back to previous '
674 'build' % (self.update_version, self.host.hostname))
675
676 logging.debug('Cleaning up old autotest directories.')
677 try:
678 installed_autodir = autotest.Autotest.get_installed_autodir(
679 self.host)
680 self._run('rm -rf ' + installed_autodir)
681 except autotest.AutodirNotFoundError:
682 logging.debug('No autotest installed directory found.')
683
684
685 def run_update(self, force_full_update):
686 """Perform a full update of a DUT in the test lab.
687
688 This downloads and installs the root FS and stateful partition
689 content needed for the update specified in `self.host` and
690 `self.update_url`. The update is performed according to the
691 requirements for provisioning a DUT for testing the requested
692 build.
693
694 @param force_full_update: When true, update the root file
695 system to the new build, even if the target DUT already has
696 that build installed.
697 @returns A tuple of the form `(image_name, attributes)`, where
698 `image_name` is the name of the image installed, and
699 `attributes` is new attributes to be applied to the DUT.
700 """
701 logging.debug('Update URL is %s', self.update_url)
702
703 # Report provision stats.
704 server_name = dev_server.get_hostname(self.update_url)
705 (metrics.Counter('chromeos/autotest/provision/install')
706 .increment(fields={'devserver': server_name}))
707
708 # Create a file to indicate if provision fails. The file will be
709 # removed by any successful update.
710 self._run('touch %s' % PROVISION_FAILED)
711
712 update_complete = False
713 if not force_full_update:
714 try:
715 # If the DUT is already running the same build, try stateful
716 # update first as it's much quicker than a full re-image.
717 update_complete = self._try_stateful_update()
718 except Exception as e:
719 logging.exception(e)
720
721 inactive_kernel = None
722 if update_complete:
723 logging.info('Install complete without full update')
724 else:
725 logging.info('DUT requires full update.')
726 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
727 self.host.prepare_for_update()
728
729 self._install_update()
730
731 # Give it some time in case of IO issues.
732 time.sleep(10)
733
734 # Figure out active and inactive kernel.
735 active_kernel, inactive_kernel = self.get_kernel_state()
736
737 # Ensure inactive kernel has higher priority than active.
738 if (self.get_kernel_priority(inactive_kernel)
739 < self.get_kernel_priority(active_kernel)):
740 raise ChromiumOSError(
741 'Update failed. The priority of the inactive kernel'
742 ' partition is less than that of the active kernel'
743 ' partition.')
744
745 # Update has returned successfully; reboot the host.
746 #
747 # Regarding the 'crossystem' command below: In some cases,
748 # the update flow puts the TPM into a state such that it
749 # fails verification. We don't know why. However, this
750 # call papers over the problem by clearing the TPM during
751 # the reboot.
752 #
753 # We ignore failures from 'crossystem'. Although failure
754 # here is unexpected, and could signal a bug, the point of
755 # the exercise is to paper over problems; allowing this to
756 # fail would defeat the purpose.
757 self._run('crossystem clear_tpm_owner_request=1',
758 ignore_status=True)
759 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
760
761 self._post_update_processing(inactive_kernel)
762 image_name = url_to_image_name(self.update_url)
763 # update_url is different from devserver url needed to stage autotest
764 # packages, therefore, resolve a new devserver url here.
765 devserver_url = dev_server.ImageServer.resolve(
766 image_name, self.host.hostname).url()
767 repo_url = tools.get_package_url(devserver_url, image_name)
768 return image_name, {ds_constants.JOB_REPO_URL: repo_url}
769
770
Dale Curtisa94c19c2011-05-02 15:05:17 -0700771 def check_version(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800772 """Check the image running in DUT has the desired version.
773
774 @returns: True if the DUT's image version matches the version that
775 the autoupdater tries to update to.
776
777 """
Dan Shi549fb822015-03-24 18:01:11 -0700778 booted_version = self.host.get_release_version()
Richard Barnette0173ea82018-05-04 21:13:57 +0000779 return self.update_version.endswith(booted_version)
Dan Shib95bb862013-03-22 16:29:28 -0700780
781
782 def check_version_to_confirm_install(self):
783 """Check image running in DUT has the desired version to be installed.
784
785 The method should not be used to check if DUT needs to have a full
786 reimage. Only use it to confirm a image is installed.
787
Dan Shi549fb822015-03-24 18:01:11 -0700788 The method is designed to verify version for following 6 scenarios with
Dan Shi190c7802013-04-04 13:05:30 -0700789 samples of version to update to and expected booted version:
790 1. trybot paladin build.
791 update version: trybot-lumpy-paladin/R27-3837.0.0-b123
792 booted version: 3837.0.2013_03_21_1340
793
794 2. trybot release build.
795 update version: trybot-lumpy-release/R27-3837.0.0-b456
796 booted version: 3837.0.0
797
798 3. buildbot official release build.
799 update version: lumpy-release/R27-3837.0.0
800 booted version: 3837.0.0
801
802 4. non-official paladin rc build.
803 update version: lumpy-paladin/R27-3878.0.0-rc7
804 booted version: 3837.0.0-rc7
Dan Shib95bb862013-03-22 16:29:28 -0700805
Dan Shi7f795512013-04-12 10:08:17 -0700806 5. chrome-perf build.
807 update version: lumpy-chrome-perf/R28-3837.0.0-b2996
808 booted version: 3837.0.0
809
Dan Shi73aa2902013-05-03 11:22:11 -0700810 6. pgo-generate build.
811 update version: lumpy-release-pgo-generate/R28-3837.0.0-b2996
812 booted version: 3837.0.0-pgo-generate
813
Dan Shib95bb862013-03-22 16:29:28 -0700814 When we are checking if a DUT needs to do a full install, we should NOT
815 use this method to check if the DUT is running the same version, since
Dan Shi190c7802013-04-04 13:05:30 -0700816 it may return false positive for a DUT running trybot paladin build to
817 be updated to another trybot paladin build.
Dan Shib95bb862013-03-22 16:29:28 -0700818
Dan Shi190c7802013-04-04 13:05:30 -0700819 TODO: This logic has a bug if a trybot paladin build failed to be
820 installed in a DUT running an older trybot paladin build with same
821 platform number, but different build number (-b###). So to conclusively
822 determine if a tryjob paladin build is imaged successfully, we may need
823 to find out the date string from update url.
Dan Shib95bb862013-03-22 16:29:28 -0700824
825 @returns: True if the DUT's image version (without the date string if
826 the image is a trybot build), matches the version that the
827 autoupdater is trying to update to.
828
829 """
830 # Always try the default check_version method first, this prevents
831 # any backward compatibility issue.
832 if self.check_version():
833 return True
834
Dan Shi549fb822015-03-24 18:01:11 -0700835 return utils.version_match(self.update_version,
836 self.host.get_release_version(),
837 self.update_url)
Chris Sosa65425082013-10-16 13:26:22 -0700838
839
840 def verify_boot_expectations(self, expected_kernel_state, rollback_message):
841 """Verifies that we fully booted given expected kernel state.
842
843 This method both verifies that we booted using the correct kernel
844 state and that the OS has marked the kernel as good.
845
846 @param expected_kernel_state: kernel state that we are verifying with
847 i.e. I expect to be booted onto partition 4 etc. See output of
848 get_kernel_state.
849 @param rollback_message: string to raise as a ChromiumOSError
850 if we booted with the wrong partition.
851
852 @raises ChromiumOSError: If we didn't.
853 """
854 # Figure out the newly active kernel.
855 active_kernel_state = self.get_kernel_state()[0]
856
857 # Check for rollback due to a bad build.
858 if (expected_kernel_state and
859 active_kernel_state != expected_kernel_state):
Don Garrett56b1cc82013-12-06 17:49:20 -0800860
861 # Kernel crash reports should be wiped between test runs, but
862 # may persist from earlier parts of the test, or from problems
863 # with provisioning.
864 #
865 # Kernel crash reports will NOT be present if the crash happened
866 # before encrypted stateful is mounted.
867 #
868 # TODO(dgarrett): Integrate with server/crashcollect.py at some
869 # point.
870 kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash')
871 if kernel_crashes:
872 rollback_message += ': kernel_crash'
873 logging.debug('Found %d kernel crash reports:',
874 len(kernel_crashes))
875 # The crash names contain timestamps that may be useful:
876 # kernel.20131207.005945.0.kcrash
877 for crash in kernel_crashes:
Dan Shi0942b1d2015-03-31 11:07:00 -0700878 logging.debug(' %s', os.path.basename(crash))
Don Garrett56b1cc82013-12-06 17:49:20 -0800879
Chris Sosa65425082013-10-16 13:26:22 -0700880 # Print out some information to make it easier to debug
881 # the rollback.
882 logging.debug('Dumping partition table.')
883 self._run('cgpt show $(rootdev -s -d)')
884 logging.debug('Dumping crossystem for firmware debugging.')
885 self._run('crossystem --all')
886 raise ChromiumOSError(rollback_message)
887
888 # Make sure chromeos-setgoodkernel runs.
889 try:
890 utils.poll_for_condition(
891 lambda: (self.get_kernel_tries(active_kernel_state) == 0
892 and self.get_kernel_success(active_kernel_state)),
893 exception=ChromiumOSError(),
894 timeout=self.KERNEL_UPDATE_TIMEOUT, sleep_interval=5)
895 except ChromiumOSError:
896 services_status = self._run('status system-services').stdout
897 if services_status != 'system-services start/running\n':
898 event = ('Chrome failed to reach login screen')
899 else:
900 event = ('update-engine failed to call '
901 'chromeos-setgoodkernel')
902 raise ChromiumOSError(
903 'After update and reboot, %s '
904 'within %d seconds' % (event,
905 self.KERNEL_UPDATE_TIMEOUT))