blob: 3e50332e2af2b69d547dd7c43c6720165a947c9b [file] [log] [blame]
Chris Sosa5e4246b2012-05-22 18:05:22 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Sean O'Connor5346e4e2010-08-12 18:49:24 +02002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Don Garrett56b1cc82013-12-06 17:49:20 -08005import glob
Sean O'Connor5346e4e2010-08-12 18:49:24 +02006import logging
Dale Curtis5c32c722011-05-04 19:24:23 -07007import os
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -07008import time
Sean O'Connor5346e4e2010-08-12 18:49:24 +02009import re
Sean O'Connor5346e4e2010-08-12 18:49:24 +020010import urlparse
Prashanth B32baa9b2014-03-13 13:23:01 -070011import urllib2
Sean O'Connor5346e4e2010-08-12 18:49:24 +020012
Chris Sosa65425082013-10-16 13:26:22 -070013from autotest_lib.client.bin import utils
Dale Curtis5c32c722011-05-04 19:24:23 -070014from autotest_lib.client.common_lib import error, global_config
Prashanth B32baa9b2014-03-13 13:23:01 -070015from autotest_lib.client.common_lib.cros import dev_server
Shelley Chen61d28982016-10-28 09:40:20 -070016from autotest_lib.server import utils as server_utils
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -080017from chromite.lib import retry_util
Dan Shif3a35f72016-01-25 11:18:14 -080018
Shelley Chen16b8df32016-10-27 16:24:21 -070019try:
20 from chromite.lib import metrics
Dan Shi5e2efb72017-02-07 11:40:23 -080021except ImportError:
22 metrics = utils.metrics_mock
Sean O'Connor5346e4e2010-08-12 18:49:24 +020023
Dale Curtis5c32c722011-05-04 19:24:23 -070024# Local stateful update path is relative to the CrOS source directory.
25LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update'
Chris Sosaa3ac2152012-05-23 22:23:13 -070026LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update'
Sean O'Connor5346e4e2010-08-12 18:49:24 +020027UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020028UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
beeps5e8c45a2013-12-17 22:05:11 -080029# A list of update engine client states that occur after an update is triggered.
30UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE',
31 'UPDATE_STATUS_UPDATE_AVAILABLE',
32 'UPDATE_STATUS_DOWNLOADING',
33 'UPDATE_STATUS_FINALIZING']
Sean O'Connor5346e4e2010-08-12 18:49:24 +020034
35class ChromiumOSError(error.InstallError):
36 """Generic error for ChromiumOS-specific exceptions."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -070037
38
39class BrilloError(error.InstallError):
40 """Generic error for Brillo-specific exceptions."""
Sean O'Connor5346e4e2010-08-12 18:49:24 +020041
42
Chris Sosa77556d82012-04-05 15:23:14 -070043class RootFSUpdateError(ChromiumOSError):
44 """Raised when the RootFS fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070045
46
47class StatefulUpdateError(ChromiumOSError):
48 """Raised when the stateful partition fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070049
50
Sean O'Connor5346e4e2010-08-12 18:49:24 +020051def url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -080052 """Return the version based on update_url.
53
54 @param update_url: url to the image to update to.
55
56 """
Dale Curtisddfdb942011-07-14 13:59:24 -070057 # The Chrome OS version is generally the last element in the URL. The only
58 # exception is delta update URLs, which are rooted under the version; e.g.,
59 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
60 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -070061 return re.sub('/au/.*', '',
62 urlparse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +020063
64
Scott Zawalskieadbf702013-03-14 09:23:06 -040065def url_to_image_name(update_url):
66 """Return the image name based on update_url.
67
68 From a URL like:
69 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
70 return lumpy-release/R27-3837.0.0
71
72 @param update_url: url to the image to update to.
73 @returns a string representing the image name in the update_url.
74
75 """
76 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
77
78
Prashanth B32baa9b2014-03-13 13:23:01 -070079def _get_devserver_build_from_update_url(update_url):
80 """Get the devserver and build from the update url.
81
82 @param update_url: The url for update.
83 Eg: http://devserver:port/update/build.
84
85 @return: A tuple of (devserver url, build) or None if the update_url
86 doesn't match the expected pattern.
87
88 @raises ValueError: If the update_url doesn't match the expected pattern.
89 @raises ValueError: If no global_config was found, or it doesn't contain an
90 image_url_pattern.
91 """
92 pattern = global_config.global_config.get_config_value(
93 'CROS', 'image_url_pattern', type=str, default='')
94 if not pattern:
95 raise ValueError('Cannot parse update_url, the global config needs '
96 'an image_url_pattern.')
97 re_pattern = pattern.replace('%s', '(\S+)')
98 parts = re.search(re_pattern, update_url)
99 if not parts or len(parts.groups()) < 2:
100 raise ValueError('%s is not an update url' % update_url)
101 return parts.groups()
102
103
104def list_image_dir_contents(update_url):
105 """Lists the contents of the devserver for a given build/update_url.
106
107 @param update_url: An update url. Eg: http://devserver:port/update/build.
108 """
109 if not update_url:
110 logging.warning('Need update_url to list contents of the devserver.')
111 return
112 error_msg = 'Cannot check contents of devserver, update url %s' % update_url
113 try:
114 devserver_url, build = _get_devserver_build_from_update_url(update_url)
115 except ValueError as e:
116 logging.warning('%s: %s', error_msg, e)
117 return
118 devserver = dev_server.ImageServer(devserver_url)
119 try:
120 devserver.list_image_dir(build)
121 # The devserver will retry on URLError to avoid flaky connections, but will
122 # eventually raise the URLError if it persists. All HTTPErrors get
123 # converted to DevServerExceptions.
124 except (dev_server.DevServerException, urllib2.URLError) as e:
125 logging.warning('%s: %s', error_msg, e)
126
127
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700128# TODO(garnold) This implements shared updater functionality needed for
129# supporting the autoupdate_EndToEnd server-side test. We should probably
130# migrate more of the existing ChromiumOSUpdater functionality to it as we
131# expand non-CrOS support in other tests.
132class BaseUpdater(object):
133 """Platform-agnostic DUT update functionality."""
134
135 def __init__(self, updater_ctrl_bin, update_url, host):
136 """Initializes the object.
137
138 @param updater_ctrl_bin: Path to update_engine_client.
139 @param update_url: The URL we want the update to use.
140 @param host: A client.common_lib.hosts.Host implementation.
141 """
142 self.updater_ctrl_bin = updater_ctrl_bin
143 self.update_url = update_url
144 self.host = host
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700145
146
147 def check_update_status(self):
148 """Returns the current update engine state.
149
150 We use the `update_engine_client -status' command and parse the line
151 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE".
152 """
153 update_status = self.host.run(
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700154 '%s -status | grep CURRENT_OP' % self.updater_ctrl_bin)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700155 return update_status.stdout.strip().split('=')[-1]
156
157
Shuqian Zhaod9992722016-02-29 12:26:38 -0800158 def get_last_update_error(self):
159 """Get the last autoupdate error code."""
160 error_msg = self.host.run(
161 '%s --last_attempt_error' % self.updater_ctrl_bin)
162 error_msg = (error_msg.stdout.strip()).replace('\n', ', ')
163 return error_msg
164
165
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800166 def _base_update_handler_no_retry(self, run_args):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800167 """Base function to handle a remote update ssh call.
168
169 @param run_args: Dictionary of args passed to ssh_host.run function.
Shuqian Zhaod9992722016-02-29 12:26:38 -0800170
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800171 @throws: intercepts and re-throws all exceptions
Shuqian Zhaod9992722016-02-29 12:26:38 -0800172 """
Shuqian Zhaod9992722016-02-29 12:26:38 -0800173 try:
174 self.host.run(**run_args)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800175 except Exception as e:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800176 logging.debug('exception in update handler: %s', e)
177 raise e
Shuqian Zhaod9992722016-02-29 12:26:38 -0800178
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800179
180 def _base_update_handler(self, run_args, err_msg_prefix=None):
181 """Handle a remote update ssh call, possibly with retries.
182
183 @param run_args: Dictionary of args passed to ssh_host.run function.
184 @param err_msg_prefix: Prefix of the exception error message.
185 """
186 def exception_handler(e):
187 """Examines exceptions and returns True if the update handler
188 should be retried.
189
190 @param e: the exception intercepted by the retry util.
191 """
192 return (isinstance(e, error.AutoservSSHTimeout) or
193 (isinstance(e, error.GenericHostRunError) and
194 hasattr(e, 'description') and
195 (re.search('ERROR_CODE=37', e.description) or
196 re.search('generic error .255.', e.description))))
197
198 try:
199 # Try the update twice (arg 2 is max_retry, not including the first
200 # call). Some exceptions may be caught by the retry handler.
201 retry_util.GenericRetry(exception_handler, 1,
202 self._base_update_handler_no_retry,
203 run_args)
204 except Exception as e:
205 message = err_msg_prefix + ': ' + str(e)
206 raise RootFSUpdateError(message)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800207
208
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700209 def trigger_update(self):
210 """Triggers a background update.
211
Shuqian Zhaod9992722016-02-29 12:26:38 -0800212 @raise RootFSUpdateError or unknown Exception if anything went wrong.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700213 """
214 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' %
215 (self.updater_ctrl_bin, self.update_url))
Shuqian Zhaod9992722016-02-29 12:26:38 -0800216 run_args = {'command': autoupdate_cmd}
217 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700218 logging.info('Triggering update via: %s', autoupdate_cmd)
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800219 try:
220 to_raise = None
221 self._base_update_handler(run_args, err_prefix)
222 except Exception as e:
223 to_raise = e
Dan Shi5e2efb72017-02-07 11:40:23 -0800224
225 build_name = url_to_image_name(self.update_url)
226 try:
227 board, build_type, milestone, _ = server_utils.ParseBuildName(
228 build_name)
229 except server_utils.ParseBuildNameException:
230 logging.warning('Unable to parse build name %s for metrics. '
231 'Continuing anyway.', build_name)
232 board, build_type, milestone = ('', '', '')
233 c = metrics.Counter('chromeos/autotest/autoupdater/trigger')
234 f = {'dev_server':
235 dev_server.get_hostname(self.update_url),
236 'success': to_raise is None,
237 'board': board,
238 'build_type': build_type,
239 'milestone': milestone}
240 c.increment(fields=f)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800241 if to_raise:
Shelley Chen16b8df32016-10-27 16:24:21 -0700242 raise to_raise
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700243
244
245 def _verify_update_completed(self):
246 """Verifies that an update has completed.
247
248 @raise RootFSUpdateError: if verification fails.
249 """
250 status = self.check_update_status()
251 if status != UPDATER_NEED_REBOOT:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800252 error_msg = ''
253 if status == UPDATER_IDLE:
254 error_msg = 'Update error: %s' % self.get_last_update_error()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700255 raise RootFSUpdateError('Update did not complete with correct '
Shuqian Zhaod9992722016-02-29 12:26:38 -0800256 'status. Expecting %s, actual %s. %s' %
257 (UPDATER_NEED_REBOOT, status, error_msg))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700258
259
260 def update_image(self):
261 """Updates the device image and verifies success."""
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700262 autoupdate_cmd = ('%s --update --omaha_url=%s' %
Shuqian Zhaod9992722016-02-29 12:26:38 -0800263 (self.updater_ctrl_bin, self.update_url))
264 run_args = {'command': autoupdate_cmd, 'timeout': 3600}
265 err_prefix = ('Failed to install device image using payload at %s '
266 'on %s. ' % (self.update_url, self.host.hostname))
267 logging.info('Updating image via: %s', autoupdate_cmd)
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800268 try:
269 to_raise = None
270 self._base_update_handler(run_args, err_prefix)
271 except Exception as e:
272 to_raise = e
Dan Shi5e2efb72017-02-07 11:40:23 -0800273
274 build_name = url_to_image_name(self.update_url)
275 try:
276 board, build_type, milestone, _ = server_utils.ParseBuildName(
277 build_name)
278 except server_utils.ParseBuildNameException:
279 logging.warning('Unable to parse build name %s for metrics. '
280 'Continuing anyway.', build_name)
281 board, build_type, milestone = ('', '', '')
282 c = metrics.Counter('chromeos/autotest/autoupdater/update')
283 f = {'dev_server':
284 dev_server.get_hostname(self.update_url),
285 'success': to_raise is None,
286 'board': board,
287 'build_type': build_type,
288 'milestone': milestone}
289 c.increment(fields=f)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800290 if to_raise:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800291 raise to_raise
Aviv Keshetf37b2d72016-06-01 19:27:59 -0700292 self._verify_update_completed()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700293
294
295class ChromiumOSUpdater(BaseUpdater):
Dan Shi0f466e82013-02-22 15:44:58 -0800296 """Helper class used to update DUT with image of desired version."""
Gilad Arnold0c0df732015-09-21 06:37:59 -0700297 REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update'
298 UPDATER_BIN = '/usr/bin/update_engine_client'
299 STATEFUL_UPDATE = '/tmp/stateful_update'
300 UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed'
301 UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine']
302
Dale Curtisa94c19c2011-05-02 15:05:17 -0700303 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
304 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
Chris Sosa65425082013-10-16 13:26:22 -0700305 # Time to wait for new kernel to be marked successful after
306 # auto update.
307 KERNEL_UPDATE_TIMEOUT = 120
Dale Curtisa94c19c2011-05-02 15:05:17 -0700308
Chris Sosaa3ac2152012-05-23 22:23:13 -0700309 def __init__(self, update_url, host=None, local_devserver=False):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700310 super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url,
311 host)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700312 self.local_devserver = local_devserver
313 if not local_devserver:
Dan Shif3a35f72016-01-25 11:18:14 -0800314 self.update_version = url_to_version(update_url)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700315 else:
Dan Shif3a35f72016-01-25 11:18:14 -0800316 self.update_version = None
Sean Oc053dfe2010-08-23 18:22:26 +0200317
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700318
Sean Oc053dfe2010-08-23 18:22:26 +0200319 def reset_update_engine(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700320 """Resets the host to prepare for a clean update regardless of state."""
Gilad Arnold0c0df732015-09-21 06:37:59 -0700321 self._run('rm -f %s' % self.UPDATED_MARKER)
Chris Sosae92399e2015-04-24 11:32:59 -0700322 self._run('stop ui || true')
323 self._run('stop update-engine || true')
324 self._run('start update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -0700325
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700326 # Wait for update engine to be ready.
327 retry=3
328 while retry >= 0:
329 retry -= 1
330 try:
331 status = self.check_update_status()
332 break
333 except error.AutoservRunError as e:
334 if retry > 0:
335 logging.info('Retrying to get the update_engine status...')
336 time.sleep(5)
337 continue
338 else:
339 raise e
340
341 if status != UPDATER_IDLE:
Sean Oc053dfe2010-08-23 18:22:26 +0200342 raise ChromiumOSError('%s is not in an installable state' %
343 self.host.hostname)
344
345
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200346 def _run(self, cmd, *args, **kwargs):
Dale Curtis5c32c722011-05-04 19:24:23 -0700347 """Abbreviated form of self.host.run(...)"""
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200348 return self.host.run(cmd, *args, **kwargs)
349
Sean Oc053dfe2010-08-23 18:22:26 +0200350
Dale Curtisa94c19c2011-05-02 15:05:17 -0700351 def rootdev(self, options=''):
Dan Shi0f466e82013-02-22 15:44:58 -0800352 """Returns the stripped output of rootdev <options>.
353
354 @param options: options to run rootdev.
355
356 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700357 return self._run('rootdev %s' % options).stdout.strip()
358
359
360 def get_kernel_state(self):
361 """Returns the (<active>, <inactive>) kernel state as a pair."""
362 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0])
363 if active_root == self.KERNEL_A['root']:
364 return self.KERNEL_A, self.KERNEL_B
365 elif active_root == self.KERNEL_B['root']:
366 return self.KERNEL_B, self.KERNEL_A
367 else:
Dale Curtis5c32c722011-05-04 19:24:23 -0700368 raise ChromiumOSError('Encountered unknown root partition: %s' %
Dale Curtisa94c19c2011-05-02 15:05:17 -0700369 active_root)
370
371
372 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
373 """Return numeric cgpt value for the specified flag, kernel, device. """
374 return int(self._run('cgpt show -n -i %d %s %s' % (
375 kernel['kernel'], flag, dev)).stdout.strip())
376
377
378 def get_kernel_priority(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800379 """Return numeric priority for the specified kernel.
380
381 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
382
383 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700384 return self._cgpt('-P', kernel)
385
386
387 def get_kernel_success(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800388 """Return boolean success flag for the specified kernel.
389
390 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
391
392 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700393 return self._cgpt('-S', kernel) != 0
394
395
396 def get_kernel_tries(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800397 """Return tries count for the specified kernel.
398
399 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
400
401 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700402 return self._cgpt('-T', kernel)
Sean O267c00b2010-08-31 15:54:55 +0200403
404
Chris Sosa5e4246b2012-05-22 18:05:22 -0700405 def get_stateful_update_script(self):
406 """Returns the path to the stateful update script on the target."""
Chris Sosaa3ac2152012-05-23 22:23:13 -0700407 # We attempt to load the local stateful update path in 3 different
408 # ways. First we use the location specified in the autotest global
409 # config. If this doesn't exist, we attempt to use the Chromium OS
410 # Chroot path to the installed script. If all else fails, we use the
411 # stateful update script on the host.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700412 stateful_update_path = os.path.join(
413 global_config.global_config.get_config_value(
414 'CROS', 'source_tree', default=''),
415 LOCAL_STATEFUL_UPDATE_PATH)
416
Chris Sosaa3ac2152012-05-23 22:23:13 -0700417 if not os.path.exists(stateful_update_path):
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700418 logging.warning('Could not find Chrome OS source location for '
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700419 'stateful_update script at %s, falling back to '
420 'chroot copy.', stateful_update_path)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700421 stateful_update_path = LOCAL_CHROOT_STATEFUL_UPDATE_PATH
422
423 if not os.path.exists(stateful_update_path):
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700424 logging.warning('Could not chroot stateful_update script, falling '
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700425 'back on client copy.')
Gilad Arnold0c0df732015-09-21 06:37:59 -0700426 statefuldev_script = self.REMOTE_STATEUL_UPDATE_PATH
Chris Sosaa3ac2152012-05-23 22:23:13 -0700427 else:
Chris Sosa5e4246b2012-05-22 18:05:22 -0700428 self.host.send_file(
Gilad Arnold0c0df732015-09-21 06:37:59 -0700429 stateful_update_path, self.STATEFUL_UPDATE,
430 delete_dest=True)
431 statefuldev_script = self.STATEFUL_UPDATE
Chris Sosa5e4246b2012-05-22 18:05:22 -0700432
433 return statefuldev_script
434
435
436 def reset_stateful_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800437 """Clear any pending stateful update request."""
Chris Sosa5e4246b2012-05-22 18:05:22 -0700438 statefuldev_cmd = [self.get_stateful_update_script()]
439 statefuldev_cmd += ['--stateful_change=reset', '2>&1']
Chris Sosa66d74072013-09-19 11:21:29 -0700440 self._run(' '.join(statefuldev_cmd))
Chris Sosa5e4246b2012-05-22 18:05:22 -0700441
442
Sean O267c00b2010-08-31 15:54:55 +0200443 def revert_boot_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800444 """Revert the boot partition."""
Dale Curtisd9b26b92011-10-24 13:34:46 -0700445 part = self.rootdev('-s')
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700446 logging.warning('Reverting update; Boot partition will be %s', part)
Sean O267c00b2010-08-31 15:54:55 +0200447 return self._run('/postinst %s 2>&1' % part)
448
449
Chris Sosac1932172013-10-16 13:28:53 -0700450 def rollback_rootfs(self, powerwash):
451 """Triggers rollback and waits for it to complete.
452
453 @param powerwash: If true, powerwash as part of rollback.
454
455 @raise RootFSUpdateError if anything went wrong.
456
457 """
Dan Shi549fb822015-03-24 18:01:11 -0700458 version = self.host.get_release_version()
Chris Sosac8617522014-06-09 23:22:26 +0000459 # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches
460 # X.Y.Z. This version split just pulls the first part out.
461 try:
462 build_number = int(version.split('.')[0])
463 except ValueError:
464 logging.error('Could not parse build number.')
465 build_number = 0
466
467 if build_number >= 5772:
Gilad Arnold0c0df732015-09-21 06:37:59 -0700468 can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN
Chris Sosac8617522014-06-09 23:22:26 +0000469 logging.info('Checking for rollback.')
470 try:
471 self._run(can_rollback_cmd)
472 except error.AutoservRunError as e:
473 raise RootFSUpdateError("Rollback isn't possible on %s: %s" %
474 (self.host.hostname, str(e)))
475
Gilad Arnold0c0df732015-09-21 06:37:59 -0700476 rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN
Chris Sosac1932172013-10-16 13:28:53 -0700477 if not powerwash:
Dan Shif3a35f72016-01-25 11:18:14 -0800478 rollback_cmd += ' --nopowerwash'
Chris Sosac1932172013-10-16 13:28:53 -0700479
Chris Sosac8617522014-06-09 23:22:26 +0000480 logging.info('Performing rollback.')
Chris Sosac1932172013-10-16 13:28:53 -0700481 try:
482 self._run(rollback_cmd)
Chris Sosac1932172013-10-16 13:28:53 -0700483 except error.AutoservRunError as e:
484 raise RootFSUpdateError('Rollback failed on %s: %s' %
485 (self.host.hostname, str(e)))
486
487 self._verify_update_completed()
488
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800489
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700490 # TODO(garnold) This is here for backward compatibility and should be
491 # deprecated once we shift to using update_image() everywhere.
Chris Sosa2f1ae9f2013-08-13 10:00:15 -0700492 def update_rootfs(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700493 """Run the standard command to force an update."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700494 return self.update_image()
Dale Curtis5c32c722011-05-04 19:24:23 -0700495
496
Chris Sosa72312602013-04-16 15:01:56 -0700497 def update_stateful(self, clobber=True):
498 """Updates the stateful partition.
499
500 @param clobber: If True, a clean stateful installation.
501 """
Chris Sosa77556d82012-04-05 15:23:14 -0700502 logging.info('Updating stateful partition...')
joychen03eaad92013-06-26 09:55:21 -0700503 statefuldev_url = self.update_url.replace('update',
504 'static')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700505
Dale Curtis5c32c722011-05-04 19:24:23 -0700506 # Attempt stateful partition update; this must succeed so that the newly
507 # installed host is testable after update.
Chris Sosa72312602013-04-16 15:01:56 -0700508 statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url]
509 if clobber:
510 statefuldev_cmd.append('--stateful_change=clean')
511
512 statefuldev_cmd.append('2>&1')
Dale Curtis5c32c722011-05-04 19:24:23 -0700513 try:
Dan Shi205b8732016-01-25 10:56:22 -0800514 self._run(' '.join(statefuldev_cmd), timeout=1200)
Dale Curtis5c32c722011-05-04 19:24:23 -0700515 except error.AutoservRunError:
Gilad Arnold62cf3a42015-10-01 09:15:25 -0700516 update_error = StatefulUpdateError(
517 'Failed to perform stateful update on %s' %
518 self.host.hostname)
Chris Sosa77556d82012-04-05 15:23:14 -0700519 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700520
Chris Sosae92399e2015-04-24 11:32:59 -0700521 def run_update(self, update_root=True):
Dan Shi0f466e82013-02-22 15:44:58 -0800522 """Update the DUT with image of specific version.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700523
Chris Sosae92399e2015-04-24 11:32:59 -0700524 @param update_root: True to force a rootfs update.
Dan Shi0f466e82013-02-22 15:44:58 -0800525 """
Dan Shi549fb822015-03-24 18:01:11 -0700526 booted_version = self.host.get_release_version()
Chris Sosaa3ac2152012-05-23 22:23:13 -0700527 if self.update_version:
528 logging.info('Updating from version %s to %s.',
529 booted_version, self.update_version)
Dale Curtis53d55862011-05-16 12:17:59 -0700530
Dale Curtis5c32c722011-05-04 19:24:23 -0700531 # Check that Dev Server is accepting connections (from autoserv's host).
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200532 # If we can't talk to it, the machine host probably can't either.
xixuanccf2e722016-06-10 16:42:38 -0700533 auserver_host = 'http://%s' % urlparse.urlparse(self.update_url)[1]
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200534 try:
xixuanccf2e722016-06-10 16:42:38 -0700535 if not dev_server.ImageServer.devserver_healthy(auserver_host):
536 raise ChromiumOSError(
537 'Update server at %s not healthy' % auserver_host)
538 except Exception as e:
539 logging.debug('Error happens in connection to devserver: %r', e)
Dale Curtis5c32c722011-05-04 19:24:23 -0700540 raise ChromiumOSError(
541 'Update server at %s not available' % auserver_host)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200542
Chris Sosaa3ac2152012-05-23 22:23:13 -0700543 logging.info('Installing from %s to %s', self.update_url,
Chris Sosa77556d82012-04-05 15:23:14 -0700544 self.host.hostname)
545
Chris Sosa5e4246b2012-05-22 18:05:22 -0700546 # Reset update state.
Chris Sosa77556d82012-04-05 15:23:14 -0700547 self.reset_update_engine()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700548 self.reset_stateful_partition()
Sean Oc053dfe2010-08-23 18:22:26 +0200549
Dale Curtis1e973182011-07-12 18:21:36 -0700550 try:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700551 try:
552 if not update_root:
553 logging.info('Root update is skipped.')
554 else:
555 self.update_rootfs()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200556
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700557 self.update_stateful()
558 except:
Chris Sosa77556d82012-04-05 15:23:14 -0700559 self.revert_boot_partition()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700560 self.reset_stateful_partition()
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700561 raise
Sean Oc053dfe2010-08-23 18:22:26 +0200562
Dale Curtis1e973182011-07-12 18:21:36 -0700563 logging.info('Update complete.')
Dale Curtis1e973182011-07-12 18:21:36 -0700564 except:
565 # Collect update engine logs in the event of failure.
566 if self.host.job:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700567 logging.info('Collecting update engine logs due to failure...')
Dale Curtis1e973182011-07-12 18:21:36 -0700568 self.host.get_file(
Gilad Arnold0c0df732015-09-21 06:37:59 -0700569 self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
570 preserve_perm=False)
Prashanth B32baa9b2014-03-13 13:23:01 -0700571 list_image_dir_contents(self.update_url)
Dale Curtis1e973182011-07-12 18:21:36 -0700572 raise
Dan Shi10e992b2013-08-30 11:02:59 -0700573 finally:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800574 logging.info('Update engine log has downloaded in '
575 'sysinfo/update_engine dir. Check the lastest.')
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200576
577
Dale Curtisa94c19c2011-05-02 15:05:17 -0700578 def check_version(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800579 """Check the image running in DUT has the desired version.
580
581 @returns: True if the DUT's image version matches the version that
582 the autoupdater tries to update to.
583
584 """
Dan Shi549fb822015-03-24 18:01:11 -0700585 booted_version = self.host.get_release_version()
Dan Shib95bb862013-03-22 16:29:28 -0700586 return (self.update_version and
587 self.update_version.endswith(booted_version))
588
589
590 def check_version_to_confirm_install(self):
591 """Check image running in DUT has the desired version to be installed.
592
593 The method should not be used to check if DUT needs to have a full
594 reimage. Only use it to confirm a image is installed.
595
Dan Shi549fb822015-03-24 18:01:11 -0700596 The method is designed to verify version for following 6 scenarios with
Dan Shi190c7802013-04-04 13:05:30 -0700597 samples of version to update to and expected booted version:
598 1. trybot paladin build.
599 update version: trybot-lumpy-paladin/R27-3837.0.0-b123
600 booted version: 3837.0.2013_03_21_1340
601
602 2. trybot release build.
603 update version: trybot-lumpy-release/R27-3837.0.0-b456
604 booted version: 3837.0.0
605
606 3. buildbot official release build.
607 update version: lumpy-release/R27-3837.0.0
608 booted version: 3837.0.0
609
610 4. non-official paladin rc build.
611 update version: lumpy-paladin/R27-3878.0.0-rc7
612 booted version: 3837.0.0-rc7
Dan Shib95bb862013-03-22 16:29:28 -0700613
Dan Shi7f795512013-04-12 10:08:17 -0700614 5. chrome-perf build.
615 update version: lumpy-chrome-perf/R28-3837.0.0-b2996
616 booted version: 3837.0.0
617
Dan Shi73aa2902013-05-03 11:22:11 -0700618 6. pgo-generate build.
619 update version: lumpy-release-pgo-generate/R28-3837.0.0-b2996
620 booted version: 3837.0.0-pgo-generate
621
Dan Shib95bb862013-03-22 16:29:28 -0700622 When we are checking if a DUT needs to do a full install, we should NOT
623 use this method to check if the DUT is running the same version, since
Dan Shi190c7802013-04-04 13:05:30 -0700624 it may return false positive for a DUT running trybot paladin build to
625 be updated to another trybot paladin build.
Dan Shib95bb862013-03-22 16:29:28 -0700626
Dan Shi190c7802013-04-04 13:05:30 -0700627 TODO: This logic has a bug if a trybot paladin build failed to be
628 installed in a DUT running an older trybot paladin build with same
629 platform number, but different build number (-b###). So to conclusively
630 determine if a tryjob paladin build is imaged successfully, we may need
631 to find out the date string from update url.
Dan Shib95bb862013-03-22 16:29:28 -0700632
633 @returns: True if the DUT's image version (without the date string if
634 the image is a trybot build), matches the version that the
635 autoupdater is trying to update to.
636
637 """
J. Richard Barnetteec1de422013-06-26 15:44:07 -0700638 # In the local_devserver case, we can't know the expected
639 # build, so just pass.
640 if not self.update_version:
641 return True
642
Dan Shib95bb862013-03-22 16:29:28 -0700643 # Always try the default check_version method first, this prevents
644 # any backward compatibility issue.
645 if self.check_version():
646 return True
647
Dan Shi549fb822015-03-24 18:01:11 -0700648 return utils.version_match(self.update_version,
649 self.host.get_release_version(),
650 self.update_url)
Chris Sosa65425082013-10-16 13:26:22 -0700651
652
653 def verify_boot_expectations(self, expected_kernel_state, rollback_message):
654 """Verifies that we fully booted given expected kernel state.
655
656 This method both verifies that we booted using the correct kernel
657 state and that the OS has marked the kernel as good.
658
659 @param expected_kernel_state: kernel state that we are verifying with
660 i.e. I expect to be booted onto partition 4 etc. See output of
661 get_kernel_state.
662 @param rollback_message: string to raise as a ChromiumOSError
663 if we booted with the wrong partition.
664
665 @raises ChromiumOSError: If we didn't.
666 """
667 # Figure out the newly active kernel.
668 active_kernel_state = self.get_kernel_state()[0]
669
670 # Check for rollback due to a bad build.
671 if (expected_kernel_state and
672 active_kernel_state != expected_kernel_state):
Don Garrett56b1cc82013-12-06 17:49:20 -0800673
674 # Kernel crash reports should be wiped between test runs, but
675 # may persist from earlier parts of the test, or from problems
676 # with provisioning.
677 #
678 # Kernel crash reports will NOT be present if the crash happened
679 # before encrypted stateful is mounted.
680 #
681 # TODO(dgarrett): Integrate with server/crashcollect.py at some
682 # point.
683 kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash')
684 if kernel_crashes:
685 rollback_message += ': kernel_crash'
686 logging.debug('Found %d kernel crash reports:',
687 len(kernel_crashes))
688 # The crash names contain timestamps that may be useful:
689 # kernel.20131207.005945.0.kcrash
690 for crash in kernel_crashes:
Dan Shi0942b1d2015-03-31 11:07:00 -0700691 logging.debug(' %s', os.path.basename(crash))
Don Garrett56b1cc82013-12-06 17:49:20 -0800692
Chris Sosa65425082013-10-16 13:26:22 -0700693 # Print out some information to make it easier to debug
694 # the rollback.
695 logging.debug('Dumping partition table.')
696 self._run('cgpt show $(rootdev -s -d)')
697 logging.debug('Dumping crossystem for firmware debugging.')
698 self._run('crossystem --all')
699 raise ChromiumOSError(rollback_message)
700
701 # Make sure chromeos-setgoodkernel runs.
702 try:
703 utils.poll_for_condition(
704 lambda: (self.get_kernel_tries(active_kernel_state) == 0
705 and self.get_kernel_success(active_kernel_state)),
706 exception=ChromiumOSError(),
707 timeout=self.KERNEL_UPDATE_TIMEOUT, sleep_interval=5)
708 except ChromiumOSError:
709 services_status = self._run('status system-services').stdout
710 if services_status != 'system-services start/running\n':
711 event = ('Chrome failed to reach login screen')
712 else:
713 event = ('update-engine failed to call '
714 'chromeos-setgoodkernel')
715 raise ChromiumOSError(
716 'After update and reboot, %s '
717 'within %d seconds' % (event,
718 self.KERNEL_UPDATE_TIMEOUT))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700719
720
721class BrilloUpdater(BaseUpdater):
722 """Helper class for updating a Brillo DUT."""
723
724 def __init__(self, update_url, host=None):
725 """Initialize the object.
726
727 @param update_url: The URL we want the update to use.
728 @param host: A client.common_lib.hosts.Host implementation.
729 """
Gilad Arnoldecf894b2015-10-07 08:48:22 -0700730 super(BrilloUpdater, self).__init__(
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700731 '/system/bin/update_engine_client', update_url, host)