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 httplib |
| 7 | import logging |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 8 | import multiprocessing |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 9 | import os |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 10 | import re |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 11 | import urlparse |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 12 | import urllib2 |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 13 | |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 14 | from autotest_lib.client.bin import utils |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 15 | from autotest_lib.client.common_lib import error, global_config |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 16 | from autotest_lib.client.common_lib.cros import dev_server |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 17 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 18 | # Local stateful update path is relative to the CrOS source directory. |
| 19 | LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update' |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 20 | LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update' |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 21 | UPDATER_IDLE = 'UPDATE_STATUS_IDLE' |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 22 | UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' |
beeps | 5e8c45a | 2013-12-17 22:05:11 -0800 | [diff] [blame] | 23 | # A list of update engine client states that occur after an update is triggered. |
| 24 | UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE', |
| 25 | 'UPDATE_STATUS_UPDATE_AVAILABLE', |
| 26 | 'UPDATE_STATUS_DOWNLOADING', |
| 27 | 'UPDATE_STATUS_FINALIZING'] |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 28 | |
| 29 | class ChromiumOSError(error.InstallError): |
| 30 | """Generic error for ChromiumOS-specific exceptions.""" |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 31 | |
| 32 | |
| 33 | class BrilloError(error.InstallError): |
| 34 | """Generic error for Brillo-specific exceptions.""" |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 35 | |
| 36 | |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 37 | class RootFSUpdateError(ChromiumOSError): |
| 38 | """Raised when the RootFS fails to update.""" |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 39 | |
| 40 | |
| 41 | class StatefulUpdateError(ChromiumOSError): |
| 42 | """Raised when the stateful partition fails to update.""" |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 43 | |
| 44 | |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 45 | def url_to_version(update_url): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 46 | """Return the version based on update_url. |
| 47 | |
| 48 | @param update_url: url to the image to update to. |
| 49 | |
| 50 | """ |
Dale Curtis | ddfdb94 | 2011-07-14 13:59:24 -0700 | [diff] [blame] | 51 | # The Chrome OS version is generally the last element in the URL. The only |
| 52 | # exception is delta update URLs, which are rooted under the version; e.g., |
| 53 | # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to |
| 54 | # strip off the au section of the path before reading the version. |
Dan Shi | 5002cfc | 2013-04-29 10:45:05 -0700 | [diff] [blame] | 55 | return re.sub('/au/.*', '', |
| 56 | urlparse.urlparse(update_url).path).split('/')[-1].strip() |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 57 | |
| 58 | |
Scott Zawalski | eadbf70 | 2013-03-14 09:23:06 -0400 | [diff] [blame] | 59 | def url_to_image_name(update_url): |
| 60 | """Return the image name based on update_url. |
| 61 | |
| 62 | From a URL like: |
| 63 | http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0 |
| 64 | return lumpy-release/R27-3837.0.0 |
| 65 | |
| 66 | @param update_url: url to the image to update to. |
| 67 | @returns a string representing the image name in the update_url. |
| 68 | |
| 69 | """ |
| 70 | return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:]) |
| 71 | |
| 72 | |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 73 | def _get_devserver_build_from_update_url(update_url): |
| 74 | """Get the devserver and build from the update url. |
| 75 | |
| 76 | @param update_url: The url for update. |
| 77 | Eg: http://devserver:port/update/build. |
| 78 | |
| 79 | @return: A tuple of (devserver url, build) or None if the update_url |
| 80 | doesn't match the expected pattern. |
| 81 | |
| 82 | @raises ValueError: If the update_url doesn't match the expected pattern. |
| 83 | @raises ValueError: If no global_config was found, or it doesn't contain an |
| 84 | image_url_pattern. |
| 85 | """ |
| 86 | pattern = global_config.global_config.get_config_value( |
| 87 | 'CROS', 'image_url_pattern', type=str, default='') |
| 88 | if not pattern: |
| 89 | raise ValueError('Cannot parse update_url, the global config needs ' |
| 90 | 'an image_url_pattern.') |
| 91 | re_pattern = pattern.replace('%s', '(\S+)') |
| 92 | parts = re.search(re_pattern, update_url) |
| 93 | if not parts or len(parts.groups()) < 2: |
| 94 | raise ValueError('%s is not an update url' % update_url) |
| 95 | return parts.groups() |
| 96 | |
| 97 | |
| 98 | def list_image_dir_contents(update_url): |
| 99 | """Lists the contents of the devserver for a given build/update_url. |
| 100 | |
| 101 | @param update_url: An update url. Eg: http://devserver:port/update/build. |
| 102 | """ |
| 103 | if not update_url: |
| 104 | logging.warning('Need update_url to list contents of the devserver.') |
| 105 | return |
| 106 | error_msg = 'Cannot check contents of devserver, update url %s' % update_url |
| 107 | try: |
| 108 | devserver_url, build = _get_devserver_build_from_update_url(update_url) |
| 109 | except ValueError as e: |
| 110 | logging.warning('%s: %s', error_msg, e) |
| 111 | return |
| 112 | devserver = dev_server.ImageServer(devserver_url) |
| 113 | try: |
| 114 | devserver.list_image_dir(build) |
| 115 | # The devserver will retry on URLError to avoid flaky connections, but will |
| 116 | # eventually raise the URLError if it persists. All HTTPErrors get |
| 117 | # converted to DevServerExceptions. |
| 118 | except (dev_server.DevServerException, urllib2.URLError) as e: |
| 119 | logging.warning('%s: %s', error_msg, e) |
| 120 | |
| 121 | |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 122 | # TODO(garnold) This implements shared updater functionality needed for |
| 123 | # supporting the autoupdate_EndToEnd server-side test. We should probably |
| 124 | # migrate more of the existing ChromiumOSUpdater functionality to it as we |
| 125 | # expand non-CrOS support in other tests. |
| 126 | class BaseUpdater(object): |
| 127 | """Platform-agnostic DUT update functionality.""" |
| 128 | |
| 129 | def __init__(self, updater_ctrl_bin, update_url, host): |
| 130 | """Initializes the object. |
| 131 | |
| 132 | @param updater_ctrl_bin: Path to update_engine_client. |
| 133 | @param update_url: The URL we want the update to use. |
| 134 | @param host: A client.common_lib.hosts.Host implementation. |
| 135 | """ |
| 136 | self.updater_ctrl_bin = updater_ctrl_bin |
| 137 | self.update_url = update_url |
| 138 | self.host = host |
| 139 | self._update_error_queue = multiprocessing.Queue(2) |
| 140 | |
| 141 | |
| 142 | def check_update_status(self): |
| 143 | """Returns the current update engine state. |
| 144 | |
| 145 | We use the `update_engine_client -status' command and parse the line |
| 146 | indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE". |
| 147 | """ |
| 148 | update_status = self.host.run( |
| 149 | '%s -status 2>&1 | grep CURRENT_OP' % self.updater_ctrl_bin) |
| 150 | return update_status.stdout.strip().split('=')[-1] |
| 151 | |
| 152 | |
| 153 | def trigger_update(self): |
| 154 | """Triggers a background update. |
| 155 | |
| 156 | @raise RootFSUpdateError if anything went wrong. |
| 157 | """ |
| 158 | autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' % |
| 159 | (self.updater_ctrl_bin, self.update_url)) |
| 160 | err_msg = 'Failed to trigger an update on %s.' % self.host.hostname |
| 161 | logging.info('Triggering update via: %s', autoupdate_cmd) |
| 162 | try: |
| 163 | self.host.run(autoupdate_cmd) |
| 164 | except (error.AutoservSshPermissionDeniedError, |
| 165 | error.AutoservSSHTimeout) as e: |
| 166 | err_msg += ' SSH reports an error: %s' % type(e).__name__ |
| 167 | raise RootFSUpdateError(err_msg) |
| 168 | except error.AutoservRunError as e: |
| 169 | # Check if the exit code is 255, if so it's probably a generic |
| 170 | # SSH error. |
| 171 | result = e.args[1] |
| 172 | if result.exit_status == 255: |
| 173 | err_msg += (' SSH reports a generic error (255), which could ' |
| 174 | 'indicate a problem with underlying connectivity ' |
| 175 | 'layers.') |
| 176 | raise RootFSUpdateError(err_msg) |
| 177 | |
| 178 | # We have ruled out all SSH cases, the error code is from |
| 179 | # update_engine_client, though we still don't know why. |
| 180 | list_image_dir_contents(self.update_url) |
| 181 | err_msg += (' It could be that the devserver is unreachable, the ' |
| 182 | 'payload unavailable, or there is a bug in the update ' |
| 183 | 'engine (unlikely). Reported error: %s' % |
| 184 | type(e).__name__) |
| 185 | raise RootFSUpdateError(err_msg) |
| 186 | |
| 187 | |
| 188 | def _verify_update_completed(self): |
| 189 | """Verifies that an update has completed. |
| 190 | |
| 191 | @raise RootFSUpdateError: if verification fails. |
| 192 | """ |
| 193 | status = self.check_update_status() |
| 194 | if status != UPDATER_NEED_REBOOT: |
| 195 | raise RootFSUpdateError('Update did not complete with correct ' |
| 196 | 'status. Expecting %s, actual %s' % |
| 197 | (UPDATER_NEED_REBOOT, status)) |
| 198 | |
| 199 | |
| 200 | def update_image(self): |
| 201 | """Updates the device image and verifies success.""" |
| 202 | try: |
| 203 | autoupdate_cmd = ('%s --update --omaha_url=%s 2>&1' % |
| 204 | (self.updater_ctrl_bin, self.update_url)) |
| 205 | self.host.run(autoupdate_cmd, timeout=1200) |
| 206 | except error.AutoservRunError: |
| 207 | list_image_dir_contents(self.update_url) |
| 208 | update_error = RootFSUpdateError( |
| 209 | 'Failed to install device image using update engine on %s' % |
| 210 | self.host.hostname) |
| 211 | self._update_error_queue.put(update_error) |
| 212 | raise update_error |
| 213 | except Exception as e: |
| 214 | # Don't allow other exceptions to not be caught. |
| 215 | self._update_error_queue.put(e) |
| 216 | raise e |
| 217 | |
| 218 | try: |
| 219 | self._verify_update_completed() |
| 220 | except RootFSUpdateError as e: |
| 221 | self._update_error_queue.put(e) |
| 222 | raise |
| 223 | |
| 224 | |
| 225 | class ChromiumOSUpdater(BaseUpdater): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 226 | """Helper class used to update DUT with image of desired version.""" |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 227 | REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update' |
| 228 | UPDATER_BIN = '/usr/bin/update_engine_client' |
| 229 | STATEFUL_UPDATE = '/tmp/stateful_update' |
| 230 | UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed' |
| 231 | UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine'] |
| 232 | |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 233 | KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3} |
| 234 | KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5} |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 235 | # Time to wait for new kernel to be marked successful after |
| 236 | # auto update. |
| 237 | KERNEL_UPDATE_TIMEOUT = 120 |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 238 | |
| 239 | |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 240 | def __init__(self, update_url, host=None, local_devserver=False): |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 241 | super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url, |
| 242 | host) |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 243 | self.local_devserver = local_devserver |
| 244 | if not local_devserver: |
| 245 | self.update_version = url_to_version(update_url) |
| 246 | else: |
| 247 | self.update_version = None |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 248 | |
Gilad Arnold | 5f2ff44 | 2015-09-21 07:06:40 -0700 | [diff] [blame] | 249 | |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 250 | def reset_update_engine(self): |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 251 | """Resets the host to prepare for a clean update regardless of state.""" |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 252 | self._run('rm -f %s' % self.UPDATED_MARKER) |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 253 | self._run('stop ui || true') |
| 254 | self._run('stop update-engine || true') |
| 255 | self._run('start update-engine') |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 256 | |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 257 | if self.check_update_status() != UPDATER_IDLE: |
| 258 | raise ChromiumOSError('%s is not in an installable state' % |
| 259 | self.host.hostname) |
| 260 | |
| 261 | |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 262 | def _run(self, cmd, *args, **kwargs): |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 263 | """Abbreviated form of self.host.run(...)""" |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 264 | return self.host.run(cmd, *args, **kwargs) |
| 265 | |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 266 | |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 267 | def rootdev(self, options=''): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 268 | """Returns the stripped output of rootdev <options>. |
| 269 | |
| 270 | @param options: options to run rootdev. |
| 271 | |
| 272 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 273 | return self._run('rootdev %s' % options).stdout.strip() |
| 274 | |
| 275 | |
| 276 | def get_kernel_state(self): |
| 277 | """Returns the (<active>, <inactive>) kernel state as a pair.""" |
| 278 | active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0]) |
| 279 | if active_root == self.KERNEL_A['root']: |
| 280 | return self.KERNEL_A, self.KERNEL_B |
| 281 | elif active_root == self.KERNEL_B['root']: |
| 282 | return self.KERNEL_B, self.KERNEL_A |
| 283 | else: |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 284 | raise ChromiumOSError('Encountered unknown root partition: %s' % |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 285 | active_root) |
| 286 | |
| 287 | |
| 288 | def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'): |
| 289 | """Return numeric cgpt value for the specified flag, kernel, device. """ |
| 290 | return int(self._run('cgpt show -n -i %d %s %s' % ( |
| 291 | kernel['kernel'], flag, dev)).stdout.strip()) |
| 292 | |
| 293 | |
| 294 | def get_kernel_priority(self, kernel): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 295 | """Return numeric priority for the specified kernel. |
| 296 | |
| 297 | @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. |
| 298 | |
| 299 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 300 | return self._cgpt('-P', kernel) |
| 301 | |
| 302 | |
| 303 | def get_kernel_success(self, kernel): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 304 | """Return boolean success flag for the specified kernel. |
| 305 | |
| 306 | @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. |
| 307 | |
| 308 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 309 | return self._cgpt('-S', kernel) != 0 |
| 310 | |
| 311 | |
| 312 | def get_kernel_tries(self, kernel): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 313 | """Return tries count for the specified kernel. |
| 314 | |
| 315 | @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. |
| 316 | |
| 317 | """ |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 318 | return self._cgpt('-T', kernel) |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 319 | |
| 320 | |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 321 | def get_stateful_update_script(self): |
| 322 | """Returns the path to the stateful update script on the target.""" |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 323 | # We attempt to load the local stateful update path in 3 different |
| 324 | # ways. First we use the location specified in the autotest global |
| 325 | # config. If this doesn't exist, we attempt to use the Chromium OS |
| 326 | # Chroot path to the installed script. If all else fails, we use the |
| 327 | # stateful update script on the host. |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 328 | stateful_update_path = os.path.join( |
| 329 | global_config.global_config.get_config_value( |
| 330 | 'CROS', 'source_tree', default=''), |
| 331 | LOCAL_STATEFUL_UPDATE_PATH) |
| 332 | |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 333 | if not os.path.exists(stateful_update_path): |
Ilja H. Friedel | 04be2bd | 2014-05-07 21:29:59 -0700 | [diff] [blame] | 334 | logging.warning('Could not find Chrome OS source location for ' |
Gilad Arnold | 5f2ff44 | 2015-09-21 07:06:40 -0700 | [diff] [blame] | 335 | 'stateful_update script at %s, falling back to ' |
| 336 | 'chroot copy.', stateful_update_path) |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 337 | stateful_update_path = LOCAL_CHROOT_STATEFUL_UPDATE_PATH |
| 338 | |
| 339 | if not os.path.exists(stateful_update_path): |
Ilja H. Friedel | 04be2bd | 2014-05-07 21:29:59 -0700 | [diff] [blame] | 340 | logging.warning('Could not chroot stateful_update script, falling ' |
Gilad Arnold | 5f2ff44 | 2015-09-21 07:06:40 -0700 | [diff] [blame] | 341 | 'back on client copy.') |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 342 | statefuldev_script = self.REMOTE_STATEUL_UPDATE_PATH |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 343 | else: |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 344 | self.host.send_file( |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 345 | stateful_update_path, self.STATEFUL_UPDATE, |
| 346 | delete_dest=True) |
| 347 | statefuldev_script = self.STATEFUL_UPDATE |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 348 | |
| 349 | return statefuldev_script |
| 350 | |
| 351 | |
| 352 | def reset_stateful_partition(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 353 | """Clear any pending stateful update request.""" |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 354 | statefuldev_cmd = [self.get_stateful_update_script()] |
| 355 | statefuldev_cmd += ['--stateful_change=reset', '2>&1'] |
Chris Sosa | 66d7407 | 2013-09-19 11:21:29 -0700 | [diff] [blame] | 356 | self._run(' '.join(statefuldev_cmd)) |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 357 | |
| 358 | |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 359 | def revert_boot_partition(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 360 | """Revert the boot partition.""" |
Dale Curtis | d9b26b9 | 2011-10-24 13:34:46 -0700 | [diff] [blame] | 361 | part = self.rootdev('-s') |
Ilja H. Friedel | 04be2bd | 2014-05-07 21:29:59 -0700 | [diff] [blame] | 362 | logging.warning('Reverting update; Boot partition will be %s', part) |
Sean O | 267c00b | 2010-08-31 15:54:55 +0200 | [diff] [blame] | 363 | return self._run('/postinst %s 2>&1' % part) |
| 364 | |
| 365 | |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 366 | def rollback_rootfs(self, powerwash): |
| 367 | """Triggers rollback and waits for it to complete. |
| 368 | |
| 369 | @param powerwash: If true, powerwash as part of rollback. |
| 370 | |
| 371 | @raise RootFSUpdateError if anything went wrong. |
| 372 | |
| 373 | """ |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 374 | version = self.host.get_release_version() |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 375 | # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches |
| 376 | # X.Y.Z. This version split just pulls the first part out. |
| 377 | try: |
| 378 | build_number = int(version.split('.')[0]) |
| 379 | except ValueError: |
| 380 | logging.error('Could not parse build number.') |
| 381 | build_number = 0 |
| 382 | |
| 383 | if build_number >= 5772: |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 384 | can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 385 | logging.info('Checking for rollback.') |
| 386 | try: |
| 387 | self._run(can_rollback_cmd) |
| 388 | except error.AutoservRunError as e: |
| 389 | raise RootFSUpdateError("Rollback isn't possible on %s: %s" % |
| 390 | (self.host.hostname, str(e))) |
| 391 | |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 392 | rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 393 | if not powerwash: |
| 394 | rollback_cmd += ' --nopowerwash' |
| 395 | |
Chris Sosa | c861752 | 2014-06-09 23:22:26 +0000 | [diff] [blame] | 396 | logging.info('Performing rollback.') |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 397 | try: |
| 398 | self._run(rollback_cmd) |
Chris Sosa | c193217 | 2013-10-16 13:28:53 -0700 | [diff] [blame] | 399 | except error.AutoservRunError as e: |
| 400 | raise RootFSUpdateError('Rollback failed on %s: %s' % |
| 401 | (self.host.hostname, str(e))) |
| 402 | |
| 403 | self._verify_update_completed() |
| 404 | |
Gilad Arnold | 0ed760c | 2012-11-05 23:42:53 -0800 | [diff] [blame] | 405 | |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 406 | # TODO(garnold) This is here for backward compatibility and should be |
| 407 | # deprecated once we shift to using update_image() everywhere. |
Chris Sosa | 2f1ae9f | 2013-08-13 10:00:15 -0700 | [diff] [blame] | 408 | def update_rootfs(self): |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 409 | """Run the standard command to force an update.""" |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 410 | return self.update_image() |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 411 | |
| 412 | |
Chris Sosa | 7231260 | 2013-04-16 15:01:56 -0700 | [diff] [blame] | 413 | def update_stateful(self, clobber=True): |
| 414 | """Updates the stateful partition. |
| 415 | |
| 416 | @param clobber: If True, a clean stateful installation. |
| 417 | """ |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 418 | logging.info('Updating stateful partition...') |
joychen | 03eaad9 | 2013-06-26 09:55:21 -0700 | [diff] [blame] | 419 | statefuldev_url = self.update_url.replace('update', |
| 420 | 'static') |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 421 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 422 | # Attempt stateful partition update; this must succeed so that the newly |
| 423 | # installed host is testable after update. |
Chris Sosa | 7231260 | 2013-04-16 15:01:56 -0700 | [diff] [blame] | 424 | statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url] |
| 425 | if clobber: |
| 426 | statefuldev_cmd.append('--stateful_change=clean') |
| 427 | |
| 428 | statefuldev_cmd.append('2>&1') |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 429 | try: |
| 430 | self._run(' '.join(statefuldev_cmd), timeout=600) |
| 431 | except error.AutoservRunError: |
Gilad Arnold | 62cf3a4 | 2015-10-01 09:15:25 -0700 | [diff] [blame] | 432 | update_error = StatefulUpdateError( |
| 433 | 'Failed to perform stateful update on %s' % |
| 434 | self.host.hostname) |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 435 | self._update_error_queue.put(update_error) |
| 436 | raise update_error |
Simran Basi | 3b858a2 | 2015-03-17 16:23:24 -0700 | [diff] [blame] | 437 | except Exception as e: |
| 438 | # Don't allow other exceptions to not be caught. |
| 439 | self._update_error_queue.put(e) |
| 440 | raise e |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 441 | |
| 442 | |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 443 | def run_update(self, update_root=True): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 444 | """Update the DUT with image of specific version. |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 445 | |
Chris Sosa | e92399e | 2015-04-24 11:32:59 -0700 | [diff] [blame] | 446 | @param update_root: True to force a rootfs update. |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 447 | """ |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 448 | booted_version = self.host.get_release_version() |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 449 | if self.update_version: |
| 450 | logging.info('Updating from version %s to %s.', |
| 451 | booted_version, self.update_version) |
Dale Curtis | 53d5586 | 2011-05-16 12:17:59 -0700 | [diff] [blame] | 452 | |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 453 | # Check that Dev Server is accepting connections (from autoserv's host). |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 454 | # If we can't talk to it, the machine host probably can't either. |
| 455 | auserver_host = urlparse.urlparse(self.update_url)[1] |
| 456 | try: |
| 457 | httplib.HTTPConnection(auserver_host).connect() |
Dale Curtis | 5c32c72 | 2011-05-04 19:24:23 -0700 | [diff] [blame] | 458 | except IOError: |
| 459 | raise ChromiumOSError( |
| 460 | 'Update server at %s not available' % auserver_host) |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 461 | |
Chris Sosa | a3ac215 | 2012-05-23 22:23:13 -0700 | [diff] [blame] | 462 | logging.info('Installing from %s to %s', self.update_url, |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 463 | self.host.hostname) |
| 464 | |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 465 | # Reset update state. |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 466 | self.reset_update_engine() |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 467 | self.reset_stateful_partition() |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 468 | |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 469 | try: |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 470 | updaters = [ |
Chris Sosa | 2f1ae9f | 2013-08-13 10:00:15 -0700 | [diff] [blame] | 471 | multiprocessing.process.Process(target=self.update_rootfs), |
Chris Sosa | 7231260 | 2013-04-16 15:01:56 -0700 | [diff] [blame] | 472 | multiprocessing.process.Process(target=self.update_stateful) |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 473 | ] |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 474 | if not update_root: |
| 475 | logging.info('Root update is skipped.') |
| 476 | updaters = updaters[1:] |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 477 | |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 478 | # Run the updaters in parallel. |
| 479 | for updater in updaters: updater.start() |
| 480 | for updater in updaters: updater.join() |
| 481 | |
| 482 | # Re-raise the first error that occurred. |
| 483 | if not self._update_error_queue.empty(): |
| 484 | update_error = self._update_error_queue.get() |
| 485 | self.revert_boot_partition() |
Chris Sosa | 5e4246b | 2012-05-22 18:05:22 -0700 | [diff] [blame] | 486 | self.reset_stateful_partition() |
Chris Sosa | 77556d8 | 2012-04-05 15:23:14 -0700 | [diff] [blame] | 487 | raise update_error |
Sean O | c053dfe | 2010-08-23 18:22:26 +0200 | [diff] [blame] | 488 | |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 489 | logging.info('Update complete.') |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 490 | except: |
| 491 | # Collect update engine logs in the event of failure. |
| 492 | if self.host.job: |
| 493 | logging.info('Collecting update engine logs...') |
| 494 | self.host.get_file( |
Gilad Arnold | 0c0df73 | 2015-09-21 06:37:59 -0700 | [diff] [blame] | 495 | self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir, |
| 496 | preserve_perm=False) |
Prashanth B | 32baa9b | 2014-03-13 13:23:01 -0700 | [diff] [blame] | 497 | list_image_dir_contents(self.update_url) |
Dale Curtis | 1e97318 | 2011-07-12 18:21:36 -0700 | [diff] [blame] | 498 | raise |
Dan Shi | 10e992b | 2013-08-30 11:02:59 -0700 | [diff] [blame] | 499 | finally: |
| 500 | self.host.show_update_engine_log() |
Sean O'Connor | 5346e4e | 2010-08-12 18:49:24 +0200 | [diff] [blame] | 501 | |
| 502 | |
Dale Curtis | a94c19c | 2011-05-02 15:05:17 -0700 | [diff] [blame] | 503 | def check_version(self): |
Dan Shi | 0f466e8 | 2013-02-22 15:44:58 -0800 | [diff] [blame] | 504 | """Check the image running in DUT has the desired version. |
| 505 | |
| 506 | @returns: True if the DUT's image version matches the version that |
| 507 | the autoupdater tries to update to. |
| 508 | |
| 509 | """ |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 510 | booted_version = self.host.get_release_version() |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 511 | return (self.update_version and |
| 512 | self.update_version.endswith(booted_version)) |
| 513 | |
| 514 | |
| 515 | def check_version_to_confirm_install(self): |
| 516 | """Check image running in DUT has the desired version to be installed. |
| 517 | |
| 518 | The method should not be used to check if DUT needs to have a full |
| 519 | reimage. Only use it to confirm a image is installed. |
| 520 | |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 521 | The method is designed to verify version for following 6 scenarios with |
Dan Shi | 190c780 | 2013-04-04 13:05:30 -0700 | [diff] [blame] | 522 | samples of version to update to and expected booted version: |
| 523 | 1. trybot paladin build. |
| 524 | update version: trybot-lumpy-paladin/R27-3837.0.0-b123 |
| 525 | booted version: 3837.0.2013_03_21_1340 |
| 526 | |
| 527 | 2. trybot release build. |
| 528 | update version: trybot-lumpy-release/R27-3837.0.0-b456 |
| 529 | booted version: 3837.0.0 |
| 530 | |
| 531 | 3. buildbot official release build. |
| 532 | update version: lumpy-release/R27-3837.0.0 |
| 533 | booted version: 3837.0.0 |
| 534 | |
| 535 | 4. non-official paladin rc build. |
| 536 | update version: lumpy-paladin/R27-3878.0.0-rc7 |
| 537 | booted version: 3837.0.0-rc7 |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 538 | |
Dan Shi | 7f79551 | 2013-04-12 10:08:17 -0700 | [diff] [blame] | 539 | 5. chrome-perf build. |
| 540 | update version: lumpy-chrome-perf/R28-3837.0.0-b2996 |
| 541 | booted version: 3837.0.0 |
| 542 | |
Dan Shi | 73aa290 | 2013-05-03 11:22:11 -0700 | [diff] [blame] | 543 | 6. pgo-generate build. |
| 544 | update version: lumpy-release-pgo-generate/R28-3837.0.0-b2996 |
| 545 | booted version: 3837.0.0-pgo-generate |
| 546 | |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 547 | When we are checking if a DUT needs to do a full install, we should NOT |
| 548 | 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] | 549 | it may return false positive for a DUT running trybot paladin build to |
| 550 | be updated to another trybot paladin build. |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 551 | |
Dan Shi | 190c780 | 2013-04-04 13:05:30 -0700 | [diff] [blame] | 552 | TODO: This logic has a bug if a trybot paladin build failed to be |
| 553 | installed in a DUT running an older trybot paladin build with same |
| 554 | platform number, but different build number (-b###). So to conclusively |
| 555 | determine if a tryjob paladin build is imaged successfully, we may need |
| 556 | to find out the date string from update url. |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 557 | |
| 558 | @returns: True if the DUT's image version (without the date string if |
| 559 | the image is a trybot build), matches the version that the |
| 560 | autoupdater is trying to update to. |
| 561 | |
| 562 | """ |
J. Richard Barnette | ec1de42 | 2013-06-26 15:44:07 -0700 | [diff] [blame] | 563 | # In the local_devserver case, we can't know the expected |
| 564 | # build, so just pass. |
| 565 | if not self.update_version: |
| 566 | return True |
| 567 | |
Dan Shi | b95bb86 | 2013-03-22 16:29:28 -0700 | [diff] [blame] | 568 | # Always try the default check_version method first, this prevents |
| 569 | # any backward compatibility issue. |
| 570 | if self.check_version(): |
| 571 | return True |
| 572 | |
Dan Shi | 549fb82 | 2015-03-24 18:01:11 -0700 | [diff] [blame] | 573 | return utils.version_match(self.update_version, |
| 574 | self.host.get_release_version(), |
| 575 | self.update_url) |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 576 | |
| 577 | |
| 578 | def verify_boot_expectations(self, expected_kernel_state, rollback_message): |
| 579 | """Verifies that we fully booted given expected kernel state. |
| 580 | |
| 581 | This method both verifies that we booted using the correct kernel |
| 582 | state and that the OS has marked the kernel as good. |
| 583 | |
| 584 | @param expected_kernel_state: kernel state that we are verifying with |
| 585 | i.e. I expect to be booted onto partition 4 etc. See output of |
| 586 | get_kernel_state. |
| 587 | @param rollback_message: string to raise as a ChromiumOSError |
| 588 | if we booted with the wrong partition. |
| 589 | |
| 590 | @raises ChromiumOSError: If we didn't. |
| 591 | """ |
| 592 | # Figure out the newly active kernel. |
| 593 | active_kernel_state = self.get_kernel_state()[0] |
| 594 | |
| 595 | # Check for rollback due to a bad build. |
| 596 | if (expected_kernel_state and |
| 597 | active_kernel_state != expected_kernel_state): |
Don Garrett | 56b1cc8 | 2013-12-06 17:49:20 -0800 | [diff] [blame] | 598 | |
| 599 | # Kernel crash reports should be wiped between test runs, but |
| 600 | # may persist from earlier parts of the test, or from problems |
| 601 | # with provisioning. |
| 602 | # |
| 603 | # Kernel crash reports will NOT be present if the crash happened |
| 604 | # before encrypted stateful is mounted. |
| 605 | # |
| 606 | # TODO(dgarrett): Integrate with server/crashcollect.py at some |
| 607 | # point. |
| 608 | kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash') |
| 609 | if kernel_crashes: |
| 610 | rollback_message += ': kernel_crash' |
| 611 | logging.debug('Found %d kernel crash reports:', |
| 612 | len(kernel_crashes)) |
| 613 | # The crash names contain timestamps that may be useful: |
| 614 | # kernel.20131207.005945.0.kcrash |
| 615 | for crash in kernel_crashes: |
Dan Shi | 0942b1d | 2015-03-31 11:07:00 -0700 | [diff] [blame] | 616 | logging.debug(' %s', os.path.basename(crash)) |
Don Garrett | 56b1cc8 | 2013-12-06 17:49:20 -0800 | [diff] [blame] | 617 | |
Chris Sosa | 6542508 | 2013-10-16 13:26:22 -0700 | [diff] [blame] | 618 | # Print out some information to make it easier to debug |
| 619 | # the rollback. |
| 620 | logging.debug('Dumping partition table.') |
| 621 | self._run('cgpt show $(rootdev -s -d)') |
| 622 | logging.debug('Dumping crossystem for firmware debugging.') |
| 623 | self._run('crossystem --all') |
| 624 | raise ChromiumOSError(rollback_message) |
| 625 | |
| 626 | # Make sure chromeos-setgoodkernel runs. |
| 627 | try: |
| 628 | utils.poll_for_condition( |
| 629 | lambda: (self.get_kernel_tries(active_kernel_state) == 0 |
| 630 | and self.get_kernel_success(active_kernel_state)), |
| 631 | exception=ChromiumOSError(), |
| 632 | timeout=self.KERNEL_UPDATE_TIMEOUT, sleep_interval=5) |
| 633 | except ChromiumOSError: |
| 634 | services_status = self._run('status system-services').stdout |
| 635 | if services_status != 'system-services start/running\n': |
| 636 | event = ('Chrome failed to reach login screen') |
| 637 | else: |
| 638 | event = ('update-engine failed to call ' |
| 639 | 'chromeos-setgoodkernel') |
| 640 | raise ChromiumOSError( |
| 641 | 'After update and reboot, %s ' |
| 642 | 'within %d seconds' % (event, |
| 643 | self.KERNEL_UPDATE_TIMEOUT)) |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 644 | |
| 645 | |
| 646 | class BrilloUpdater(BaseUpdater): |
| 647 | """Helper class for updating a Brillo DUT.""" |
| 648 | |
| 649 | def __init__(self, update_url, host=None): |
| 650 | """Initialize the object. |
| 651 | |
| 652 | @param update_url: The URL we want the update to use. |
| 653 | @param host: A client.common_lib.hosts.Host implementation. |
| 654 | """ |
Gilad Arnold | ecf894b | 2015-10-07 08:48:22 -0700 | [diff] [blame^] | 655 | super(BrilloUpdater, self).__init__( |
Gilad Arnold | d6adeb8 | 2015-09-21 07:10:03 -0700 | [diff] [blame] | 656 | '/system/bin/update_engine_client', update_url, host) |