blob: 2c1e78306531dfa488157245cdd0f75b69752bf6 [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 httplib
7import logging
Chris Sosa77556d82012-04-05 15:23:14 -07008import multiprocessing
Dale Curtis5c32c722011-05-04 19:24:23 -07009import os
Sean O'Connor5346e4e2010-08-12 18:49:24 +020010import re
Sean O'Connor5346e4e2010-08-12 18:49:24 +020011import urlparse
Prashanth B32baa9b2014-03-13 13:23:01 -070012import urllib2
Sean O'Connor5346e4e2010-08-12 18:49:24 +020013
Chris Sosa65425082013-10-16 13:26:22 -070014from autotest_lib.client.bin import utils
Dale Curtis5c32c722011-05-04 19:24:23 -070015from autotest_lib.client.common_lib import error, global_config
Prashanth B32baa9b2014-03-13 13:23:01 -070016from autotest_lib.client.common_lib.cros import dev_server
Dan Shif3a35f72016-01-25 11:18:14 -080017from autotest_lib.client.common_lib.cros.graphite import autotest_stats
18
Sean O'Connor5346e4e2010-08-12 18:49:24 +020019
Dale Curtis5c32c722011-05-04 19:24:23 -070020# Local stateful update path is relative to the CrOS source directory.
21LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update'
Chris Sosaa3ac2152012-05-23 22:23:13 -070022LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update'
Sean O'Connor5346e4e2010-08-12 18:49:24 +020023UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020024UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
beeps5e8c45a2013-12-17 22:05:11 -080025# A list of update engine client states that occur after an update is triggered.
26UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE',
27 'UPDATE_STATUS_UPDATE_AVAILABLE',
28 'UPDATE_STATUS_DOWNLOADING',
29 'UPDATE_STATUS_FINALIZING']
Sean O'Connor5346e4e2010-08-12 18:49:24 +020030
31class ChromiumOSError(error.InstallError):
32 """Generic error for ChromiumOS-specific exceptions."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -070033
34
35class BrilloError(error.InstallError):
36 """Generic error for Brillo-specific exceptions."""
Sean O'Connor5346e4e2010-08-12 18:49:24 +020037
38
Chris Sosa77556d82012-04-05 15:23:14 -070039class RootFSUpdateError(ChromiumOSError):
40 """Raised when the RootFS fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070041
42
43class StatefulUpdateError(ChromiumOSError):
44 """Raised when the stateful partition fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070045
46
Sean O'Connor5346e4e2010-08-12 18:49:24 +020047def url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -080048 """Return the version based on update_url.
49
50 @param update_url: url to the image to update to.
51
52 """
Dale Curtisddfdb942011-07-14 13:59:24 -070053 # The Chrome OS version is generally the last element in the URL. The only
54 # exception is delta update URLs, which are rooted under the version; e.g.,
55 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
56 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -070057 return re.sub('/au/.*', '',
58 urlparse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +020059
60
Scott Zawalskieadbf702013-03-14 09:23:06 -040061def url_to_image_name(update_url):
62 """Return the image name based on update_url.
63
64 From a URL like:
65 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
66 return lumpy-release/R27-3837.0.0
67
68 @param update_url: url to the image to update to.
69 @returns a string representing the image name in the update_url.
70
71 """
72 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
73
74
Prashanth B32baa9b2014-03-13 13:23:01 -070075def _get_devserver_build_from_update_url(update_url):
76 """Get the devserver and build from the update url.
77
78 @param update_url: The url for update.
79 Eg: http://devserver:port/update/build.
80
81 @return: A tuple of (devserver url, build) or None if the update_url
82 doesn't match the expected pattern.
83
84 @raises ValueError: If the update_url doesn't match the expected pattern.
85 @raises ValueError: If no global_config was found, or it doesn't contain an
86 image_url_pattern.
87 """
88 pattern = global_config.global_config.get_config_value(
89 'CROS', 'image_url_pattern', type=str, default='')
90 if not pattern:
91 raise ValueError('Cannot parse update_url, the global config needs '
92 'an image_url_pattern.')
93 re_pattern = pattern.replace('%s', '(\S+)')
94 parts = re.search(re_pattern, update_url)
95 if not parts or len(parts.groups()) < 2:
96 raise ValueError('%s is not an update url' % update_url)
97 return parts.groups()
98
99
100def list_image_dir_contents(update_url):
101 """Lists the contents of the devserver for a given build/update_url.
102
103 @param update_url: An update url. Eg: http://devserver:port/update/build.
104 """
105 if not update_url:
106 logging.warning('Need update_url to list contents of the devserver.')
107 return
108 error_msg = 'Cannot check contents of devserver, update url %s' % update_url
109 try:
110 devserver_url, build = _get_devserver_build_from_update_url(update_url)
111 except ValueError as e:
112 logging.warning('%s: %s', error_msg, e)
113 return
114 devserver = dev_server.ImageServer(devserver_url)
115 try:
116 devserver.list_image_dir(build)
117 # The devserver will retry on URLError to avoid flaky connections, but will
118 # eventually raise the URLError if it persists. All HTTPErrors get
119 # converted to DevServerExceptions.
120 except (dev_server.DevServerException, urllib2.URLError) as e:
121 logging.warning('%s: %s', error_msg, e)
122
123
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700124# TODO(garnold) This implements shared updater functionality needed for
125# supporting the autoupdate_EndToEnd server-side test. We should probably
126# migrate more of the existing ChromiumOSUpdater functionality to it as we
127# expand non-CrOS support in other tests.
128class BaseUpdater(object):
129 """Platform-agnostic DUT update functionality."""
130
131 def __init__(self, updater_ctrl_bin, update_url, host):
132 """Initializes the object.
133
134 @param updater_ctrl_bin: Path to update_engine_client.
135 @param update_url: The URL we want the update to use.
136 @param host: A client.common_lib.hosts.Host implementation.
137 """
138 self.updater_ctrl_bin = updater_ctrl_bin
139 self.update_url = update_url
140 self.host = host
141 self._update_error_queue = multiprocessing.Queue(2)
142
143
144 def check_update_status(self):
145 """Returns the current update engine state.
146
147 We use the `update_engine_client -status' command and parse the line
148 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE".
149 """
150 update_status = self.host.run(
151 '%s -status 2>&1 | grep CURRENT_OP' % self.updater_ctrl_bin)
152 return update_status.stdout.strip().split('=')[-1]
153
154
Shuqian Zhaod9992722016-02-29 12:26:38 -0800155 def get_last_update_error(self):
156 """Get the last autoupdate error code."""
157 error_msg = self.host.run(
158 '%s --last_attempt_error' % self.updater_ctrl_bin)
159 error_msg = (error_msg.stdout.strip()).replace('\n', ', ')
160 return error_msg
161
162
163 def _base_update_handler(self, run_args, err_msg_prefix=None):
164 """Base function to handle a remote update ssh call.
165
166 @param run_args: Dictionary of args passed to ssh_host.run function.
167 @param err_msg_prefix: Prefix of the exception error message.
168
169 @returns: The exception thrown, None if no exception.
170 """
171 to_raise = None
172 err_msg = err_msg_prefix
173 try:
174 self.host.run(**run_args)
175 except (error.AutoservSshPermissionDeniedError,
176 error.AutoservSSHTimeout) as e:
Shuqian Zhaoc2a15eb2016-03-31 14:03:27 -0700177 logging.exception(e)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800178 err_msg += 'SSH reports an error: %s' % type(e).__name__
179 to_raise = RootFSUpdateError(err_msg)
180 except error.AutoservRunError as e:
Shuqian Zhaoc2a15eb2016-03-31 14:03:27 -0700181 logging.exception(e)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800182 # Check if exit code is 255, if so it's probably a generic SSH error
183 result = e.args[1]
184 if result.exit_status == 255:
185 err_msg += ('SSH reports a generic error (255) which is '
186 'probably a lab network failure')
187 to_raise = RootFSUpdateError(err_msg)
188
189 # We have ruled out all SSH cases, the error code is from
190 # update_engine_client.
191 else:
192 list_image_dir_contents(self.update_url)
193 err_msg += ('Update failed. Returned update_engine error code: '
194 '%s. Reported error: %s' %
195 (self.get_last_update_error(), type(e).__name__))
Shuqian Zhaod9992722016-02-29 12:26:38 -0800196 to_raise = RootFSUpdateError(err_msg)
197 except Exception as e:
198 to_raise = e
199
200 return to_raise
201
202
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700203 def trigger_update(self):
204 """Triggers a background update.
205
Shuqian Zhaod9992722016-02-29 12:26:38 -0800206 @raise RootFSUpdateError or unknown Exception if anything went wrong.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700207 """
208 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' %
209 (self.updater_ctrl_bin, self.update_url))
Shuqian Zhaod9992722016-02-29 12:26:38 -0800210 run_args = {'command': autoupdate_cmd}
211 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700212 logging.info('Triggering update via: %s', autoupdate_cmd)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800213 to_raise = self._base_update_handler(run_args, err_prefix)
214 if to_raise:
215 raise to_raise
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700216
217
218 def _verify_update_completed(self):
219 """Verifies that an update has completed.
220
221 @raise RootFSUpdateError: if verification fails.
222 """
223 status = self.check_update_status()
224 if status != UPDATER_NEED_REBOOT:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800225 error_msg = ''
226 if status == UPDATER_IDLE:
227 error_msg = 'Update error: %s' % self.get_last_update_error()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700228 raise RootFSUpdateError('Update did not complete with correct '
Shuqian Zhaod9992722016-02-29 12:26:38 -0800229 'status. Expecting %s, actual %s. %s' %
230 (UPDATER_NEED_REBOOT, status, error_msg))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700231
232
233 def update_image(self):
234 """Updates the device image and verifies success."""
Shuqian Zhaod9992722016-02-29 12:26:38 -0800235 autoupdate_cmd = ('%s --update --omaha_url=%s 2>&1' %
236 (self.updater_ctrl_bin, self.update_url))
237 run_args = {'command': autoupdate_cmd, 'timeout': 3600}
238 err_prefix = ('Failed to install device image using payload at %s '
239 'on %s. ' % (self.update_url, self.host.hostname))
240 logging.info('Updating image via: %s', autoupdate_cmd)
241 to_raise = self._base_update_handler(run_args, err_prefix)
242 if to_raise:
243 self._update_error_queue.put(to_raise)
244 raise to_raise
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700245
246 try:
247 self._verify_update_completed()
248 except RootFSUpdateError as e:
249 self._update_error_queue.put(e)
250 raise
251
252
253class ChromiumOSUpdater(BaseUpdater):
Dan Shi0f466e82013-02-22 15:44:58 -0800254 """Helper class used to update DUT with image of desired version."""
Gilad Arnold0c0df732015-09-21 06:37:59 -0700255 REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update'
256 UPDATER_BIN = '/usr/bin/update_engine_client'
257 STATEFUL_UPDATE = '/tmp/stateful_update'
258 UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed'
259 UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine']
260
Dale Curtisa94c19c2011-05-02 15:05:17 -0700261 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
262 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
Chris Sosa65425082013-10-16 13:26:22 -0700263 # Time to wait for new kernel to be marked successful after
264 # auto update.
265 KERNEL_UPDATE_TIMEOUT = 120
Dale Curtisa94c19c2011-05-02 15:05:17 -0700266
Dan Shif3a35f72016-01-25 11:18:14 -0800267 _timer = autotest_stats.Timer('cros_autoupdater')
Dale Curtisa94c19c2011-05-02 15:05:17 -0700268
Chris Sosaa3ac2152012-05-23 22:23:13 -0700269 def __init__(self, update_url, host=None, local_devserver=False):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700270 super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url,
271 host)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700272 self.local_devserver = local_devserver
273 if not local_devserver:
Dan Shif3a35f72016-01-25 11:18:14 -0800274 self.update_version = url_to_version(update_url)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700275 else:
Dan Shif3a35f72016-01-25 11:18:14 -0800276 self.update_version = None
Sean Oc053dfe2010-08-23 18:22:26 +0200277
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700278
Sean Oc053dfe2010-08-23 18:22:26 +0200279 def reset_update_engine(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700280 """Resets the host to prepare for a clean update regardless of state."""
Gilad Arnold0c0df732015-09-21 06:37:59 -0700281 self._run('rm -f %s' % self.UPDATED_MARKER)
Chris Sosae92399e2015-04-24 11:32:59 -0700282 self._run('stop ui || true')
283 self._run('stop update-engine || true')
284 self._run('start update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -0700285
Sean Oc053dfe2010-08-23 18:22:26 +0200286 if self.check_update_status() != UPDATER_IDLE:
287 raise ChromiumOSError('%s is not in an installable state' %
288 self.host.hostname)
289
290
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200291 def _run(self, cmd, *args, **kwargs):
Dale Curtis5c32c722011-05-04 19:24:23 -0700292 """Abbreviated form of self.host.run(...)"""
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200293 return self.host.run(cmd, *args, **kwargs)
294
Sean Oc053dfe2010-08-23 18:22:26 +0200295
Dale Curtisa94c19c2011-05-02 15:05:17 -0700296 def rootdev(self, options=''):
Dan Shi0f466e82013-02-22 15:44:58 -0800297 """Returns the stripped output of rootdev <options>.
298
299 @param options: options to run rootdev.
300
301 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700302 return self._run('rootdev %s' % options).stdout.strip()
303
304
305 def get_kernel_state(self):
306 """Returns the (<active>, <inactive>) kernel state as a pair."""
307 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0])
308 if active_root == self.KERNEL_A['root']:
309 return self.KERNEL_A, self.KERNEL_B
310 elif active_root == self.KERNEL_B['root']:
311 return self.KERNEL_B, self.KERNEL_A
312 else:
Dale Curtis5c32c722011-05-04 19:24:23 -0700313 raise ChromiumOSError('Encountered unknown root partition: %s' %
Dale Curtisa94c19c2011-05-02 15:05:17 -0700314 active_root)
315
316
317 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
318 """Return numeric cgpt value for the specified flag, kernel, device. """
319 return int(self._run('cgpt show -n -i %d %s %s' % (
320 kernel['kernel'], flag, dev)).stdout.strip())
321
322
323 def get_kernel_priority(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800324 """Return numeric priority for the specified kernel.
325
326 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
327
328 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700329 return self._cgpt('-P', kernel)
330
331
332 def get_kernel_success(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800333 """Return boolean success flag for the specified kernel.
334
335 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
336
337 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700338 return self._cgpt('-S', kernel) != 0
339
340
341 def get_kernel_tries(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800342 """Return tries count for the specified kernel.
343
344 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
345
346 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700347 return self._cgpt('-T', kernel)
Sean O267c00b2010-08-31 15:54:55 +0200348
349
Chris Sosa5e4246b2012-05-22 18:05:22 -0700350 def get_stateful_update_script(self):
351 """Returns the path to the stateful update script on the target."""
Chris Sosaa3ac2152012-05-23 22:23:13 -0700352 # We attempt to load the local stateful update path in 3 different
353 # ways. First we use the location specified in the autotest global
354 # config. If this doesn't exist, we attempt to use the Chromium OS
355 # Chroot path to the installed script. If all else fails, we use the
356 # stateful update script on the host.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700357 stateful_update_path = os.path.join(
358 global_config.global_config.get_config_value(
359 'CROS', 'source_tree', default=''),
360 LOCAL_STATEFUL_UPDATE_PATH)
361
Chris Sosaa3ac2152012-05-23 22:23:13 -0700362 if not os.path.exists(stateful_update_path):
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700363 logging.warning('Could not find Chrome OS source location for '
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700364 'stateful_update script at %s, falling back to '
365 'chroot copy.', stateful_update_path)
Chris Sosaa3ac2152012-05-23 22:23:13 -0700366 stateful_update_path = LOCAL_CHROOT_STATEFUL_UPDATE_PATH
367
368 if not os.path.exists(stateful_update_path):
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700369 logging.warning('Could not chroot stateful_update script, falling '
Gilad Arnold5f2ff442015-09-21 07:06:40 -0700370 'back on client copy.')
Gilad Arnold0c0df732015-09-21 06:37:59 -0700371 statefuldev_script = self.REMOTE_STATEUL_UPDATE_PATH
Chris Sosaa3ac2152012-05-23 22:23:13 -0700372 else:
Chris Sosa5e4246b2012-05-22 18:05:22 -0700373 self.host.send_file(
Gilad Arnold0c0df732015-09-21 06:37:59 -0700374 stateful_update_path, self.STATEFUL_UPDATE,
375 delete_dest=True)
376 statefuldev_script = self.STATEFUL_UPDATE
Chris Sosa5e4246b2012-05-22 18:05:22 -0700377
378 return statefuldev_script
379
380
381 def reset_stateful_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800382 """Clear any pending stateful update request."""
Chris Sosa5e4246b2012-05-22 18:05:22 -0700383 statefuldev_cmd = [self.get_stateful_update_script()]
384 statefuldev_cmd += ['--stateful_change=reset', '2>&1']
Chris Sosa66d74072013-09-19 11:21:29 -0700385 self._run(' '.join(statefuldev_cmd))
Chris Sosa5e4246b2012-05-22 18:05:22 -0700386
387
Sean O267c00b2010-08-31 15:54:55 +0200388 def revert_boot_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800389 """Revert the boot partition."""
Dale Curtisd9b26b92011-10-24 13:34:46 -0700390 part = self.rootdev('-s')
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700391 logging.warning('Reverting update; Boot partition will be %s', part)
Sean O267c00b2010-08-31 15:54:55 +0200392 return self._run('/postinst %s 2>&1' % part)
393
394
Chris Sosac1932172013-10-16 13:28:53 -0700395 def rollback_rootfs(self, powerwash):
396 """Triggers rollback and waits for it to complete.
397
398 @param powerwash: If true, powerwash as part of rollback.
399
400 @raise RootFSUpdateError if anything went wrong.
401
402 """
Dan Shi549fb822015-03-24 18:01:11 -0700403 version = self.host.get_release_version()
Chris Sosac8617522014-06-09 23:22:26 +0000404 # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches
405 # X.Y.Z. This version split just pulls the first part out.
406 try:
407 build_number = int(version.split('.')[0])
408 except ValueError:
409 logging.error('Could not parse build number.')
410 build_number = 0
411
412 if build_number >= 5772:
Gilad Arnold0c0df732015-09-21 06:37:59 -0700413 can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN
Chris Sosac8617522014-06-09 23:22:26 +0000414 logging.info('Checking for rollback.')
415 try:
416 self._run(can_rollback_cmd)
417 except error.AutoservRunError as e:
418 raise RootFSUpdateError("Rollback isn't possible on %s: %s" %
419 (self.host.hostname, str(e)))
420
Gilad Arnold0c0df732015-09-21 06:37:59 -0700421 rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN
Chris Sosac1932172013-10-16 13:28:53 -0700422 if not powerwash:
Dan Shif3a35f72016-01-25 11:18:14 -0800423 rollback_cmd += ' --nopowerwash'
Chris Sosac1932172013-10-16 13:28:53 -0700424
Chris Sosac8617522014-06-09 23:22:26 +0000425 logging.info('Performing rollback.')
Chris Sosac1932172013-10-16 13:28:53 -0700426 try:
427 self._run(rollback_cmd)
Chris Sosac1932172013-10-16 13:28:53 -0700428 except error.AutoservRunError as e:
429 raise RootFSUpdateError('Rollback failed on %s: %s' %
430 (self.host.hostname, str(e)))
431
432 self._verify_update_completed()
433
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800434
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700435 # TODO(garnold) This is here for backward compatibility and should be
436 # deprecated once we shift to using update_image() everywhere.
Dan Shif3a35f72016-01-25 11:18:14 -0800437 @_timer.decorate
Chris Sosa2f1ae9f2013-08-13 10:00:15 -0700438 def update_rootfs(self):
Chris Sosae92399e2015-04-24 11:32:59 -0700439 """Run the standard command to force an update."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700440 return self.update_image()
Dale Curtis5c32c722011-05-04 19:24:23 -0700441
442
Dan Shif3a35f72016-01-25 11:18:14 -0800443 @_timer.decorate
Chris Sosa72312602013-04-16 15:01:56 -0700444 def update_stateful(self, clobber=True):
445 """Updates the stateful partition.
446
447 @param clobber: If True, a clean stateful installation.
448 """
Chris Sosa77556d82012-04-05 15:23:14 -0700449 logging.info('Updating stateful partition...')
joychen03eaad92013-06-26 09:55:21 -0700450 statefuldev_url = self.update_url.replace('update',
451 'static')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700452
Dale Curtis5c32c722011-05-04 19:24:23 -0700453 # Attempt stateful partition update; this must succeed so that the newly
454 # installed host is testable after update.
Chris Sosa72312602013-04-16 15:01:56 -0700455 statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url]
456 if clobber:
457 statefuldev_cmd.append('--stateful_change=clean')
458
459 statefuldev_cmd.append('2>&1')
Dale Curtis5c32c722011-05-04 19:24:23 -0700460 try:
Dan Shi205b8732016-01-25 10:56:22 -0800461 self._run(' '.join(statefuldev_cmd), timeout=1200)
Dale Curtis5c32c722011-05-04 19:24:23 -0700462 except error.AutoservRunError:
Gilad Arnold62cf3a42015-10-01 09:15:25 -0700463 update_error = StatefulUpdateError(
464 'Failed to perform stateful update on %s' %
465 self.host.hostname)
Chris Sosa77556d82012-04-05 15:23:14 -0700466 self._update_error_queue.put(update_error)
467 raise update_error
Simran Basi3b858a22015-03-17 16:23:24 -0700468 except Exception as e:
469 # Don't allow other exceptions to not be caught.
470 self._update_error_queue.put(e)
471 raise e
Dale Curtis5c32c722011-05-04 19:24:23 -0700472
473
Dan Shif3a35f72016-01-25 11:18:14 -0800474 @_timer.decorate
Chris Sosae92399e2015-04-24 11:32:59 -0700475 def run_update(self, update_root=True):
Dan Shi0f466e82013-02-22 15:44:58 -0800476 """Update the DUT with image of specific version.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700477
Chris Sosae92399e2015-04-24 11:32:59 -0700478 @param update_root: True to force a rootfs update.
Dan Shi0f466e82013-02-22 15:44:58 -0800479 """
Dan Shi549fb822015-03-24 18:01:11 -0700480 booted_version = self.host.get_release_version()
Chris Sosaa3ac2152012-05-23 22:23:13 -0700481 if self.update_version:
482 logging.info('Updating from version %s to %s.',
483 booted_version, self.update_version)
Dale Curtis53d55862011-05-16 12:17:59 -0700484
Dale Curtis5c32c722011-05-04 19:24:23 -0700485 # Check that Dev Server is accepting connections (from autoserv's host).
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200486 # If we can't talk to it, the machine host probably can't either.
487 auserver_host = urlparse.urlparse(self.update_url)[1]
488 try:
489 httplib.HTTPConnection(auserver_host).connect()
Dale Curtis5c32c722011-05-04 19:24:23 -0700490 except IOError:
491 raise ChromiumOSError(
492 'Update server at %s not available' % auserver_host)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200493
Chris Sosaa3ac2152012-05-23 22:23:13 -0700494 logging.info('Installing from %s to %s', self.update_url,
Chris Sosa77556d82012-04-05 15:23:14 -0700495 self.host.hostname)
496
Chris Sosa5e4246b2012-05-22 18:05:22 -0700497 # Reset update state.
Chris Sosa77556d82012-04-05 15:23:14 -0700498 self.reset_update_engine()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700499 self.reset_stateful_partition()
Sean Oc053dfe2010-08-23 18:22:26 +0200500
Dale Curtis1e973182011-07-12 18:21:36 -0700501 try:
Chris Sosa77556d82012-04-05 15:23:14 -0700502 updaters = [
Chris Sosa2f1ae9f2013-08-13 10:00:15 -0700503 multiprocessing.process.Process(target=self.update_rootfs),
Chris Sosa72312602013-04-16 15:01:56 -0700504 multiprocessing.process.Process(target=self.update_stateful)
Chris Sosa77556d82012-04-05 15:23:14 -0700505 ]
Dan Shi0f466e82013-02-22 15:44:58 -0800506 if not update_root:
507 logging.info('Root update is skipped.')
508 updaters = updaters[1:]
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200509
Chris Sosa77556d82012-04-05 15:23:14 -0700510 # Run the updaters in parallel.
511 for updater in updaters: updater.start()
512 for updater in updaters: updater.join()
513
514 # Re-raise the first error that occurred.
515 if not self._update_error_queue.empty():
516 update_error = self._update_error_queue.get()
517 self.revert_boot_partition()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700518 self.reset_stateful_partition()
Chris Sosa77556d82012-04-05 15:23:14 -0700519 raise update_error
Sean Oc053dfe2010-08-23 18:22:26 +0200520
Dale Curtis1e973182011-07-12 18:21:36 -0700521 logging.info('Update complete.')
Dale Curtis1e973182011-07-12 18:21:36 -0700522 except:
523 # Collect update engine logs in the event of failure.
524 if self.host.job:
525 logging.info('Collecting update engine logs...')
526 self.host.get_file(
Gilad Arnold0c0df732015-09-21 06:37:59 -0700527 self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
528 preserve_perm=False)
Prashanth B32baa9b2014-03-13 13:23:01 -0700529 list_image_dir_contents(self.update_url)
Dale Curtis1e973182011-07-12 18:21:36 -0700530 raise
Dan Shi10e992b2013-08-30 11:02:59 -0700531 finally:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800532 logging.info('Update engine log has downloaded in '
533 'sysinfo/update_engine dir. Check the lastest.')
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200534
535
Dale Curtisa94c19c2011-05-02 15:05:17 -0700536 def check_version(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800537 """Check the image running in DUT has the desired version.
538
539 @returns: True if the DUT's image version matches the version that
540 the autoupdater tries to update to.
541
542 """
Dan Shi549fb822015-03-24 18:01:11 -0700543 booted_version = self.host.get_release_version()
Dan Shib95bb862013-03-22 16:29:28 -0700544 return (self.update_version and
545 self.update_version.endswith(booted_version))
546
547
548 def check_version_to_confirm_install(self):
549 """Check image running in DUT has the desired version to be installed.
550
551 The method should not be used to check if DUT needs to have a full
552 reimage. Only use it to confirm a image is installed.
553
Dan Shi549fb822015-03-24 18:01:11 -0700554 The method is designed to verify version for following 6 scenarios with
Dan Shi190c7802013-04-04 13:05:30 -0700555 samples of version to update to and expected booted version:
556 1. trybot paladin build.
557 update version: trybot-lumpy-paladin/R27-3837.0.0-b123
558 booted version: 3837.0.2013_03_21_1340
559
560 2. trybot release build.
561 update version: trybot-lumpy-release/R27-3837.0.0-b456
562 booted version: 3837.0.0
563
564 3. buildbot official release build.
565 update version: lumpy-release/R27-3837.0.0
566 booted version: 3837.0.0
567
568 4. non-official paladin rc build.
569 update version: lumpy-paladin/R27-3878.0.0-rc7
570 booted version: 3837.0.0-rc7
Dan Shib95bb862013-03-22 16:29:28 -0700571
Dan Shi7f795512013-04-12 10:08:17 -0700572 5. chrome-perf build.
573 update version: lumpy-chrome-perf/R28-3837.0.0-b2996
574 booted version: 3837.0.0
575
Dan Shi73aa2902013-05-03 11:22:11 -0700576 6. pgo-generate build.
577 update version: lumpy-release-pgo-generate/R28-3837.0.0-b2996
578 booted version: 3837.0.0-pgo-generate
579
Dan Shib95bb862013-03-22 16:29:28 -0700580 When we are checking if a DUT needs to do a full install, we should NOT
581 use this method to check if the DUT is running the same version, since
Dan Shi190c7802013-04-04 13:05:30 -0700582 it may return false positive for a DUT running trybot paladin build to
583 be updated to another trybot paladin build.
Dan Shib95bb862013-03-22 16:29:28 -0700584
Dan Shi190c7802013-04-04 13:05:30 -0700585 TODO: This logic has a bug if a trybot paladin build failed to be
586 installed in a DUT running an older trybot paladin build with same
587 platform number, but different build number (-b###). So to conclusively
588 determine if a tryjob paladin build is imaged successfully, we may need
589 to find out the date string from update url.
Dan Shib95bb862013-03-22 16:29:28 -0700590
591 @returns: True if the DUT's image version (without the date string if
592 the image is a trybot build), matches the version that the
593 autoupdater is trying to update to.
594
595 """
J. Richard Barnetteec1de422013-06-26 15:44:07 -0700596 # In the local_devserver case, we can't know the expected
597 # build, so just pass.
598 if not self.update_version:
599 return True
600
Dan Shib95bb862013-03-22 16:29:28 -0700601 # Always try the default check_version method first, this prevents
602 # any backward compatibility issue.
603 if self.check_version():
604 return True
605
Dan Shi549fb822015-03-24 18:01:11 -0700606 return utils.version_match(self.update_version,
607 self.host.get_release_version(),
608 self.update_url)
Chris Sosa65425082013-10-16 13:26:22 -0700609
610
611 def verify_boot_expectations(self, expected_kernel_state, rollback_message):
612 """Verifies that we fully booted given expected kernel state.
613
614 This method both verifies that we booted using the correct kernel
615 state and that the OS has marked the kernel as good.
616
617 @param expected_kernel_state: kernel state that we are verifying with
618 i.e. I expect to be booted onto partition 4 etc. See output of
619 get_kernel_state.
620 @param rollback_message: string to raise as a ChromiumOSError
621 if we booted with the wrong partition.
622
623 @raises ChromiumOSError: If we didn't.
624 """
625 # Figure out the newly active kernel.
626 active_kernel_state = self.get_kernel_state()[0]
627
628 # Check for rollback due to a bad build.
629 if (expected_kernel_state and
630 active_kernel_state != expected_kernel_state):
Don Garrett56b1cc82013-12-06 17:49:20 -0800631
632 # Kernel crash reports should be wiped between test runs, but
633 # may persist from earlier parts of the test, or from problems
634 # with provisioning.
635 #
636 # Kernel crash reports will NOT be present if the crash happened
637 # before encrypted stateful is mounted.
638 #
639 # TODO(dgarrett): Integrate with server/crashcollect.py at some
640 # point.
641 kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash')
642 if kernel_crashes:
643 rollback_message += ': kernel_crash'
644 logging.debug('Found %d kernel crash reports:',
645 len(kernel_crashes))
646 # The crash names contain timestamps that may be useful:
647 # kernel.20131207.005945.0.kcrash
648 for crash in kernel_crashes:
Dan Shi0942b1d2015-03-31 11:07:00 -0700649 logging.debug(' %s', os.path.basename(crash))
Don Garrett56b1cc82013-12-06 17:49:20 -0800650
Chris Sosa65425082013-10-16 13:26:22 -0700651 # Print out some information to make it easier to debug
652 # the rollback.
653 logging.debug('Dumping partition table.')
654 self._run('cgpt show $(rootdev -s -d)')
655 logging.debug('Dumping crossystem for firmware debugging.')
656 self._run('crossystem --all')
657 raise ChromiumOSError(rollback_message)
658
659 # Make sure chromeos-setgoodkernel runs.
660 try:
661 utils.poll_for_condition(
662 lambda: (self.get_kernel_tries(active_kernel_state) == 0
663 and self.get_kernel_success(active_kernel_state)),
664 exception=ChromiumOSError(),
665 timeout=self.KERNEL_UPDATE_TIMEOUT, sleep_interval=5)
666 except ChromiumOSError:
667 services_status = self._run('status system-services').stdout
668 if services_status != 'system-services start/running\n':
669 event = ('Chrome failed to reach login screen')
670 else:
671 event = ('update-engine failed to call '
672 'chromeos-setgoodkernel')
673 raise ChromiumOSError(
674 'After update and reboot, %s '
675 'within %d seconds' % (event,
676 self.KERNEL_UPDATE_TIMEOUT))
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700677
678
679class BrilloUpdater(BaseUpdater):
680 """Helper class for updating a Brillo DUT."""
681
682 def __init__(self, update_url, host=None):
683 """Initialize the object.
684
685 @param update_url: The URL we want the update to use.
686 @param host: A client.common_lib.hosts.Host implementation.
687 """
Gilad Arnoldecf894b2015-10-07 08:48:22 -0700688 super(BrilloUpdater, self).__init__(
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700689 '/system/bin/update_engine_client', update_url, host)