blob: 329f54ac758b3606237f844e298a720cac8f94cc [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
Richard Barnette3e8b2282018-05-15 20:42:20 +000029 _STATEFUL_UPDATE_PATH = devserver.__path__[0]
Gwendal Grignou3e96cc22017-06-07 16:22:51 -070030except ImportError:
Richard Barnette3e8b2282018-05-15 20:42:20 +000031 _STATEFUL_UPDATE_PATH = '/usr/bin'
Gwendal Grignou3e96cc22017-06-07 16:22:51 -070032
Dale Curtis5c32c722011-05-04 19:24:23 -070033# Local stateful update path is relative to the CrOS source directory.
Sean O'Connor5346e4e2010-08-12 18:49:24 +020034UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020035UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
beeps5e8c45a2013-12-17 22:05:11 -080036# A list of update engine client states that occur after an update is triggered.
37UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE',
38 'UPDATE_STATUS_UPDATE_AVAILABLE',
39 'UPDATE_STATUS_DOWNLOADING',
40 'UPDATE_STATUS_FINALIZING']
Sean O'Connor5346e4e2010-08-12 18:49:24 +020041
Richard Barnette0beb14b2018-05-15 18:07:52 +000042
Richard Barnette3e8b2282018-05-15 20:42:20 +000043_STATEFUL_UPDATE_SCRIPT = 'stateful_update'
44_REMOTE_STATEFUL_UPDATE_PATH = os.path.join(
45 '/usr/local/bin', _STATEFUL_UPDATE_SCRIPT)
46_REMOTE_TMP_STATEFUL_UPDATE = os.path.join(
47 '/tmp', _STATEFUL_UPDATE_SCRIPT)
48
49_UPDATER_BIN = '/usr/bin/update_engine_client'
50_UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine']
51
52_KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
53_KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
54
55# Time to wait for new kernel to be marked successful after
56# auto update.
57_KERNEL_UPDATE_TIMEOUT = 120
58
59
Richard Barnette0beb14b2018-05-15 18:07:52 +000060# PROVISION_FAILED - A flag file to indicate provision failures. The
61# file is created at the start of any AU procedure (see
62# `ChromiumOSUpdater.run_full_update()`). The file's location in
63# stateful means that on successul update it will be removed. Thus, if
64# this file exists, it indicates that we've tried and failed in a
65# previous attempt to update.
66PROVISION_FAILED = '/var/tmp/provision_failed'
67
68
Richard Barnette3e8b2282018-05-15 20:42:20 +000069# A flag file used to enable special handling in lab DUTs. Some
70# parts of the system in Chromium OS test images will behave in ways
71# convenient to the test lab when this file is present. Generally,
72# we create this immediately after any update completes.
73_LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
74
75
Sean O'Connor5346e4e2010-08-12 18:49:24 +020076class ChromiumOSError(error.InstallError):
77 """Generic error for ChromiumOS-specific exceptions."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -070078
79
Chris Sosa77556d82012-04-05 15:23:14 -070080class RootFSUpdateError(ChromiumOSError):
81 """Raised when the RootFS fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070082
83
84class StatefulUpdateError(ChromiumOSError):
85 """Raised when the stateful partition fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070086
87
Richard Barnette3e8b2282018-05-15 20:42:20 +000088def _url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -080089 """Return the version based on update_url.
90
91 @param update_url: url to the image to update to.
92
93 """
Dale Curtisddfdb942011-07-14 13:59:24 -070094 # The Chrome OS version is generally the last element in the URL. The only
95 # exception is delta update URLs, which are rooted under the version; e.g.,
96 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
97 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -070098 return re.sub('/au/.*', '',
99 urlparse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200100
101
Scott Zawalskieadbf702013-03-14 09:23:06 -0400102def url_to_image_name(update_url):
103 """Return the image name based on update_url.
104
105 From a URL like:
106 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
107 return lumpy-release/R27-3837.0.0
108
109 @param update_url: url to the image to update to.
110 @returns a string representing the image name in the update_url.
111
112 """
113 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
114
115
Prashanth B32baa9b2014-03-13 13:23:01 -0700116def _get_devserver_build_from_update_url(update_url):
117 """Get the devserver and build from the update url.
118
119 @param update_url: The url for update.
120 Eg: http://devserver:port/update/build.
121
122 @return: A tuple of (devserver url, build) or None if the update_url
123 doesn't match the expected pattern.
124
125 @raises ValueError: If the update_url doesn't match the expected pattern.
126 @raises ValueError: If no global_config was found, or it doesn't contain an
127 image_url_pattern.
128 """
129 pattern = global_config.global_config.get_config_value(
130 'CROS', 'image_url_pattern', type=str, default='')
131 if not pattern:
132 raise ValueError('Cannot parse update_url, the global config needs '
133 'an image_url_pattern.')
134 re_pattern = pattern.replace('%s', '(\S+)')
135 parts = re.search(re_pattern, update_url)
136 if not parts or len(parts.groups()) < 2:
137 raise ValueError('%s is not an update url' % update_url)
138 return parts.groups()
139
140
Richard Barnette3e8b2282018-05-15 20:42:20 +0000141def _list_image_dir_contents(update_url):
Prashanth B32baa9b2014-03-13 13:23:01 -0700142 """Lists the contents of the devserver for a given build/update_url.
143
144 @param update_url: An update url. Eg: http://devserver:port/update/build.
145 """
146 if not update_url:
147 logging.warning('Need update_url to list contents of the devserver.')
148 return
149 error_msg = 'Cannot check contents of devserver, update url %s' % update_url
150 try:
151 devserver_url, build = _get_devserver_build_from_update_url(update_url)
152 except ValueError as e:
153 logging.warning('%s: %s', error_msg, e)
154 return
155 devserver = dev_server.ImageServer(devserver_url)
156 try:
157 devserver.list_image_dir(build)
158 # The devserver will retry on URLError to avoid flaky connections, but will
159 # eventually raise the URLError if it persists. All HTTPErrors get
160 # converted to DevServerExceptions.
161 except (dev_server.DevServerException, urllib2.URLError) as e:
162 logging.warning('%s: %s', error_msg, e)
163
164
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700165# TODO(garnold) This implements shared updater functionality needed for
166# supporting the autoupdate_EndToEnd server-side test. We should probably
167# migrate more of the existing ChromiumOSUpdater functionality to it as we
168# expand non-CrOS support in other tests.
Richard Barnette3e8b2282018-05-15 20:42:20 +0000169class ChromiumOSUpdater(object):
170 """Chromium OS specific DUT update functionality."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700171
Richard Barnette3e8b2282018-05-15 20:42:20 +0000172 def __init__(self, update_url, host=None, interactive=True):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700173 """Initializes the object.
174
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700175 @param update_url: The URL we want the update to use.
176 @param host: A client.common_lib.hosts.Host implementation.
David Haddock76a4c882017-12-13 18:50:09 -0800177 @param interactive: Bool whether we are doing an interactive update.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700178 """
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700179 self.update_url = update_url
180 self.host = host
David Haddock76a4c882017-12-13 18:50:09 -0800181 self.interactive = interactive
Richard Barnette3e8b2282018-05-15 20:42:20 +0000182 self.update_version = _url_to_version(update_url)
183
184
185 def _run(self, cmd, *args, **kwargs):
186 """Abbreviated form of self.host.run(...)"""
187 return self.host.run(cmd, *args, **kwargs)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700188
189
190 def check_update_status(self):
191 """Returns the current update engine state.
192
193 We use the `update_engine_client -status' command and parse the line
194 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE".
195 """
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800196 update_status = self.host.run(command='%s -status | grep CURRENT_OP' %
Richard Barnette3e8b2282018-05-15 20:42:20 +0000197 _UPDATER_BIN)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700198 return update_status.stdout.strip().split('=')[-1]
199
200
Richard Barnette3e8b2282018-05-15 20:42:20 +0000201 def _get_last_update_error(self):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800202 """Get the last autoupdate error code."""
Richard Barnette3e8b2282018-05-15 20:42:20 +0000203 command_result = self._run(
204 '%s --last_attempt_error' % _UPDATER_BIN)
205 return command_result.stdout.strip().replace('\n', ', ')
Shuqian Zhaod9992722016-02-29 12:26:38 -0800206
207
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800208 def _base_update_handler_no_retry(self, run_args):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800209 """Base function to handle a remote update ssh call.
210
211 @param run_args: Dictionary of args passed to ssh_host.run function.
Shuqian Zhaod9992722016-02-29 12:26:38 -0800212
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800213 @throws: intercepts and re-throws all exceptions
Shuqian Zhaod9992722016-02-29 12:26:38 -0800214 """
Shuqian Zhaod9992722016-02-29 12:26:38 -0800215 try:
216 self.host.run(**run_args)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800217 except Exception as e:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800218 logging.debug('exception in update handler: %s', e)
219 raise e
Shuqian Zhaod9992722016-02-29 12:26:38 -0800220
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800221
222 def _base_update_handler(self, run_args, err_msg_prefix=None):
223 """Handle a remote update ssh call, possibly with retries.
224
225 @param run_args: Dictionary of args passed to ssh_host.run function.
226 @param err_msg_prefix: Prefix of the exception error message.
227 """
228 def exception_handler(e):
229 """Examines exceptions and returns True if the update handler
230 should be retried.
231
232 @param e: the exception intercepted by the retry util.
233 """
234 return (isinstance(e, error.AutoservSSHTimeout) or
235 (isinstance(e, error.GenericHostRunError) and
236 hasattr(e, 'description') and
237 (re.search('ERROR_CODE=37', e.description) or
238 re.search('generic error .255.', e.description))))
239
240 try:
241 # Try the update twice (arg 2 is max_retry, not including the first
242 # call). Some exceptions may be caught by the retry handler.
243 retry_util.GenericRetry(exception_handler, 1,
244 self._base_update_handler_no_retry,
245 run_args)
246 except Exception as e:
247 message = err_msg_prefix + ': ' + str(e)
248 raise RootFSUpdateError(message)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800249
250
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800251 def _wait_for_update_service(self):
252 """Ensure that the update engine daemon is running, possibly
253 by waiting for it a bit in case the DUT just rebooted and the
254 service hasn't started yet.
255 """
256 def handler(e):
257 """Retry exception handler.
258
259 Assumes that the error is due to the update service not having
260 started yet.
261
262 @param e: the exception intercepted by the retry util.
263 """
264 if isinstance(e, error.AutoservRunError):
265 logging.debug('update service check exception: %s\n'
266 'retrying...', e)
267 return True
268 else:
269 return False
270
271 # Retry at most three times, every 5s.
272 status = retry_util.GenericRetry(handler, 3,
273 self.check_update_status,
274 sleep=5)
275
276 # Expect the update engine to be idle.
277 if status != UPDATER_IDLE:
278 raise ChromiumOSError('%s is not in an installable state' %
279 self.host.hostname)
280
281
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700282 def trigger_update(self):
283 """Triggers a background update.
284
Shuqian Zhaod9992722016-02-29 12:26:38 -0800285 @raise RootFSUpdateError or unknown Exception if anything went wrong.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700286 """
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800287 # If this function is called immediately after reboot (which it is at
288 # this time), there is no guarantee that the update service is up and
289 # running yet, so wait for it.
290 self._wait_for_update_service()
291
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700292 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' %
Richard Barnette3e8b2282018-05-15 20:42:20 +0000293 (_UPDATER_BIN, self.update_url))
Shuqian Zhaod9992722016-02-29 12:26:38 -0800294 run_args = {'command': autoupdate_cmd}
295 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700296 logging.info('Triggering update via: %s', autoupdate_cmd)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700297 metric_fields = {'success': False}
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800298 try:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800299 self._base_update_handler(run_args, err_prefix)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700300 metric_fields['success'] = True
301 finally:
Allen Li1a5cc0a2017-06-20 14:08:59 -0700302 c = metrics.Counter('chromeos/autotest/autoupdater/trigger')
Allen Lib5420a72017-06-20 14:14:07 -0700303 metric_fields.update(self._get_metric_fields())
Allen Li1a5cc0a2017-06-20 14:08:59 -0700304 c.increment(fields=metric_fields)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700305
306
Allen Lib5420a72017-06-20 14:14:07 -0700307 def _get_metric_fields(self):
308 """Return a dict of metric fields.
309
310 This is used for sending autoupdate metrics for this instance.
311 """
312 build_name = url_to_image_name(self.update_url)
313 try:
314 board, build_type, milestone, _ = server_utils.ParseBuildName(
315 build_name)
316 except server_utils.ParseBuildNameException:
317 logging.warning('Unable to parse build name %s for metrics. '
318 'Continuing anyway.', build_name)
319 board, build_type, milestone = ('', '', '')
320 return {
321 'dev_server': dev_server.get_hostname(self.update_url),
322 'board': board,
323 'build_type': build_type,
324 'milestone': milestone,
325 }
326
327
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700328 def _verify_update_completed(self):
329 """Verifies that an update has completed.
330
331 @raise RootFSUpdateError: if verification fails.
332 """
333 status = self.check_update_status()
334 if status != UPDATER_NEED_REBOOT:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800335 error_msg = ''
336 if status == UPDATER_IDLE:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000337 error_msg = 'Update error: %s' % self._get_last_update_error()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700338 raise RootFSUpdateError('Update did not complete with correct '
Shuqian Zhaod9992722016-02-29 12:26:38 -0800339 'status. Expecting %s, actual %s. %s' %
340 (UPDATER_NEED_REBOOT, status, error_msg))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700341
342
343 def update_image(self):
344 """Updates the device image and verifies success."""
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700345 autoupdate_cmd = ('%s --update --omaha_url=%s' %
Richard Barnette3e8b2282018-05-15 20:42:20 +0000346 (_UPDATER_BIN, self.update_url))
David Haddock76a4c882017-12-13 18:50:09 -0800347 if not self.interactive:
348 autoupdate_cmd = '%s --interactive=false' % autoupdate_cmd
Shuqian Zhaod9992722016-02-29 12:26:38 -0800349 run_args = {'command': autoupdate_cmd, 'timeout': 3600}
350 err_prefix = ('Failed to install device image using payload at %s '
351 'on %s. ' % (self.update_url, self.host.hostname))
352 logging.info('Updating image via: %s', autoupdate_cmd)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700353 metric_fields = {'success': False}
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800354 try:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800355 self._base_update_handler(run_args, err_prefix)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700356 metric_fields['success'] = True
357 finally:
Allen Li1a5cc0a2017-06-20 14:08:59 -0700358 c = metrics.Counter('chromeos/autotest/autoupdater/update')
Allen Lib5420a72017-06-20 14:14:07 -0700359 metric_fields.update(self._get_metric_fields())
Allen Li1a5cc0a2017-06-20 14:08:59 -0700360 c.increment(fields=metric_fields)
Aviv Keshetf37b2d72016-06-01 19:27:59 -0700361 self._verify_update_completed()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700362
363
Richard Barnette3e8b2282018-05-15 20:42:20 +0000364 def _reset_update_engine(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700365 """Resets the host to prepare for a clean update regardless of state."""
Chris Sosae92399e2015-04-24 11:32:59 -0700366 self._run('stop ui || true')
367 self._run('stop update-engine || true')
368 self._run('start update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -0700369
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700370 # Wait for update engine to be ready.
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800371 self._wait_for_update_service()
Sean Oc053dfe2010-08-23 18:22:26 +0200372
373
Richard Barnette3e8b2282018-05-15 20:42:20 +0000374 def _rootdev(self, options=''):
Dan Shi0f466e82013-02-22 15:44:58 -0800375 """Returns the stripped output of rootdev <options>.
376
377 @param options: options to run rootdev.
378
379 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700380 return self._run('rootdev %s' % options).stdout.strip()
381
382
383 def get_kernel_state(self):
384 """Returns the (<active>, <inactive>) kernel state as a pair."""
Richard Barnette3e8b2282018-05-15 20:42:20 +0000385 active_root = int(re.findall('\d+\Z', self._rootdev('-s'))[0])
386 if active_root == _KERNEL_A['root']:
387 return _KERNEL_A, _KERNEL_B
388 elif active_root == _KERNEL_B['root']:
389 return _KERNEL_B, _KERNEL_A
Dale Curtisa94c19c2011-05-02 15:05:17 -0700390 else:
Dale Curtis5c32c722011-05-04 19:24:23 -0700391 raise ChromiumOSError('Encountered unknown root partition: %s' %
Dale Curtisa94c19c2011-05-02 15:05:17 -0700392 active_root)
393
394
395 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
396 """Return numeric cgpt value for the specified flag, kernel, device. """
397 return int(self._run('cgpt show -n -i %d %s %s' % (
398 kernel['kernel'], flag, dev)).stdout.strip())
399
400
Richard Barnetted3069432018-05-17 18:17:25 +0000401 def _get_next_kernel(self):
402 """Return the kernel that has priority for the next boot."""
403 priority_a = self._cgpt('-P', _KERNEL_A)
404 priority_b = self._cgpt('-P', _KERNEL_B)
405 if priority_a > priority_b:
406 return _KERNEL_A
407 else:
408 return _KERNEL_B
Dale Curtisa94c19c2011-05-02 15:05:17 -0700409
410
Richard Barnette3e8b2282018-05-15 20:42:20 +0000411 def _get_kernel_success(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800412 """Return boolean success flag for the specified kernel.
413
Richard Barnette3e8b2282018-05-15 20:42:20 +0000414 @param kernel: information of the given kernel, either _KERNEL_A
415 or _KERNEL_B.
Dan Shi0f466e82013-02-22 15:44:58 -0800416 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700417 return self._cgpt('-S', kernel) != 0
418
419
Richard Barnette3e8b2282018-05-15 20:42:20 +0000420 def _get_kernel_tries(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800421 """Return tries count for the specified kernel.
422
Richard Barnette3e8b2282018-05-15 20:42:20 +0000423 @param kernel: information of the given kernel, either _KERNEL_A
424 or _KERNEL_B.
Dan Shi0f466e82013-02-22 15:44:58 -0800425 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700426 return self._cgpt('-T', kernel)
Sean O267c00b2010-08-31 15:54:55 +0200427
428
Chris Sosa5e4246b2012-05-22 18:05:22 -0700429 def get_stateful_update_script(self):
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700430 """Returns the path to the stateful update script on the target.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700431
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700432 When runnning test_that, stateful_update is in chroot /usr/sbin,
433 as installed by chromeos-base/devserver packages.
434 In the lab, it is installed with the python module devserver, by
435 build_externals.py command.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700436
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700437 If we can find it, we hope it exists already on the DUT, we assert
438 otherwise.
439 """
Richard Barnette3e8b2282018-05-15 20:42:20 +0000440 stateful_update_file = os.path.join(_STATEFUL_UPDATE_PATH,
441 _STATEFUL_UPDATE_SCRIPT)
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700442 if os.path.exists(stateful_update_file):
Chris Sosa5e4246b2012-05-22 18:05:22 -0700443 self.host.send_file(
Richard Barnette3e8b2282018-05-15 20:42:20 +0000444 stateful_update_file, _REMOTE_TMP_STATEFUL_UPDATE,
Gilad Arnold0c0df732015-09-21 06:37:59 -0700445 delete_dest=True)
Richard Barnette3e8b2282018-05-15 20:42:20 +0000446 return _REMOTE_TMP_STATEFUL_UPDATE
Chris Sosa5e4246b2012-05-22 18:05:22 -0700447
Richard Barnette3e8b2282018-05-15 20:42:20 +0000448 if self.host.path_exists(_REMOTE_STATEFUL_UPDATE_PATH):
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700449 logging.warning('Could not chroot %s script, falling back on %s',
Richard Barnette3e8b2282018-05-15 20:42:20 +0000450 _STATEFUL_UPDATE_SCRIPT,
451 _REMOTE_STATEFUL_UPDATE_PATH)
452 return _REMOTE_STATEFUL_UPDATE_PATH
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700453 else:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000454 raise ChromiumOSError('Could not locate %s' %
455 _STATEFUL_UPDATE_SCRIPT)
Chris Sosa5e4246b2012-05-22 18:05:22 -0700456
457
Richard Barnette3e8b2282018-05-15 20:42:20 +0000458 def _reset_stateful_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800459 """Clear any pending stateful update request."""
Chris Sosa5e4246b2012-05-22 18:05:22 -0700460 statefuldev_cmd = [self.get_stateful_update_script()]
461 statefuldev_cmd += ['--stateful_change=reset', '2>&1']
Chris Sosa66d74072013-09-19 11:21:29 -0700462 self._run(' '.join(statefuldev_cmd))
Chris Sosa5e4246b2012-05-22 18:05:22 -0700463
464
Richard Barnette3e8b2282018-05-15 20:42:20 +0000465 def _revert_boot_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800466 """Revert the boot partition."""
Richard Barnette3e8b2282018-05-15 20:42:20 +0000467 part = self._rootdev('-s')
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700468 logging.warning('Reverting update; Boot partition will be %s', part)
Sean O267c00b2010-08-31 15:54:55 +0200469 return self._run('/postinst %s 2>&1' % part)
470
471
Chris Sosac1932172013-10-16 13:28:53 -0700472 def rollback_rootfs(self, powerwash):
473 """Triggers rollback and waits for it to complete.
474
475 @param powerwash: If true, powerwash as part of rollback.
476
477 @raise RootFSUpdateError if anything went wrong.
478
479 """
Dan Shi549fb822015-03-24 18:01:11 -0700480 version = self.host.get_release_version()
Chris Sosac8617522014-06-09 23:22:26 +0000481 # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches
482 # X.Y.Z. This version split just pulls the first part out.
483 try:
484 build_number = int(version.split('.')[0])
485 except ValueError:
486 logging.error('Could not parse build number.')
487 build_number = 0
488
489 if build_number >= 5772:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000490 can_rollback_cmd = '%s --can_rollback' % _UPDATER_BIN
Chris Sosac8617522014-06-09 23:22:26 +0000491 logging.info('Checking for rollback.')
492 try:
493 self._run(can_rollback_cmd)
494 except error.AutoservRunError as e:
495 raise RootFSUpdateError("Rollback isn't possible on %s: %s" %
496 (self.host.hostname, str(e)))
497
Richard Barnette3e8b2282018-05-15 20:42:20 +0000498 rollback_cmd = '%s --rollback --follow' % _UPDATER_BIN
Chris Sosac1932172013-10-16 13:28:53 -0700499 if not powerwash:
Dan Shif3a35f72016-01-25 11:18:14 -0800500 rollback_cmd += ' --nopowerwash'
Chris Sosac1932172013-10-16 13:28:53 -0700501
Chris Sosac8617522014-06-09 23:22:26 +0000502 logging.info('Performing rollback.')
Chris Sosac1932172013-10-16 13:28:53 -0700503 try:
504 self._run(rollback_cmd)
Chris Sosac1932172013-10-16 13:28:53 -0700505 except error.AutoservRunError as e:
506 raise RootFSUpdateError('Rollback failed on %s: %s' %
507 (self.host.hostname, str(e)))
508
509 self._verify_update_completed()
510
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800511
Chris Sosa72312602013-04-16 15:01:56 -0700512 def update_stateful(self, clobber=True):
513 """Updates the stateful partition.
514
515 @param clobber: If True, a clean stateful installation.
516 """
Chris Sosa77556d82012-04-05 15:23:14 -0700517 logging.info('Updating stateful partition...')
joychen03eaad92013-06-26 09:55:21 -0700518 statefuldev_url = self.update_url.replace('update',
519 'static')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700520
Dale Curtis5c32c722011-05-04 19:24:23 -0700521 # Attempt stateful partition update; this must succeed so that the newly
522 # installed host is testable after update.
Chris Sosa72312602013-04-16 15:01:56 -0700523 statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url]
524 if clobber:
525 statefuldev_cmd.append('--stateful_change=clean')
526
527 statefuldev_cmd.append('2>&1')
Dale Curtis5c32c722011-05-04 19:24:23 -0700528 try:
Dan Shi205b8732016-01-25 10:56:22 -0800529 self._run(' '.join(statefuldev_cmd), timeout=1200)
Dale Curtis5c32c722011-05-04 19:24:23 -0700530 except error.AutoservRunError:
Gilad Arnold62cf3a42015-10-01 09:15:25 -0700531 update_error = StatefulUpdateError(
532 'Failed to perform stateful update on %s' %
533 self.host.hostname)
Chris Sosa77556d82012-04-05 15:23:14 -0700534 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700535
Chris Sosaa3ac2152012-05-23 22:23:13 -0700536
Richard Barnette0beb14b2018-05-15 18:07:52 +0000537 def _install_update(self, update_root=True):
538 """Install the requested image on the DUT, but don't start it.
539
540 This downloads all content needed for the requested update, and
541 installs it in place on the DUT. This does not reboot the DUT,
542 so the update is merely pending when the function returns.
543
544 @param update_root: When true, force a rootfs update; otherwise
545 update the stateful partition only.
Dan Shi0f466e82013-02-22 15:44:58 -0800546 """
Dan Shi549fb822015-03-24 18:01:11 -0700547 booted_version = self.host.get_release_version()
Richard Barnette0173ea82018-05-04 21:13:57 +0000548 logging.info('Updating from version %s to %s.',
549 booted_version, self.update_version)
Dale Curtis53d55862011-05-16 12:17:59 -0700550
Dale Curtis5c32c722011-05-04 19:24:23 -0700551 # Check that Dev Server is accepting connections (from autoserv's host).
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200552 # If we can't talk to it, the machine host probably can't either.
xixuanccf2e722016-06-10 16:42:38 -0700553 auserver_host = 'http://%s' % urlparse.urlparse(self.update_url)[1]
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200554 try:
xixuanccf2e722016-06-10 16:42:38 -0700555 if not dev_server.ImageServer.devserver_healthy(auserver_host):
556 raise ChromiumOSError(
557 'Update server at %s not healthy' % auserver_host)
558 except Exception as e:
559 logging.debug('Error happens in connection to devserver: %r', e)
Dale Curtis5c32c722011-05-04 19:24:23 -0700560 raise ChromiumOSError(
561 'Update server at %s not available' % auserver_host)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200562
Chris Sosaa3ac2152012-05-23 22:23:13 -0700563 logging.info('Installing from %s to %s', self.update_url,
Chris Sosa77556d82012-04-05 15:23:14 -0700564 self.host.hostname)
565
Chris Sosa5e4246b2012-05-22 18:05:22 -0700566 # Reset update state.
Richard Barnette3e8b2282018-05-15 20:42:20 +0000567 self._reset_update_engine()
568 self._reset_stateful_partition()
Sean Oc053dfe2010-08-23 18:22:26 +0200569
Dale Curtis1e973182011-07-12 18:21:36 -0700570 try:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700571 try:
572 if not update_root:
573 logging.info('Root update is skipped.')
574 else:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000575 self.update_image()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200576
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700577 self.update_stateful()
578 except:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000579 self._revert_boot_partition()
580 self._reset_stateful_partition()
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700581 raise
Sean Oc053dfe2010-08-23 18:22:26 +0200582
Dale Curtis1e973182011-07-12 18:21:36 -0700583 logging.info('Update complete.')
Dale Curtis1e973182011-07-12 18:21:36 -0700584 except:
585 # Collect update engine logs in the event of failure.
586 if self.host.job:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700587 logging.info('Collecting update engine logs due to failure...')
Dale Curtis1e973182011-07-12 18:21:36 -0700588 self.host.get_file(
Richard Barnette3e8b2282018-05-15 20:42:20 +0000589 _UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
Gilad Arnold0c0df732015-09-21 06:37:59 -0700590 preserve_perm=False)
Richard Barnette3e8b2282018-05-15 20:42:20 +0000591 _list_image_dir_contents(self.update_url)
Dale Curtis1e973182011-07-12 18:21:36 -0700592 raise
Dan Shi10e992b2013-08-30 11:02:59 -0700593 finally:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800594 logging.info('Update engine log has downloaded in '
595 'sysinfo/update_engine dir. Check the lastest.')
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200596
597
Richard Barnette0beb14b2018-05-15 18:07:52 +0000598 def _try_stateful_update(self):
599 """Try to use stateful update to initialize DUT.
600
601 When DUT is already running the same version that machine_install
602 tries to install, stateful update is a much faster way to clean up
603 the DUT for testing, compared to a full reimage. It is implemeted
604 by calling autoupdater._run_full_update, but skipping updating root,
605 as updating the kernel is time consuming and not necessary.
606
607 @param update_url: url of the image.
608 @param updater: ChromiumOSUpdater instance used to update the DUT.
609 @returns: True if the DUT was updated with stateful update.
610
611 """
612 self.host.prepare_for_update()
613
614 # TODO(jrbarnette): Yes, I hate this re.match() test case.
615 # It's better than the alternative: see crbug.com/360944.
616 image_name = url_to_image_name(self.update_url)
617 release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$'
618 if not re.match(release_pattern, image_name):
619 return False
Richard Barnette3e8b2282018-05-15 20:42:20 +0000620 if not self._check_version():
Richard Barnette0beb14b2018-05-15 18:07:52 +0000621 return False
622 # Following folders should be rebuilt after stateful update.
623 # A test file is used to confirm each folder gets rebuilt after
624 # the stateful update.
625 folders_to_check = ['/var', '/home', '/mnt/stateful_partition']
626 test_file = '.test_file_to_be_deleted'
627 paths = [os.path.join(folder, test_file) for folder in folders_to_check]
628 self._run('touch %s' % ' '.join(paths))
629
630 self._install_update(update_root=False)
631
632 # Reboot to complete stateful update.
633 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
634
635 # After stateful update and a reboot, all of the test_files shouldn't
636 # exist any more. Otherwise the stateful update is failed.
637 return not any(
638 self.host.path_exists(os.path.join(folder, test_file))
639 for folder in folders_to_check)
640
641
642 def _post_update_processing(self, expected_kernel):
643 """After the DUT is updated, confirm machine_install succeeded.
644
645 @param updater: ChromiumOSUpdater instance used to update the DUT.
646 @param expected_kernel: kernel expected to be active after reboot,
647 or `None` to skip rollback checking.
648
649 """
650 # Touch the lab machine file to leave a marker that
651 # distinguishes this image from other test images.
652 # Afterwards, we must re-run the autoreboot script because
653 # it depends on the _LAB_MACHINE_FILE.
654 autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
655 '( touch "$FILE" ; start autoreboot )')
Richard Barnette3e8b2282018-05-15 20:42:20 +0000656 self._run(autoreboot_cmd % _LAB_MACHINE_FILE)
Richard Barnette0beb14b2018-05-15 18:07:52 +0000657 self.verify_boot_expectations(
658 expected_kernel, rollback_message=
659 'Build %s failed to boot on %s; system rolled back to previous '
660 'build' % (self.update_version, self.host.hostname))
661
662 logging.debug('Cleaning up old autotest directories.')
663 try:
664 installed_autodir = autotest.Autotest.get_installed_autodir(
665 self.host)
666 self._run('rm -rf ' + installed_autodir)
667 except autotest.AutodirNotFoundError:
668 logging.debug('No autotest installed directory found.')
669
670
671 def run_update(self, force_full_update):
672 """Perform a full update of a DUT in the test lab.
673
674 This downloads and installs the root FS and stateful partition
675 content needed for the update specified in `self.host` and
676 `self.update_url`. The update is performed according to the
677 requirements for provisioning a DUT for testing the requested
678 build.
679
680 @param force_full_update: When true, update the root file
681 system to the new build, even if the target DUT already has
682 that build installed.
683 @returns A tuple of the form `(image_name, attributes)`, where
684 `image_name` is the name of the image installed, and
685 `attributes` is new attributes to be applied to the DUT.
686 """
687 logging.debug('Update URL is %s', self.update_url)
688
689 # Report provision stats.
690 server_name = dev_server.get_hostname(self.update_url)
691 (metrics.Counter('chromeos/autotest/provision/install')
692 .increment(fields={'devserver': server_name}))
693
694 # Create a file to indicate if provision fails. The file will be
695 # removed by any successful update.
696 self._run('touch %s' % PROVISION_FAILED)
697
698 update_complete = False
699 if not force_full_update:
700 try:
701 # If the DUT is already running the same build, try stateful
702 # update first as it's much quicker than a full re-image.
703 update_complete = self._try_stateful_update()
704 except Exception as e:
705 logging.exception(e)
706
707 inactive_kernel = None
708 if update_complete:
709 logging.info('Install complete without full update')
710 else:
711 logging.info('DUT requires full update.')
712 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
713 self.host.prepare_for_update()
714
715 self._install_update()
716
717 # Give it some time in case of IO issues.
718 time.sleep(10)
719
Richard Barnetted3069432018-05-17 18:17:25 +0000720 inactive_kernel = self.get_kernel_state()[1]
721 next_kernel = self._get_next_kernel()
722 if next_kernel != inactive_kernel:
Richard Barnette0beb14b2018-05-15 18:07:52 +0000723 raise ChromiumOSError(
Richard Barnetted3069432018-05-17 18:17:25 +0000724 'Update failed. The kernel for next boot is %s, '
725 'but %s was expected.' %
726 (next_kernel['name'], inactive_kernel['name']))
Richard Barnette0beb14b2018-05-15 18:07:52 +0000727
728 # Update has returned successfully; reboot the host.
729 #
730 # Regarding the 'crossystem' command below: In some cases,
731 # the update flow puts the TPM into a state such that it
732 # fails verification. We don't know why. However, this
733 # call papers over the problem by clearing the TPM during
734 # the reboot.
735 #
736 # We ignore failures from 'crossystem'. Although failure
737 # here is unexpected, and could signal a bug, the point of
738 # the exercise is to paper over problems; allowing this to
739 # fail would defeat the purpose.
740 self._run('crossystem clear_tpm_owner_request=1',
741 ignore_status=True)
742 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT, wait=True)
743
744 self._post_update_processing(inactive_kernel)
745 image_name = url_to_image_name(self.update_url)
746 # update_url is different from devserver url needed to stage autotest
747 # packages, therefore, resolve a new devserver url here.
748 devserver_url = dev_server.ImageServer.resolve(
749 image_name, self.host.hostname).url()
750 repo_url = tools.get_package_url(devserver_url, image_name)
751 return image_name, {ds_constants.JOB_REPO_URL: repo_url}
752
753
Richard Barnette3e8b2282018-05-15 20:42:20 +0000754 def _check_version(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800755 """Check the image running in DUT has the desired version.
756
757 @returns: True if the DUT's image version matches the version that
758 the autoupdater tries to update to.
759
760 """
Dan Shi549fb822015-03-24 18:01:11 -0700761 booted_version = self.host.get_release_version()
Richard Barnette0173ea82018-05-04 21:13:57 +0000762 return self.update_version.endswith(booted_version)
Dan Shib95bb862013-03-22 16:29:28 -0700763
764
Chris Sosa65425082013-10-16 13:26:22 -0700765 def verify_boot_expectations(self, expected_kernel_state, rollback_message):
766 """Verifies that we fully booted given expected kernel state.
767
768 This method both verifies that we booted using the correct kernel
769 state and that the OS has marked the kernel as good.
770
771 @param expected_kernel_state: kernel state that we are verifying with
772 i.e. I expect to be booted onto partition 4 etc. See output of
773 get_kernel_state.
774 @param rollback_message: string to raise as a ChromiumOSError
775 if we booted with the wrong partition.
776
777 @raises ChromiumOSError: If we didn't.
778 """
779 # Figure out the newly active kernel.
780 active_kernel_state = self.get_kernel_state()[0]
781
782 # Check for rollback due to a bad build.
783 if (expected_kernel_state and
784 active_kernel_state != expected_kernel_state):
Don Garrett56b1cc82013-12-06 17:49:20 -0800785
786 # Kernel crash reports should be wiped between test runs, but
787 # may persist from earlier parts of the test, or from problems
788 # with provisioning.
789 #
790 # Kernel crash reports will NOT be present if the crash happened
791 # before encrypted stateful is mounted.
792 #
793 # TODO(dgarrett): Integrate with server/crashcollect.py at some
794 # point.
795 kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash')
796 if kernel_crashes:
797 rollback_message += ': kernel_crash'
798 logging.debug('Found %d kernel crash reports:',
799 len(kernel_crashes))
800 # The crash names contain timestamps that may be useful:
801 # kernel.20131207.005945.0.kcrash
802 for crash in kernel_crashes:
Dan Shi0942b1d2015-03-31 11:07:00 -0700803 logging.debug(' %s', os.path.basename(crash))
Don Garrett56b1cc82013-12-06 17:49:20 -0800804
Chris Sosa65425082013-10-16 13:26:22 -0700805 # Print out some information to make it easier to debug
806 # the rollback.
807 logging.debug('Dumping partition table.')
808 self._run('cgpt show $(rootdev -s -d)')
809 logging.debug('Dumping crossystem for firmware debugging.')
810 self._run('crossystem --all')
811 raise ChromiumOSError(rollback_message)
812
813 # Make sure chromeos-setgoodkernel runs.
814 try:
815 utils.poll_for_condition(
Richard Barnette3e8b2282018-05-15 20:42:20 +0000816 lambda: (self._get_kernel_tries(active_kernel_state) == 0
817 and self._get_kernel_success(active_kernel_state)),
Chris Sosa65425082013-10-16 13:26:22 -0700818 exception=ChromiumOSError(),
Richard Barnette3e8b2282018-05-15 20:42:20 +0000819 timeout=_KERNEL_UPDATE_TIMEOUT, sleep_interval=5)
Chris Sosa65425082013-10-16 13:26:22 -0700820 except ChromiumOSError:
821 services_status = self._run('status system-services').stdout
822 if services_status != 'system-services start/running\n':
823 event = ('Chrome failed to reach login screen')
824 else:
825 event = ('update-engine failed to call '
826 'chromeos-setgoodkernel')
827 raise ChromiumOSError(
828 'After update and reboot, %s '
Richard Barnette3e8b2282018-05-15 20:42:20 +0000829 'within %d seconds' % (event, _KERNEL_UPDATE_TIMEOUT))