blob: b232f53d502f3fc9af14d2e53c20aa3a00395b89 [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
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -07005from __future__ import print_function
6
Sean O'Connor5346e4e2010-08-12 18:49:24 +02007import logging
8import re
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -07009import six
Congbin Guo63ae0302019-08-12 16:37:49 -070010import sys
Prashanth B32baa9b2014-03-13 13:23:01 -070011import urllib2
Richard Barnette0beb14b2018-05-15 18:07:52 +000012import urlparse
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
David Haddock77b75c32020-05-14 01:56:32 -070017from autotest_lib.client.common_lib.cros import kernel_utils
Richard Barnette0beb14b2018-05-15 18:07:52 +000018from autotest_lib.server import autotest
Shelley Chen61d28982016-10-28 09:40:20 -070019from autotest_lib.server import utils as server_utils
Richard Barnette0beb14b2018-05-15 18:07:52 +000020from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
21from autotest_lib.server.cros.dynamic_suite import tools
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -080022from chromite.lib import retry_util
Dan Shif3a35f72016-01-25 11:18:14 -080023
Shelley Chen16b8df32016-10-27 16:24:21 -070024try:
25 from chromite.lib import metrics
Dan Shi5e2efb72017-02-07 11:40:23 -080026except ImportError:
27 metrics = utils.metrics_mock
Sean O'Connor5346e4e2010-08-12 18:49:24 +020028
Gwendal Grignou3e96cc22017-06-07 16:22:51 -070029
Richard Barnette621a8e42018-06-25 17:34:11 -070030def _metric_name(base_name):
31 return 'chromeos/autotest/provision/' + base_name
32
33
Dale Curtis5c32c722011-05-04 19:24:23 -070034# Local stateful update path is relative to the CrOS source directory.
Sean O'Connor5346e4e2010-08-12 18:49:24 +020035UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020036UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
beeps5e8c45a2013-12-17 22:05:11 -080037# A list of update engine client states that occur after an update is triggered.
Garry Wangcd769872019-06-07 16:04:17 -070038UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FOR_UPDATE',
beeps5e8c45a2013-12-17 22:05:11 -080039 'UPDATE_STATUS_UPDATE_AVAILABLE',
40 'UPDATE_STATUS_DOWNLOADING',
Garry Wangcd769872019-06-07 16:04:17 -070041 'UPDATE_STATUS_FINALIZING',
42 'UPDATE_STATUS_VERIFYING',
43 'UPDATE_STATUS_REPORTING_ERROR_EVENT',
44 'UPDATE_STATUS_ATTEMPTING_ROLLBACK']
Sean O'Connor5346e4e2010-08-12 18:49:24 +020045
Richard Barnette0beb14b2018-05-15 18:07:52 +000046
Richard Barnette3e8b2282018-05-15 20:42:20 +000047_STATEFUL_UPDATE_SCRIPT = 'stateful_update'
Richard Barnettee86b1ce2018-06-07 10:37:23 -070048_QUICK_PROVISION_SCRIPT = 'quick-provision'
Richard Barnette3e8b2282018-05-15 20:42:20 +000049
50_UPDATER_BIN = '/usr/bin/update_engine_client'
51_UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine']
52
Richard Barnette0beb14b2018-05-15 18:07:52 +000053# PROVISION_FAILED - A flag file to indicate provision failures. The
54# file is created at the start of any AU procedure (see
Richard Barnette9d43e562018-06-05 17:20:10 +000055# `ChromiumOSUpdater._prepare_host()`). The file's location in
Richard Barnette0beb14b2018-05-15 18:07:52 +000056# stateful means that on successul update it will be removed. Thus, if
57# this file exists, it indicates that we've tried and failed in a
58# previous attempt to update.
59PROVISION_FAILED = '/var/tmp/provision_failed'
60
61
Richard Barnette3e8b2282018-05-15 20:42:20 +000062# A flag file used to enable special handling in lab DUTs. Some
63# parts of the system in Chromium OS test images will behave in ways
64# convenient to the test lab when this file is present. Generally,
65# we create this immediately after any update completes.
66_LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
67
68
Richard Barnette3ef29a82018-06-28 13:52:54 -070069# _TARGET_VERSION - A file containing the new version to which we plan
70# to update. This file is used by the CrOS shutdown code to detect and
71# handle certain version downgrade cases. Specifically: Downgrading
72# may trigger an unwanted powerwash in the target build when the
73# following conditions are met:
74# * Source build is a v4.4 kernel with R69-10756.0.0 or later.
75# * Target build predates the R69-10756.0.0 cutoff.
76# When this file is present and indicates a downgrade, the OS shutdown
77# code on the DUT knows how to prevent the powerwash.
78_TARGET_VERSION = '/run/update_target_version'
79
80
Richard Barnette5adb6d42018-06-28 15:52:32 -070081# _REBOOT_FAILURE_MESSAGE - This is the standard message text returned
82# when the Host.reboot() method fails. The source of this text comes
83# from `wait_for_restart()` in client/common_lib/hosts/base_classes.py.
84
85_REBOOT_FAILURE_MESSAGE = 'Host did not return from reboot'
86
87
Congbin Guoeb7aa2d2019-07-15 16:10:44 -070088DEVSERVER_PORT = '8082'
89GS_CACHE_PORT = '8888'
90
91
Richard Barnette9d43e562018-06-05 17:20:10 +000092class RootFSUpdateError(error.TestFail):
Chris Sosa77556d82012-04-05 15:23:14 -070093 """Raised when the RootFS fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070094
95
Richard Barnette9d43e562018-06-05 17:20:10 +000096class StatefulUpdateError(error.TestFail):
Chris Sosa77556d82012-04-05 15:23:14 -070097 """Raised when the stateful partition fails to update."""
Chris Sosa77556d82012-04-05 15:23:14 -070098
99
Richard Barnette9d43e562018-06-05 17:20:10 +0000100class _AttributedUpdateError(error.TestFail):
101 """Update failure with an attributed cause."""
102
103 def __init__(self, attribution, msg):
104 super(_AttributedUpdateError, self).__init__(
105 '%s: %s' % (attribution, msg))
Richard Barnette5adb6d42018-06-28 15:52:32 -0700106 self._message = msg
107
108 def _classify(self):
109 for err_pattern, classification in self._CLASSIFIERS:
110 if re.match(err_pattern, self._message):
111 return classification
112 return None
113
114 @property
115 def failure_summary(self):
116 """Summarize this error for metrics reporting."""
117 classification = self._classify()
118 if classification:
119 return '%s: %s' % (self._SUMMARY, classification)
120 else:
121 return self._SUMMARY
Richard Barnette9d43e562018-06-05 17:20:10 +0000122
123
124class HostUpdateError(_AttributedUpdateError):
125 """Failure updating a DUT attributable to the DUT.
126
127 This class of exception should be raised when the most likely cause
128 of failure was a condition existing on the DUT prior to the update,
129 such as a hardware problem, or a bug in the software on the DUT.
130 """
131
Richard Barnette5adb6d42018-06-28 15:52:32 -0700132 DUT_DOWN = 'No answer to ssh'
133
134 _SUMMARY = 'DUT failed prior to update'
135 _CLASSIFIERS = [
136 (DUT_DOWN, DUT_DOWN),
137 (_REBOOT_FAILURE_MESSAGE, 'Reboot failed'),
138 ]
139
Richard Barnette9d43e562018-06-05 17:20:10 +0000140 def __init__(self, hostname, msg):
141 super(HostUpdateError, self).__init__(
142 'Error on %s prior to update' % hostname, msg)
143
144
145class DevServerError(_AttributedUpdateError):
146 """Failure updating a DUT attributable to the devserver.
147
148 This class of exception should be raised when the most likely cause
149 of failure was the devserver serving the target image for update.
150 """
151
Richard Barnette5adb6d42018-06-28 15:52:32 -0700152 _SUMMARY = 'Devserver failed prior to update'
153 _CLASSIFIERS = []
154
Richard Barnette9d43e562018-06-05 17:20:10 +0000155 def __init__(self, devserver, msg):
156 super(DevServerError, self).__init__(
157 'Devserver error on %s' % devserver, msg)
158
159
160class ImageInstallError(_AttributedUpdateError):
161 """Failure updating a DUT when installing from the devserver.
162
163 This class of exception should be raised when the target DUT fails
164 to download and install the target image from the devserver, and
165 either the devserver or the DUT might be at fault.
166 """
167
Richard Barnette5adb6d42018-06-28 15:52:32 -0700168 _SUMMARY = 'Image failed to download and install'
169 _CLASSIFIERS = []
170
Richard Barnette9d43e562018-06-05 17:20:10 +0000171 def __init__(self, hostname, devserver, msg):
172 super(ImageInstallError, self).__init__(
173 'Download and install failed from %s onto %s'
174 % (devserver, hostname), msg)
175
176
177class NewBuildUpdateError(_AttributedUpdateError):
178 """Failure updating a DUT attributable to the target build.
179
180 This class of exception should be raised when updating to a new
181 build fails, and the most likely cause of the failure is a bug in
182 the newly installed target build.
183 """
184
Richard Barnette5adb6d42018-06-28 15:52:32 -0700185 CHROME_FAILURE = 'Chrome failed to reach login screen'
186 UPDATE_ENGINE_FAILURE = ('update-engine failed to call '
187 'chromeos-setgoodkernel')
188 ROLLBACK_FAILURE = 'System rolled back to previous build'
189
190 _SUMMARY = 'New build failed'
191 _CLASSIFIERS = [
192 (CHROME_FAILURE, 'Chrome did not start'),
193 (UPDATE_ENGINE_FAILURE, 'update-engine did not start'),
194 (ROLLBACK_FAILURE, ROLLBACK_FAILURE),
195 ]
196
Richard Barnette9d43e562018-06-05 17:20:10 +0000197 def __init__(self, update_version, msg):
198 super(NewBuildUpdateError, self).__init__(
199 'Failure in build %s' % update_version, msg)
200
Richard Barnette621a8e42018-06-25 17:34:11 -0700201 @property
202 def failure_summary(self):
203 #pylint: disable=missing-docstring
204 return 'Build failed to work after installing'
205
Richard Barnette9d43e562018-06-05 17:20:10 +0000206
Richard Barnette3e8b2282018-05-15 20:42:20 +0000207def _url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -0800208 """Return the version based on update_url.
209
210 @param update_url: url to the image to update to.
211
212 """
Dale Curtisddfdb942011-07-14 13:59:24 -0700213 # The Chrome OS version is generally the last element in the URL. The only
214 # exception is delta update URLs, which are rooted under the version; e.g.,
215 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
216 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -0700217 return re.sub('/au/.*', '',
218 urlparse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200219
220
Scott Zawalskieadbf702013-03-14 09:23:06 -0400221def url_to_image_name(update_url):
222 """Return the image name based on update_url.
223
224 From a URL like:
225 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
226 return lumpy-release/R27-3837.0.0
227
228 @param update_url: url to the image to update to.
229 @returns a string representing the image name in the update_url.
230
231 """
Amin Hassanib04420b2020-07-08 18:46:11 +0000232 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
Scott Zawalskieadbf702013-03-14 09:23:06 -0400233
234
Richard Barnette4c81b972018-07-18 12:35:16 -0700235def get_update_failure_reason(exception):
236 """Convert an exception into a failure reason for metrics.
237
238 The passed in `exception` should be one raised by failure of
239 `ChromiumOSUpdater.run_update`. The returned string will describe
240 the failure. If the input exception value is not a truish value
241 the return value will be `None`.
242
243 The number of possible return strings is restricted to a limited
244 enumeration of values so that the string may be safely used in
245 Monarch metrics without worrying about cardinality of the range of
246 string values.
247
248 @param exception Exception to be converted to a failure reason.
249
250 @return A string suitable for use in Monarch metrics, or `None`.
251 """
252 if exception:
253 if isinstance(exception, _AttributedUpdateError):
254 return exception.failure_summary
255 else:
256 return 'Unknown Error: %s' % type(exception).__name__
257 return None
258
259
Prashanth B32baa9b2014-03-13 13:23:01 -0700260def _get_devserver_build_from_update_url(update_url):
261 """Get the devserver and build from the update url.
262
263 @param update_url: The url for update.
264 Eg: http://devserver:port/update/build.
265
266 @return: A tuple of (devserver url, build) or None if the update_url
267 doesn't match the expected pattern.
268
269 @raises ValueError: If the update_url doesn't match the expected pattern.
270 @raises ValueError: If no global_config was found, or it doesn't contain an
271 image_url_pattern.
272 """
273 pattern = global_config.global_config.get_config_value(
274 'CROS', 'image_url_pattern', type=str, default='')
275 if not pattern:
276 raise ValueError('Cannot parse update_url, the global config needs '
277 'an image_url_pattern.')
278 re_pattern = pattern.replace('%s', '(\S+)')
279 parts = re.search(re_pattern, update_url)
280 if not parts or len(parts.groups()) < 2:
281 raise ValueError('%s is not an update url' % update_url)
282 return parts.groups()
283
284
Richard Barnette3e8b2282018-05-15 20:42:20 +0000285def _list_image_dir_contents(update_url):
Prashanth B32baa9b2014-03-13 13:23:01 -0700286 """Lists the contents of the devserver for a given build/update_url.
287
288 @param update_url: An update url. Eg: http://devserver:port/update/build.
289 """
290 if not update_url:
291 logging.warning('Need update_url to list contents of the devserver.')
292 return
293 error_msg = 'Cannot check contents of devserver, update url %s' % update_url
294 try:
295 devserver_url, build = _get_devserver_build_from_update_url(update_url)
296 except ValueError as e:
297 logging.warning('%s: %s', error_msg, e)
298 return
299 devserver = dev_server.ImageServer(devserver_url)
300 try:
301 devserver.list_image_dir(build)
302 # The devserver will retry on URLError to avoid flaky connections, but will
303 # eventually raise the URLError if it persists. All HTTPErrors get
304 # converted to DevServerExceptions.
305 except (dev_server.DevServerException, urllib2.URLError) as e:
306 logging.warning('%s: %s', error_msg, e)
307
308
Richard Barnette621a8e42018-06-25 17:34:11 -0700309def _get_metric_fields(update_url):
310 """Return a dict of metric fields.
311
312 This is used for sending autoupdate metrics for the given update URL.
313
314 @param update_url Metrics fields will be calculated from this URL.
315 """
316 build_name = url_to_image_name(update_url)
317 try:
318 board, build_type, milestone, _ = server_utils.ParseBuildName(
319 build_name)
320 except server_utils.ParseBuildNameException:
321 logging.warning('Unable to parse build name %s for metrics. '
322 'Continuing anyway.', build_name)
323 board, build_type, milestone = ('', '', '')
324 return {
325 'dev_server': dev_server.get_resolved_hostname(update_url),
326 'board': board,
327 'build_type': build_type,
328 'milestone': milestone,
329 }
330
331
Richard Barnette3e8b2282018-05-15 20:42:20 +0000332class ChromiumOSUpdater(object):
333 """Chromium OS specific DUT update functionality."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700334
Richard Barnette60e759e2018-07-21 20:56:59 -0700335 def __init__(self, update_url, host=None, interactive=True,
Amin Hassanib04420b2020-07-08 18:46:11 +0000336 use_quick_provision=False):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700337 """Initializes the object.
338
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700339 @param update_url: The URL we want the update to use.
340 @param host: A client.common_lib.hosts.Host implementation.
David Haddock76a4c882017-12-13 18:50:09 -0800341 @param interactive: Bool whether we are doing an interactive update.
Richard Barnette60e759e2018-07-21 20:56:59 -0700342 @param use_quick_provision: Whether we should attempt to perform
343 the update using the quick-provision script.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700344 """
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700345 self.update_url = update_url
346 self.host = host
David Haddock76a4c882017-12-13 18:50:09 -0800347 self.interactive = interactive
Richard Barnette3e8b2282018-05-15 20:42:20 +0000348 self.update_version = _url_to_version(update_url)
Richard Barnette60e759e2018-07-21 20:56:59 -0700349 self._use_quick_provision = use_quick_provision
Amin Hassanib04420b2020-07-08 18:46:11 +0000350
Richard Barnette3e8b2282018-05-15 20:42:20 +0000351
352 def _run(self, cmd, *args, **kwargs):
353 """Abbreviated form of self.host.run(...)"""
354 return self.host.run(cmd, *args, **kwargs)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700355
356
357 def check_update_status(self):
358 """Returns the current update engine state.
359
360 We use the `update_engine_client -status' command and parse the line
361 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE".
362 """
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800363 update_status = self.host.run(command='%s -status | grep CURRENT_OP' %
Richard Barnette3e8b2282018-05-15 20:42:20 +0000364 _UPDATER_BIN)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700365 return update_status.stdout.strip().split('=')[-1]
366
367
Richard Barnette55d1af82018-05-22 23:40:14 +0000368 def _rootdev(self, options=''):
369 """Returns the stripped output of rootdev <options>.
370
371 @param options: options to run rootdev.
372
373 """
374 return self._run('rootdev %s' % options).stdout.strip()
375
376
Richard Barnette3e8b2282018-05-15 20:42:20 +0000377 def _get_last_update_error(self):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800378 """Get the last autoupdate error code."""
Richard Barnette3e8b2282018-05-15 20:42:20 +0000379 command_result = self._run(
380 '%s --last_attempt_error' % _UPDATER_BIN)
381 return command_result.stdout.strip().replace('\n', ', ')
Shuqian Zhaod9992722016-02-29 12:26:38 -0800382
383
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800384 def _base_update_handler_no_retry(self, run_args):
Shuqian Zhaod9992722016-02-29 12:26:38 -0800385 """Base function to handle a remote update ssh call.
386
387 @param run_args: Dictionary of args passed to ssh_host.run function.
Shuqian Zhaod9992722016-02-29 12:26:38 -0800388
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800389 @throws: intercepts and re-throws all exceptions
Shuqian Zhaod9992722016-02-29 12:26:38 -0800390 """
Shuqian Zhaod9992722016-02-29 12:26:38 -0800391 try:
392 self.host.run(**run_args)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800393 except Exception as e:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800394 logging.debug('exception in update handler: %s', e)
395 raise e
Shuqian Zhaod9992722016-02-29 12:26:38 -0800396
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800397
398 def _base_update_handler(self, run_args, err_msg_prefix=None):
399 """Handle a remote update ssh call, possibly with retries.
400
401 @param run_args: Dictionary of args passed to ssh_host.run function.
402 @param err_msg_prefix: Prefix of the exception error message.
403 """
404 def exception_handler(e):
405 """Examines exceptions and returns True if the update handler
406 should be retried.
407
408 @param e: the exception intercepted by the retry util.
409 """
410 return (isinstance(e, error.AutoservSSHTimeout) or
411 (isinstance(e, error.GenericHostRunError) and
412 hasattr(e, 'description') and
413 (re.search('ERROR_CODE=37', e.description) or
414 re.search('generic error .255.', e.description))))
415
416 try:
417 # Try the update twice (arg 2 is max_retry, not including the first
418 # call). Some exceptions may be caught by the retry handler.
419 retry_util.GenericRetry(exception_handler, 1,
420 self._base_update_handler_no_retry,
421 run_args)
422 except Exception as e:
423 message = err_msg_prefix + ': ' + str(e)
424 raise RootFSUpdateError(message)
Shuqian Zhaod9992722016-02-29 12:26:38 -0800425
426
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800427 def _wait_for_update_service(self):
428 """Ensure that the update engine daemon is running, possibly
429 by waiting for it a bit in case the DUT just rebooted and the
430 service hasn't started yet.
431 """
432 def handler(e):
433 """Retry exception handler.
434
435 Assumes that the error is due to the update service not having
436 started yet.
437
438 @param e: the exception intercepted by the retry util.
439 """
440 if isinstance(e, error.AutoservRunError):
441 logging.debug('update service check exception: %s\n'
442 'retrying...', e)
443 return True
444 else:
445 return False
446
447 # Retry at most three times, every 5s.
448 status = retry_util.GenericRetry(handler, 3,
449 self.check_update_status,
450 sleep=5)
451
452 # Expect the update engine to be idle.
453 if status != UPDATER_IDLE:
Richard Barnette9d43e562018-06-05 17:20:10 +0000454 raise RootFSUpdateError(
455 'Update engine status is %s (%s was expected).'
456 % (status, UPDATER_IDLE))
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800457
458
Richard Barnette55d1af82018-05-22 23:40:14 +0000459 def _reset_update_engine(self):
460 """Resets the host to prepare for a clean update regardless of state."""
461 self._run('stop ui || true')
462 self._run('stop update-engine || true')
463 self._run('start update-engine')
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800464 self._wait_for_update_service()
465
Richard Barnette55d1af82018-05-22 23:40:14 +0000466
467 def _reset_stateful_partition(self):
468 """Clear any pending stateful update request."""
Richard Barnette18fd5842018-05-25 18:21:14 +0000469 self._run('%s --stateful_change=reset 2>&1'
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700470 % self._get_stateful_update_script())
Richard Barnette3ef29a82018-06-28 13:52:54 -0700471 self._run('rm -f %s' % _TARGET_VERSION)
472
473
474 def _set_target_version(self):
475 """Set the "target version" for the update."""
Amin Hassanib04420b2020-07-08 18:46:11 +0000476 version_number = self.update_version.split('-')[1]
Richard Barnette3ef29a82018-06-28 13:52:54 -0700477 self._run('echo %s > %s' % (version_number, _TARGET_VERSION))
Richard Barnette55d1af82018-05-22 23:40:14 +0000478
479
480 def _revert_boot_partition(self):
481 """Revert the boot partition."""
482 part = self._rootdev('-s')
483 logging.warning('Reverting update; Boot partition will be %s', part)
484 return self._run('/postinst %s 2>&1' % part)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700485
486
487 def _verify_update_completed(self):
488 """Verifies that an update has completed.
489
Richard Barnette9d43e562018-06-05 17:20:10 +0000490 @raise RootFSUpdateError if the DUT doesn't indicate that
491 download is complete and the DUT is ready for reboot.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700492 """
493 status = self.check_update_status()
494 if status != UPDATER_NEED_REBOOT:
Shuqian Zhaod9992722016-02-29 12:26:38 -0800495 error_msg = ''
496 if status == UPDATER_IDLE:
Richard Barnette3e8b2282018-05-15 20:42:20 +0000497 error_msg = 'Update error: %s' % self._get_last_update_error()
Richard Barnette9d43e562018-06-05 17:20:10 +0000498 raise RootFSUpdateError(
499 'Update engine status is %s (%s was expected). %s'
500 % (status, UPDATER_NEED_REBOOT, error_msg))
David Haddock77b75c32020-05-14 01:56:32 -0700501 return kernel_utils.verify_kernel_state_after_update(self.host)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700502
503
Richard Barnette55d1af82018-05-22 23:40:14 +0000504 def trigger_update(self):
Richard Barnette9d43e562018-06-05 17:20:10 +0000505 """Triggers a background update."""
506 # If this function is called immediately after reboot (which it
507 # can be), there is no guarantee that the update engine is up
508 # and running yet, so wait for it.
Richard Barnette55d1af82018-05-22 23:40:14 +0000509 self._wait_for_update_service()
510
511 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' %
512 (_UPDATER_BIN, self.update_url))
513 run_args = {'command': autoupdate_cmd}
514 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname
515 logging.info('Triggering update via: %s', autoupdate_cmd)
516 metric_fields = {'success': False}
517 try:
518 self._base_update_handler(run_args, err_prefix)
519 metric_fields['success'] = True
520 finally:
521 c = metrics.Counter('chromeos/autotest/autoupdater/trigger')
Richard Barnette621a8e42018-06-25 17:34:11 -0700522 metric_fields.update(_get_metric_fields(self.update_url))
Richard Barnette55d1af82018-05-22 23:40:14 +0000523 c.increment(fields=metric_fields)
524
525
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700526 def update_image(self):
Richard Barnette18fd5842018-05-25 18:21:14 +0000527 """Updates the device root FS and kernel and verifies success."""
Shuqian Zhaofe4d62e2016-06-23 14:46:45 -0700528 autoupdate_cmd = ('%s --update --omaha_url=%s' %
Richard Barnette3e8b2282018-05-15 20:42:20 +0000529 (_UPDATER_BIN, self.update_url))
David Haddock76a4c882017-12-13 18:50:09 -0800530 if not self.interactive:
531 autoupdate_cmd = '%s --interactive=false' % autoupdate_cmd
Shuqian Zhaod9992722016-02-29 12:26:38 -0800532 run_args = {'command': autoupdate_cmd, 'timeout': 3600}
533 err_prefix = ('Failed to install device image using payload at %s '
534 'on %s. ' % (self.update_url, self.host.hostname))
535 logging.info('Updating image via: %s', autoupdate_cmd)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700536 metric_fields = {'success': False}
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800537 try:
Luigi Semenzatoe76d9f82016-11-21 11:15:10 -0800538 self._base_update_handler(run_args, err_prefix)
Allen Li1a5cc0a2017-06-20 14:08:59 -0700539 metric_fields['success'] = True
540 finally:
Allen Li1a5cc0a2017-06-20 14:08:59 -0700541 c = metrics.Counter('chromeos/autotest/autoupdater/update')
Richard Barnette621a8e42018-06-25 17:34:11 -0700542 metric_fields.update(_get_metric_fields(self.update_url))
Allen Li1a5cc0a2017-06-20 14:08:59 -0700543 c.increment(fields=metric_fields)
Richard Barnette4d211c92018-05-24 18:56:08 +0000544 return self._verify_update_completed()
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700545
546
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700547 def _get_remote_script(self, script_name):
548 """Ensure that `script_name` is present on the DUT.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700549
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700550 The given script (e.g. `stateful_update`) may be present in the
551 stateful partition under /usr/local/bin, or we may have to
552 download it from the devserver.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700553
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700554 Determine whether the script is present or must be downloaded
555 and download if necessary. Then, return a command fragment
556 sufficient to run the script from whereever it now lives on the
557 DUT.
Richard Barnette9d43e562018-06-05 17:20:10 +0000558
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700559 @param script_name The name of the script as expected in
560 /usr/local/bin and on the devserver.
561 @return A string with the command (minus arguments) that will
562 run the target script.
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700563 """
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700564 remote_script = '/usr/local/bin/%s' % script_name
565 if self.host.path_exists(remote_script):
566 return remote_script
Laurence Goodby06fb42c2020-02-29 17:14:42 -0800567 self.host.run('mkdir -p -m 1777 /usr/local/tmp')
568 remote_tmp_script = '/usr/local/tmp/%s' % script_name
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700569 server_name = urlparse.urlparse(self.update_url)[1]
570 script_url = 'http://%s/static/%s' % (server_name, script_name)
Dana Goyette353d1d92019-06-27 10:43:59 -0700571 fetch_script = 'curl -Ss -o %s %s && head -1 %s' % (
572 remote_tmp_script, script_url, remote_tmp_script)
Chris Sosa5e4246b2012-05-22 18:05:22 -0700573
Dana Goyette353d1d92019-06-27 10:43:59 -0700574 first_line = self._run(fetch_script).stdout.strip()
575
576 if first_line and first_line.startswith('#!'):
577 script_interpreter = first_line.lstrip('#!')
578 if script_interpreter:
579 return '%s %s' % (script_interpreter, remote_tmp_script)
580 return None
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700581
582 def _get_stateful_update_script(self):
583 """Returns a command to run the stateful update script.
584
585 Find `stateful_update` on the target or install it, as
586 necessary. If installation fails, raise an exception.
587
588 @raise StatefulUpdateError if the script can't be found or
589 installed.
590 @return A string that can be joined with arguments to run the
591 `stateful_update` command on the DUT.
592 """
593 script_command = self._get_remote_script(_STATEFUL_UPDATE_SCRIPT)
594 if not script_command:
595 raise StatefulUpdateError('Could not install %s on DUT'
Richard Barnette9d43e562018-06-05 17:20:10 +0000596 % _STATEFUL_UPDATE_SCRIPT)
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700597 return script_command
Chris Sosa5e4246b2012-05-22 18:05:22 -0700598
599
Chris Sosa72312602013-04-16 15:01:56 -0700600 def update_stateful(self, clobber=True):
601 """Updates the stateful partition.
602
603 @param clobber: If True, a clean stateful installation.
Richard Barnette9d43e562018-06-05 17:20:10 +0000604
605 @raise StatefulUpdateError if the update script fails to
606 complete successfully.
Chris Sosa72312602013-04-16 15:01:56 -0700607 """
Chris Sosa77556d82012-04-05 15:23:14 -0700608 logging.info('Updating stateful partition...')
Richard Barnette18fd5842018-05-25 18:21:14 +0000609 statefuldev_url = self.update_url.replace('update', 'static')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700610
Dale Curtis5c32c722011-05-04 19:24:23 -0700611 # Attempt stateful partition update; this must succeed so that the newly
612 # installed host is testable after update.
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700613 statefuldev_cmd = [self._get_stateful_update_script(), statefuldev_url]
Chris Sosa72312602013-04-16 15:01:56 -0700614 if clobber:
615 statefuldev_cmd.append('--stateful_change=clean')
616
617 statefuldev_cmd.append('2>&1')
Dale Curtis5c32c722011-05-04 19:24:23 -0700618 try:
Dan Shi205b8732016-01-25 10:56:22 -0800619 self._run(' '.join(statefuldev_cmd), timeout=1200)
Dale Curtis5c32c722011-05-04 19:24:23 -0700620 except error.AutoservRunError:
Richard Barnette18fd5842018-05-25 18:21:14 +0000621 raise StatefulUpdateError(
Gilad Arnold62cf3a42015-10-01 09:15:25 -0700622 'Failed to perform stateful update on %s' %
623 self.host.hostname)
Dale Curtis5c32c722011-05-04 19:24:23 -0700624
Chris Sosaa3ac2152012-05-23 22:23:13 -0700625
Richard Barnette14ee84c2018-05-18 20:23:42 +0000626 def _prepare_host(self):
627 """Make sure the target DUT is working and ready for update.
628
629 Initially, the target DUT's state is unknown. The DUT is
630 expected to be online, but we strive to be forgiving if Chrome
631 and/or the update engine aren't fully functional.
632 """
633 # Summary of work, and the rationale:
634 # 1. Reboot, because it's a good way to clear out problems.
635 # 2. Touch the PROVISION_FAILED file, to allow repair to detect
636 # failure later.
637 # 3. Run the hook for host class specific preparation.
638 # 4. Stop Chrome, because the system is designed to eventually
639 # reboot if Chrome is stuck in a crash loop.
640 # 5. Force `update-engine` to start, because if Chrome failed
641 # to start properly, the status of the `update-engine` job
642 # will be uncertain.
Richard Barnette5adb6d42018-06-28 15:52:32 -0700643 if not self.host.is_up():
644 raise HostUpdateError(self.host.hostname,
645 HostUpdateError.DUT_DOWN)
Richard Barnette14ee84c2018-05-18 20:23:42 +0000646 self._reset_stateful_partition()
647 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT)
648 self._run('touch %s' % PROVISION_FAILED)
649 self.host.prepare_for_update()
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700650 self._reset_update_engine()
Richard Barnette14ee84c2018-05-18 20:23:42 +0000651 logging.info('Updating from version %s to %s.',
652 self.host.get_release_version(),
653 self.update_version)
654
Amin Hassani95f86e02020-07-14 13:06:03 -0700655 def _install_via_update_engine(self, devserver_name, image_name):
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700656 """Install an updating using the production AU flow.
657
Amin Hassani95f86e02020-07-14 13:06:03 -0700658 This uses the standard AU flow and the `stateful_update` script to
659 download and install a root FS, kernel and stateful filesystem content.
660
661 @param devserver_name: The devserver name and port (optional).
662 @param image_name: The image to be installed.
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700663
664 @return The kernel expected to be booted next.
665 """
666 logging.info('Installing image using update_engine.')
Amin Hassani95f86e02020-07-14 13:06:03 -0700667 ds = dev_server.ImageServer('http://%s' % devserver_name)
668 try:
669 ds.stage_artifacts(image_name, ['full_payload', 'stateful',
670 'autotest_packages'])
671 except dev_server.DevServerException as e:
672 raise error.TestFail, str(e), sys.exc_info()[2]
673
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700674 expected_kernel = self.update_image()
675 self.update_stateful()
Richard Barnette3ef29a82018-06-28 13:52:54 -0700676 self._set_target_version()
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700677 return expected_kernel
678
679
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700680 def _quick_provision_with_gs_cache(self, provision_command, devserver_name,
681 image_name):
682 """Run quick_provision using GsCache server.
683
684 @param provision_command: The path of quick_provision command.
685 @param devserver_name: The devserver name and port (optional).
686 @param image_name: The image to be installed.
687 """
688 logging.info('Try quick provision with gs_cache.')
689 # If enabled, GsCache server listion on different port on the
690 # devserver.
691 gs_cache_server = devserver_name.replace(DEVSERVER_PORT, GS_CACHE_PORT)
Amin Hassanib04420b2020-07-08 18:46:11 +0000692 gs_cache_url = ('http://%s/download/chromeos-image-archive'
693 % gs_cache_server)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700694
695 # Check if GS_Cache server is enabled on the server.
Congbin Guo4a2a6642019-08-12 15:03:01 -0700696 self._run('curl -s -o /dev/null %s' % gs_cache_url)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700697
698 command = '%s --noreboot %s %s' % (provision_command, image_name,
699 gs_cache_url)
700 self._run(command)
701 metrics.Counter(_metric_name('quick_provision')).increment(
702 fields={'devserver': devserver_name, 'gs_cache': True})
703
704
705 def _quick_provision_with_devserver(self, provision_command,
706 devserver_name, image_name):
707 """Run quick_provision using legacy devserver.
708
709 @param provision_command: The path of quick_provision command.
710 @param devserver_name: The devserver name and port (optional).
711 @param image_name: The image to be installed.
712 """
Congbin Guo63ae0302019-08-12 16:37:49 -0700713 logging.info('Try quick provision with devserver.')
714 ds = dev_server.ImageServer('http://%s' % devserver_name)
715 try:
Amin Hassani95f86e02020-07-14 13:06:03 -0700716 ds.stage_artifacts(image_name, ['quick_provision', 'stateful',
717 'autotest_packages'])
Congbin Guo63ae0302019-08-12 16:37:49 -0700718 except dev_server.DevServerException as e:
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -0700719 six.reraise(error.TestFail, str(e), sys.exc_info()[2])
Congbin Guo63ae0302019-08-12 16:37:49 -0700720
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700721 static_url = 'http://%s/static' % devserver_name
722 command = '%s --noreboot %s %s' % (provision_command, image_name,
723 static_url)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700724 self._run(command)
725 metrics.Counter(_metric_name('quick_provision')).increment(
726 fields={'devserver': devserver_name, 'gs_cache': False})
727
728
Amin Hassani95f86e02020-07-14 13:06:03 -0700729 def _install_via_quick_provision(self, server_name, image_name):
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700730 """Install an updating using the `quick-provision` script.
731
732 This uses the `quick-provision` script to download and install
733 a root FS, kernel and stateful filesystem content.
734
Amin Hassani95f86e02020-07-14 13:06:03 -0700735 @param server_name: The devserver name and port (optional).
736 @param image_name: The image to be installed.
737
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700738 @return The kernel expected to be booted next.
739 """
Amin Hassanib04420b2020-07-08 18:46:11 +0000740 logging.info('Installing image using quick-provision.')
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700741 provision_command = self._get_remote_script(_QUICK_PROVISION_SCRIPT)
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700742 try:
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700743 try:
744 self._quick_provision_with_gs_cache(provision_command,
745 server_name, image_name)
Amin Hassani95f86e02020-07-14 13:06:03 -0700746 except Exception as e:
747 logging.error('Failed to quick-provision with gscache with '
748 'error %s', e)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700749 self._quick_provision_with_devserver(provision_command,
750 server_name, image_name)
751
Richard Barnette3ef29a82018-06-28 13:52:54 -0700752 self._set_target_version()
David Haddock77b75c32020-05-14 01:56:32 -0700753 return kernel_utils.verify_kernel_state_after_update(self.host)
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700754 except Exception:
755 # N.B. We handle only `Exception` here. Non-Exception
756 # classes (such as KeyboardInterrupt) are handled by our
757 # caller.
758 logging.exception('quick-provision script failed; '
759 'will fall back to update_engine.')
760 self._revert_boot_partition()
761 self._reset_stateful_partition()
762 self._reset_update_engine()
763 return None
764
765
Richard Barnette54d14f52018-05-18 16:39:49 +0000766 def _install_update(self):
Richard Barnette0beb14b2018-05-15 18:07:52 +0000767 """Install the requested image on the DUT, but don't start it.
768
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700769 This downloads and installs a root FS, kernel and stateful
770 filesystem content. This does not reboot the DUT, so the update
771 is merely pending when the method returns.
772
773 @return The kernel expected to be booted next.
Dan Shi0f466e82013-02-22 15:44:58 -0800774 """
Richard Barnette14ee84c2018-05-18 20:23:42 +0000775 logging.info('Installing image at %s onto %s',
776 self.update_url, self.host.hostname)
Amin Hassani95f86e02020-07-14 13:06:03 -0700777 server_name = urlparse.urlparse(self.update_url)[1]
778 image_name = url_to_image_name(self.update_url)
779
Amin Hassani78377a82020-07-24 14:04:13 -0700780 if self._use_quick_provision:
781 return self._install_via_quick_provision(server_name, image_name)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200782 try:
Amin Hassani78377a82020-07-24 14:04:13 -0700783 return self._install_via_update_engine(server_name, image_name)
Dale Curtis1e973182011-07-12 18:21:36 -0700784 except:
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700785 # N.B. This handling code includes non-Exception classes such
786 # as KeyboardInterrupt. We need to clean up, but we also must
787 # re-raise.
Richard Barnette14ee84c2018-05-18 20:23:42 +0000788 self._revert_boot_partition()
789 self._reset_stateful_partition()
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700790 self._reset_update_engine()
Dale Curtis1e973182011-07-12 18:21:36 -0700791 # Collect update engine logs in the event of failure.
792 if self.host.job:
Aviv Keshet2610d3e2016-06-01 16:37:01 -0700793 logging.info('Collecting update engine logs due to failure...')
Dale Curtis1e973182011-07-12 18:21:36 -0700794 self.host.get_file(
Richard Barnette3e8b2282018-05-15 20:42:20 +0000795 _UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
Gilad Arnold0c0df732015-09-21 06:37:59 -0700796 preserve_perm=False)
Richard Barnette3e8b2282018-05-15 20:42:20 +0000797 _list_image_dir_contents(self.update_url)
Dale Curtis1e973182011-07-12 18:21:36 -0700798 raise
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200799
800
Richard Barnette14ee84c2018-05-18 20:23:42 +0000801 def _complete_update(self, expected_kernel):
802 """Finish the update, and confirm that it succeeded.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000803
Richard Barnette14ee84c2018-05-18 20:23:42 +0000804 Initial condition is that the target build has been downloaded
805 and installed on the DUT, but has not yet been booted. This
806 function is responsible for rebooting the DUT, and checking that
807 the new build is running successfully.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000808
Richard Barnette14ee84c2018-05-18 20:23:42 +0000809 @param expected_kernel: kernel expected to be active after reboot.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000810 """
Richard Barnette14ee84c2018-05-18 20:23:42 +0000811 # Regarding the 'crossystem' command below: In some cases,
812 # the update flow puts the TPM into a state such that it
813 # fails verification. We don't know why. However, this
814 # call papers over the problem by clearing the TPM during
815 # the reboot.
816 #
817 # We ignore failures from 'crossystem'. Although failure
818 # here is unexpected, and could signal a bug, the point of
819 # the exercise is to paper over problems; allowing this to
820 # fail would defeat the purpose.
821 self._run('crossystem clear_tpm_owner_request=1',
822 ignore_status=True)
823 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT)
824
Richard Barnette0beb14b2018-05-15 18:07:52 +0000825 # Touch the lab machine file to leave a marker that
826 # distinguishes this image from other test images.
827 # Afterwards, we must re-run the autoreboot script because
828 # it depends on the _LAB_MACHINE_FILE.
829 autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
830 '( touch "$FILE" ; start autoreboot )')
Richard Barnette3e8b2282018-05-15 20:42:20 +0000831 self._run(autoreboot_cmd % _LAB_MACHINE_FILE)
David Haddock3446a642020-05-26 03:26:49 -0700832 kernel_utils.verify_boot_expectations(
833 expected_kernel, NewBuildUpdateError.ROLLBACK_FAILURE, self.host)
Richard Barnette0beb14b2018-05-15 18:07:52 +0000834
835 logging.debug('Cleaning up old autotest directories.')
836 try:
837 installed_autodir = autotest.Autotest.get_installed_autodir(
838 self.host)
839 self._run('rm -rf ' + installed_autodir)
840 except autotest.AutodirNotFoundError:
841 logging.debug('No autotest installed directory found.')
842
843
Richard Barnette4c81b972018-07-18 12:35:16 -0700844 def run_update(self):
845 """Perform a full update of a DUT in the test lab.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000846
Richard Barnette4c81b972018-07-18 12:35:16 -0700847 This downloads and installs the root FS and stateful partition
848 content needed for the update specified in `self.host` and
849 `self.update_url`. The update is performed according to the
850 requirements for provisioning a DUT for testing the requested
851 build.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000852
Richard Barnette4c81b972018-07-18 12:35:16 -0700853 At the end of the procedure, metrics are reported describing the
854 outcome of the operation.
855
856 @returns A tuple of the form `(image_name, attributes)`, where
857 `image_name` is the name of the image installed, and
858 `attributes` is new attributes to be applied to the DUT.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000859 """
Richard Barnette4c81b972018-07-18 12:35:16 -0700860 server_name = dev_server.get_resolved_hostname(self.update_url)
861 metrics.Counter(_metric_name('install')).increment(
862 fields={'devserver': server_name})
863
Richard Barnette9d43e562018-06-05 17:20:10 +0000864 try:
865 self._prepare_host()
866 except _AttributedUpdateError:
867 raise
868 except Exception as e:
869 logging.exception('Failure preparing host prior to update.')
870 raise HostUpdateError(self.host.hostname, str(e))
871
872 try:
873 expected_kernel = self._install_update()
874 except _AttributedUpdateError:
875 raise
876 except Exception as e:
877 logging.exception('Failure during download and install.')
878 raise ImageInstallError(self.host.hostname, server_name, str(e))
879
880 try:
881 self._complete_update(expected_kernel)
882 except _AttributedUpdateError:
883 raise
884 except Exception as e:
885 logging.exception('Failure from build after update.')
886 raise NewBuildUpdateError(self.update_version, str(e))
Richard Barnette0beb14b2018-05-15 18:07:52 +0000887
Richard Barnette0beb14b2018-05-15 18:07:52 +0000888 image_name = url_to_image_name(self.update_url)
889 # update_url is different from devserver url needed to stage autotest
890 # packages, therefore, resolve a new devserver url here.
891 devserver_url = dev_server.ImageServer.resolve(
892 image_name, self.host.hostname).url()
893 repo_url = tools.get_package_url(devserver_url, image_name)
894 return image_name, {ds_constants.JOB_REPO_URL: repo_url}