Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
Don Garrett | 56b1cc8 | 2013-12-06 17:49:20 -0800 | [diff] [blame] | 5 | import glob |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 6 | import logging |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 7 | import os |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 8 | import re |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 9 | import time |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 10 | import urllib2 |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 11 | import urlparse |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 12 | |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 13 | from autotest_lib.client.bin import utils |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 14 | from autotest_lib.client.common_lib import error, global_config |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 15 | from autotest_lib.client.common_lib.cros import dev_server |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 16 | from autotest_lib.server import autotest |
Shelley Chen | 61d2898 | 2016-10-28 09:40:20 -0700 | [diff] [blame] | 17 | from autotest_lib.server import utils as server_utils |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 18 | from autotest_lib.server.cros.dynamic_suite import constants as ds_constants |
| 19 | from autotest_lib.server.cros.dynamic_suite import tools |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 20 | from chromite.lib import retry_util |
Dan Shi | f3a35f7 | 2016-01-25 11:18:14 -0800 | [diff] [blame] | 21 | |
Shelley Chen | 16b8df3 | 2016-10-27 16:24:21 -0700 | [diff] [blame] | 22 | try: |
| 23 | from chromite.lib import metrics |
Dan Shi | 5e2efb7 | 2017-02-07 11:40:23 -0800 | [diff] [blame] | 24 | except ImportError: |
| 25 | metrics = utils.metrics_mock |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 26 | |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 27 | try: |
| 28 | import devserver |
| 29 | STATEFUL_UPDATE_PATH = devserver.__path__[0] |
| 30 | except ImportError: |
| 31 | STATEFUL_UPDATE_PATH = '/usr/bin' |
| 32 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 33 | # Local stateful update path is relative to the CrOS source directory. |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 34 | STATEFUL_UPDATE_SCRIPT = 'stateful_update' |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 35 | UPDATER_IDLE = 'UPDATE_STATUS_IDLE' |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 36 | UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' |
beeps | 5e8c45a | 2013-12-17 22:05:11 -0800 | [diff] [blame] | 37 | # A list of update engine client states that occur after an update is triggered. |
| 38 | UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE', |
| 39 | 'UPDATE_STATUS_UPDATE_AVAILABLE', |
| 40 | 'UPDATE_STATUS_DOWNLOADING', |
| 41 | 'UPDATE_STATUS_FINALIZING'] |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 42 | |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 43 | |
| 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. |
| 50 | PROVISION_FAILED = '/var/tmp/provision_failed' |
| 51 | |
| 52 | |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 53 | class ChromiumOSError(error.InstallError): |
| 54 | """Generic error for ChromiumOS-specific exceptions.""" |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 55 | |
| 56 | |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 57 | class RootFSUpdateError(ChromiumOSError): |
| 58 | """Raised when the RootFS fails to update.""" |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 59 | |
| 60 | |
| 61 | class StatefulUpdateError(ChromiumOSError): |
| 62 | """Raised when the stateful partition fails to update.""" |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 63 | |
| 64 | |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 65 | def url_to_version(update_url): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 66 | """Return the version based on update_url. |
| 67 | |
| 68 | @param update_url: url to the image to update to. |
| 69 | |
| 70 | """ |
Dale Curtis | ddfdb94 | 2011-07-14 13:59:24 -0700 | [diff] [blame] | 71 | # 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 Shi | 5002cfc | 2013-04-29 10:45:05 -0700 | [diff] [blame] | 75 | return re.sub('/au/.*', '', |
| 76 | urlparse.urlparse(update_url).path).split('/')[-1].strip() |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 77 | |
| 78 | |
Scott Zawalski | eadbf70 | 2013-03-14 09:23:06 -0400 | [diff] [blame] | 79 | def 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 B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 93 | def _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 | |
| 118 | def 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 Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 142 | # 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. |
| 146 | class BaseUpdater(object): |
| 147 | """Platform-agnostic DUT update functionality.""" |
| 148 | |
David Haddock | 76a4c88 | 2017-12-13 18:50:09 -0800 | [diff] [blame] | 149 | def __init__(self, updater_ctrl_bin, update_url, host, interactive=True): |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 150 | """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 Haddock | 76a4c88 | 2017-12-13 18:50:09 -0800 | [diff] [blame] | 155 | @param interactive: Bool whether we are doing an interactive update. |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 156 | """ |
| 157 | self.updater_ctrl_bin = updater_ctrl_bin |
| 158 | self.update_url = update_url |
| 159 | self.host = host |
David Haddock | 76a4c88 | 2017-12-13 18:50:09 -0800 | [diff] [blame] | 160 | self.interactive = interactive |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 161 | |
| 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 Semenzato | f15c8fc | 2017-03-03 14:12:40 -0800 | [diff] [blame] | 169 | update_status = self.host.run(command='%s -status | grep CURRENT_OP' % |
| 170 | self.updater_ctrl_bin) |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 171 | return update_status.stdout.strip().split('=')[-1] |
| 172 | |
| 173 | |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 174 | 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 Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 182 | def _base_update_handler_no_retry(self, run_args): |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 183 | """Base function to handle a remote update ssh call. |
| 184 | |
| 185 | @param run_args: Dictionary of args passed to ssh_host.run function. |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 186 | |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 187 | @throws: intercepts and re-throws all exceptions |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 188 | """ |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 189 | try: |
| 190 | self.host.run(**run_args) |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 191 | except Exception as e: |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 192 | logging.debug('exception in update handler: %s', e) |
| 193 | raise e |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 194 | |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 195 | |
| 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 Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 223 | |
| 224 | |
Luigi Semenzato | f15c8fc | 2017-03-03 14:12:40 -0800 | [diff] [blame] | 225 | 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 Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 256 | def trigger_update(self): |
| 257 | """Triggers a background update. |
| 258 | |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 259 | @raise RootFSUpdateError or unknown Exception if anything went wrong. |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 260 | """ |
Luigi Semenzato | f15c8fc | 2017-03-03 14:12:40 -0800 | [diff] [blame] | 261 | # 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 Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 266 | autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' % |
| 267 | (self.updater_ctrl_bin, self.update_url)) |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 268 | run_args = {'command': autoupdate_cmd} |
| 269 | err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 270 | logging.info('Triggering update via: %s', autoupdate_cmd) |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 271 | metric_fields = {'success': False} |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 272 | try: |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 273 | self._base_update_handler(run_args, err_prefix) |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 274 | metric_fields['success'] = True |
| 275 | finally: |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 276 | c = metrics.Counter('chromeos/autotest/autoupdater/trigger') |
Allen Li | b5420a7 | 2017-06-20 14:14:07 -0700 | [diff] [blame] | 277 | metric_fields.update(self._get_metric_fields()) |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 278 | c.increment(fields=metric_fields) |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 279 | |
| 280 | |
Allen Li | b5420a7 | 2017-06-20 14:14:07 -0700 | [diff] [blame] | 281 | 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 Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 302 | 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 Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 309 | error_msg = '' |
| 310 | if status == UPDATER_IDLE: |
| 311 | error_msg = 'Update error: %s' % self.get_last_update_error() |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 312 | raise RootFSUpdateError('Update did not complete with correct ' |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 313 | 'status. Expecting %s, actual %s. %s' % |
| 314 | (UPDATER_NEED_REBOOT, status, error_msg)) |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 315 | |
| 316 | |
| 317 | def update_image(self): |
| 318 | """Updates the device image and verifies success.""" |
Shuqian Zhao | fe4d62e | 2016-06-23 14:46:45 -0700 | [diff] [blame] | 319 | autoupdate_cmd = ('%s --update --omaha_url=%s' % |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 320 | (self.updater_ctrl_bin, self.update_url)) |
David Haddock | 76a4c88 | 2017-12-13 18:50:09 -0800 | [diff] [blame] | 321 | if not self.interactive: |
| 322 | autoupdate_cmd = '%s --interactive=false' % autoupdate_cmd |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 323 | 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 Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 327 | metric_fields = {'success': False} |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 328 | try: |
Luigi Semenzato | e76d9f8 | 2016-11-21 11:15:10 -0800 | [diff] [blame] | 329 | self._base_update_handler(run_args, err_prefix) |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 330 | metric_fields['success'] = True |
| 331 | finally: |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 332 | c = metrics.Counter('chromeos/autotest/autoupdater/update') |
Allen Li | b5420a7 | 2017-06-20 14:14:07 -0700 | [diff] [blame] | 333 | metric_fields.update(self._get_metric_fields()) |
Allen Li | 1a5cc0a | 2017-06-20 14:08:59 -0700 | [diff] [blame] | 334 | c.increment(fields=metric_fields) |
Dan Shi | 5e2efb7 | 2017-02-07 11:40:23 -0800 | [diff] [blame] | 335 | |
Aviv Keshet | f37b2d7 | 2016-06-01 19:27:59 -0700 | [diff] [blame] | 336 | self._verify_update_completed() |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 337 | |
| 338 | |
| 339 | class ChromiumOSUpdater(BaseUpdater): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 340 | """Helper class used to update DUT with image of desired version.""" |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 341 | 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 Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 345 | UPDATER_BIN = '/usr/bin/update_engine_client' |
Mike Frysinger | 898bd55 | 2017-04-10 23:56:36 -0400 | [diff] [blame] | 346 | UPDATED_MARKER = '/run/update_engine_autoupdate_completed' |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 347 | UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine'] |
| 348 | |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 349 | KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3} |
| 350 | KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5} |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 351 | # Time to wait for new kernel to be marked successful after |
| 352 | # auto update. |
| 353 | KERNEL_UPDATE_TIMEOUT = 120 |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 354 | |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 355 | # 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 Barnette | 0173ea8 | 2018-05-04 21:13:57 +0000 | [diff] [blame] | 361 | def __init__(self, update_url, host=None, interactive=True): |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 362 | super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url, |
David Haddock | 76a4c88 | 2017-12-13 18:50:09 -0800 | [diff] [blame] | 363 | host, interactive=interactive) |
Richard Barnette | 0173ea8 | 2018-05-04 21:13:57 +0000 | [diff] [blame] | 364 | self.update_version = url_to_version(update_url) |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 365 | |
Gilad Arnold | 5f2ff44 | 2015-09-21 07:06:40 -0700 | [diff] [blame] | 366 | |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 367 | def reset_update_engine(self): |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 368 | """Resets the host to prepare for a clean update regardless of state.""" |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 369 | self._run('rm -f %s' % self.UPDATED_MARKER) |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 370 | self._run('stop ui || true') |
| 371 | self._run('stop update-engine || true') |
| 372 | self._run('start update-engine') |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 373 | |
Shuqian Zhao | fe4d62e | 2016-06-23 14:46:45 -0700 | [diff] [blame] | 374 | # Wait for update engine to be ready. |
Luigi Semenzato | f15c8fc | 2017-03-03 14:12:40 -0800 | [diff] [blame] | 375 | self._wait_for_update_service() |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 376 | |
| 377 | |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 378 | def _run(self, cmd, *args, **kwargs): |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 379 | """Abbreviated form of self.host.run(...)""" |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 380 | return self.host.run(cmd, *args, **kwargs) |
| 381 | |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 382 | |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 383 | def rootdev(self, options=''): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 384 | """Returns the stripped output of rootdev <options>. |
| 385 | |
| 386 | @param options: options to run rootdev. |
| 387 | |
| 388 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 389 | 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 Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 400 | raise ChromiumOSError('Encountered unknown root partition: %s' % |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 401 | 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 Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 411 | """Return numeric priority for the specified kernel. |
| 412 | |
| 413 | @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. |
| 414 | |
| 415 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 416 | return self._cgpt('-P', kernel) |
| 417 | |
| 418 | |
| 419 | def get_kernel_success(self, kernel): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 420 | """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 Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 425 | return self._cgpt('-S', kernel) != 0 |
| 426 | |
| 427 | |
| 428 | def get_kernel_tries(self, kernel): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 429 | """Return tries count for the specified kernel. |
| 430 | |
| 431 | @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. |
| 432 | |
| 433 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 434 | return self._cgpt('-T', kernel) |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 435 | |
| 436 | |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 437 | def get_stateful_update_script(self): |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 438 | """Returns the path to the stateful update script on the target. |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 439 | |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 440 | 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 Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 444 | |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 445 | 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 Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 451 | self.host.send_file( |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 452 | stateful_update_file, self.REMOTE_TMP_STATEFUL_UPDATE, |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 453 | delete_dest=True) |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 454 | return self.REMOTE_TMP_STATEFUL_UPDATE |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 455 | |
Gwendal Grignou | 3e96cc2 | 2017-06-07 16:22:51 -0700 | [diff] [blame] | 456 | 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 Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 463 | |
| 464 | |
| 465 | def reset_stateful_partition(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 466 | """Clear any pending stateful update request.""" |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 467 | statefuldev_cmd = [self.get_stateful_update_script()] |
| 468 | statefuldev_cmd += ['--stateful_change=reset', '2>&1'] |
Chris Sosa | 66d7407 | 2013-09-19 11:21:29 -0700 | [diff] [blame] | 469 | self._run(' '.join(statefuldev_cmd)) |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 470 | |
| 471 | |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 472 | def revert_boot_partition(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 473 | """Revert the boot partition.""" |
Dale Curtis | d9b26b9 | 2011-10-24 13:34:46 -0700 | [diff] [blame] | 474 | part = self.rootdev('-s') |
Ilja H. Friedel | 04be2bd | 2014-05-07 21:29:59 -0700 | [diff] [blame] | 475 | logging.warning('Reverting update; Boot partition will be %s', part) |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 476 | return self._run('/postinst %s 2>&1' % part) |
| 477 | |
| 478 | |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 479 | 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 Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 487 | version = self.host.get_release_version() |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 488 | # 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 Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 497 | can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 498 | 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 Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 505 | rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 506 | if not powerwash: |
Dan Shi | f3a35f7 | 2016-01-25 11:18:14 -0800 | [diff] [blame] | 507 | rollback_cmd += ' --nopowerwash' |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 508 | |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 509 | logging.info('Performing rollback.') |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 510 | try: |
| 511 | self._run(rollback_cmd) |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 512 | 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 Arnold | 0ed760c | 2012-11-05 23:42:53 -0800 | [diff] [blame] | 518 | |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 519 | # TODO(garnold) This is here for backward compatibility and should be |
| 520 | # deprecated once we shift to using update_image() everywhere. |
Chris Sosa | 2f1ae9f | 2013-08-13 10:00:15 -0700 | [diff] [blame] | 521 | def update_rootfs(self): |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 522 | """Run the standard command to force an update.""" |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 523 | return self.update_image() |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 524 | |
| 525 | |
Chris Sosa | 7231260 | 2013-04-16 15:01:56 -0700 | [diff] [blame] | 526 | def update_stateful(self, clobber=True): |
| 527 | """Updates the stateful partition. |
| 528 | |
| 529 | @param clobber: If True, a clean stateful installation. |
| 530 | """ |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 531 | logging.info('Updating stateful partition...') |
joychen | 03eaad9 | 2013-06-26 09:55:21 -0700 | [diff] [blame] | 532 | statefuldev_url = self.update_url.replace('update', |
| 533 | 'static') |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 534 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 535 | # Attempt stateful partition update; this must succeed so that the newly |
| 536 | # installed host is testable after update. |
Chris Sosa | 7231260 | 2013-04-16 15:01:56 -0700 | [diff] [blame] | 537 | 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 Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 542 | try: |
Dan Shi | 205b873 | 2016-01-25 10:56:22 -0800 | [diff] [blame] | 543 | self._run(' '.join(statefuldev_cmd), timeout=1200) |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 544 | except error.AutoservRunError: |
Gilad Arnold | 62cf3a4 | 2015-10-01 09:15:25 -0700 | [diff] [blame] | 545 | update_error = StatefulUpdateError( |
| 546 | 'Failed to perform stateful update on %s' % |
| 547 | self.host.hostname) |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 548 | raise update_error |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 549 | |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 550 | |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 551 | 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 Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 560 | """ |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 561 | booted_version = self.host.get_release_version() |
Richard Barnette | 0173ea8 | 2018-05-04 21:13:57 +0000 | [diff] [blame] | 562 | logging.info('Updating from version %s to %s.', |
| 563 | booted_version, self.update_version) |
Dale Curtis | 53d5586 | 2011-05-16 12:17:59 -0700 | [diff] [blame] | 564 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 565 | # Check that Dev Server is accepting connections (from autoserv's host). |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 566 | # If we can't talk to it, the machine host probably can't either. |
xixuan | ccf2e72 | 2016-06-10 16:42:38 -0700 | [diff] [blame] | 567 | auserver_host = 'http://%s' % urlparse.urlparse(self.update_url)[1] |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 568 | try: |
xixuan | ccf2e72 | 2016-06-10 16:42:38 -0700 | [diff] [blame] | 569 | 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 Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 574 | raise ChromiumOSError( |
| 575 | 'Update server at %s not available' % auserver_host) |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 576 | |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 577 | logging.info('Installing from %s to %s', self.update_url, |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 578 | self.host.hostname) |
| 579 | |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 580 | # Reset update state. |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 581 | self.reset_update_engine() |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 582 | self.reset_stateful_partition() |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 583 | |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 584 | try: |
Aviv Keshet | 2610d3e | 2016-06-01 16:37:01 -0700 | [diff] [blame] | 585 | try: |
| 586 | if not update_root: |
| 587 | logging.info('Root update is skipped.') |
| 588 | else: |
| 589 | self.update_rootfs() |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 590 | |
Aviv Keshet | 2610d3e | 2016-06-01 16:37:01 -0700 | [diff] [blame] | 591 | self.update_stateful() |
| 592 | except: |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 593 | self.revert_boot_partition() |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 594 | self.reset_stateful_partition() |
Aviv Keshet | 2610d3e | 2016-06-01 16:37:01 -0700 | [diff] [blame] | 595 | raise |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 596 | |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 597 | logging.info('Update complete.') |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 598 | except: |
| 599 | # Collect update engine logs in the event of failure. |
| 600 | if self.host.job: |
Aviv Keshet | 2610d3e | 2016-06-01 16:37:01 -0700 | [diff] [blame] | 601 | logging.info('Collecting update engine logs due to failure...') |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 602 | self.host.get_file( |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 603 | self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir, |
| 604 | preserve_perm=False) |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 605 | list_image_dir_contents(self.update_url) |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 606 | raise |
Dan Shi | 10e992b | 2013-08-30 11:02:59 -0700 | [diff] [blame] | 607 | finally: |
Shuqian Zhao | d999272 | 2016-02-29 12:26:38 -0800 | [diff] [blame] | 608 | logging.info('Update engine log has downloaded in ' |
| 609 | 'sysinfo/update_engine dir. Check the lastest.') |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 610 | |
| 611 | |
Richard Barnette | 0beb14b | 2018-05-15 18:07:52 +0000 | [diff] [blame^] | 612 | 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 Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 771 | def check_version(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 772 | """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 Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 778 | booted_version = self.host.get_release_version() |
Richard Barnette | 0173ea8 | 2018-05-04 21:13:57 +0000 | [diff] [blame] | 779 | return self.update_version.endswith(booted_version) |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 780 | |
| 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 Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 788 | The method is designed to verify version for following 6 scenarios with |
Dan Shi | 190c780 | 2013-04-04 13:05:30 -0700 | [diff] [blame] | 789 | 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 Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 805 | |
Dan Shi | 7f79551 | 2013-04-12 10:08:17 -0700 | [diff] [blame] | 806 | 5. chrome-perf build. |
| 807 | update version: lumpy-chrome-perf/R28-3837.0.0-b2996 |
| 808 | booted version: 3837.0.0 |
| 809 | |
Dan Shi | 73aa290 | 2013-05-03 11:22:11 -0700 | [diff] [blame] | 810 | 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 Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 814 | 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 Shi | 190c780 | 2013-04-04 13:05:30 -0700 | [diff] [blame] | 816 | it may return false positive for a DUT running trybot paladin build to |
| 817 | be updated to another trybot paladin build. |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 818 | |
Dan Shi | 190c780 | 2013-04-04 13:05:30 -0700 | [diff] [blame] | 819 | 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 Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 824 | |
| 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 Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 835 | return utils.version_match(self.update_version, |
| 836 | self.host.get_release_version(), |
| 837 | self.update_url) |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 838 | |
| 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 Garrett | 56b1cc8 | 2013-12-06 17:49:20 -0800 | [diff] [blame] | 860 | |
| 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 Shi | 0942b1d | 2015-03-31 11:07:00 -0700 | [diff] [blame] | 878 | logging.debug(' %s', os.path.basename(crash)) |
Don Garrett | 56b1cc8 | 2013-12-06 17:49:20 -0800 | [diff] [blame] | 879 | |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 880 | # 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)) |