blob: 9514c2016e77471be3986b7b9e356b90b741c757 [file] [log] [blame]
Derek Beckett5fb683c2020-08-19 15:24:13 -07001# Lint as: python2, python3
Chris Sosa5e4246b2012-05-22 18:05:22 -07002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Sean O'Connor5346e4e2010-08-12 18:49:24 +02003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -07006from __future__ import print_function
7
Sean O'Connor5346e4e2010-08-12 18:49:24 +02008import logging
Amin Hassani5cda21d2020-08-10 15:24:44 -07009import os
Sean O'Connor5346e4e2010-08-12 18:49:24 +020010import re
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -070011import six
Congbin Guo63ae0302019-08-12 16:37:49 -070012import sys
Derek Beckett5fb683c2020-08-19 15:24:13 -070013import six.moves.urllib.parse
Sean O'Connor5346e4e2010-08-12 18:49:24 +020014
Chris Sosa65425082013-10-16 13:26:22 -070015from autotest_lib.client.bin import utils
Amin Hassani18e39882020-08-10 15:32:10 -070016from autotest_lib.client.common_lib import error
Prashanth B32baa9b2014-03-13 13:23:01 -070017from autotest_lib.client.common_lib.cros import dev_server
David Haddock77b75c32020-05-14 01:56:32 -070018from autotest_lib.client.common_lib.cros import kernel_utils
Richard Barnette0beb14b2018-05-15 18:07:52 +000019from autotest_lib.server import autotest
Shelley Chen61d28982016-10-28 09:40:20 -070020from autotest_lib.server import utils as server_utils
Richard Barnette0beb14b2018-05-15 18:07:52 +000021from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
22from autotest_lib.server.cros.dynamic_suite import tools
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
Richard Barnettee86b1ce2018-06-07 10:37:23 -070034_QUICK_PROVISION_SCRIPT = 'quick-provision'
Richard Barnette3e8b2282018-05-15 20:42:20 +000035
Richard Barnette0beb14b2018-05-15 18:07:52 +000036# PROVISION_FAILED - A flag file to indicate provision failures. The
37# file is created at the start of any AU procedure (see
Richard Barnette9d43e562018-06-05 17:20:10 +000038# `ChromiumOSUpdater._prepare_host()`). The file's location in
Richard Barnette0beb14b2018-05-15 18:07:52 +000039# stateful means that on successul update it will be removed. Thus, if
40# this file exists, it indicates that we've tried and failed in a
41# previous attempt to update.
42PROVISION_FAILED = '/var/tmp/provision_failed'
43
44
Richard Barnette3e8b2282018-05-15 20:42:20 +000045# A flag file used to enable special handling in lab DUTs. Some
46# parts of the system in Chromium OS test images will behave in ways
47# convenient to the test lab when this file is present. Generally,
48# we create this immediately after any update completes.
49_LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
50
51
Richard Barnette3ef29a82018-06-28 13:52:54 -070052# _TARGET_VERSION - A file containing the new version to which we plan
53# to update. This file is used by the CrOS shutdown code to detect and
54# handle certain version downgrade cases. Specifically: Downgrading
55# may trigger an unwanted powerwash in the target build when the
56# following conditions are met:
57# * Source build is a v4.4 kernel with R69-10756.0.0 or later.
58# * Target build predates the R69-10756.0.0 cutoff.
59# When this file is present and indicates a downgrade, the OS shutdown
60# code on the DUT knows how to prevent the powerwash.
61_TARGET_VERSION = '/run/update_target_version'
62
63
Richard Barnette5adb6d42018-06-28 15:52:32 -070064# _REBOOT_FAILURE_MESSAGE - This is the standard message text returned
65# when the Host.reboot() method fails. The source of this text comes
66# from `wait_for_restart()` in client/common_lib/hosts/base_classes.py.
67
68_REBOOT_FAILURE_MESSAGE = 'Host did not return from reboot'
69
70
Congbin Guoeb7aa2d2019-07-15 16:10:44 -070071DEVSERVER_PORT = '8082'
72GS_CACHE_PORT = '8888'
73
74
Richard Barnette9d43e562018-06-05 17:20:10 +000075class _AttributedUpdateError(error.TestFail):
76 """Update failure with an attributed cause."""
77
78 def __init__(self, attribution, msg):
79 super(_AttributedUpdateError, self).__init__(
80 '%s: %s' % (attribution, msg))
Richard Barnette5adb6d42018-06-28 15:52:32 -070081 self._message = msg
82
83 def _classify(self):
84 for err_pattern, classification in self._CLASSIFIERS:
85 if re.match(err_pattern, self._message):
86 return classification
87 return None
88
89 @property
90 def failure_summary(self):
91 """Summarize this error for metrics reporting."""
92 classification = self._classify()
93 if classification:
94 return '%s: %s' % (self._SUMMARY, classification)
95 else:
96 return self._SUMMARY
Richard Barnette9d43e562018-06-05 17:20:10 +000097
98
99class HostUpdateError(_AttributedUpdateError):
100 """Failure updating a DUT attributable to the DUT.
101
102 This class of exception should be raised when the most likely cause
103 of failure was a condition existing on the DUT prior to the update,
104 such as a hardware problem, or a bug in the software on the DUT.
105 """
106
Richard Barnette5adb6d42018-06-28 15:52:32 -0700107 DUT_DOWN = 'No answer to ssh'
108
109 _SUMMARY = 'DUT failed prior to update'
110 _CLASSIFIERS = [
111 (DUT_DOWN, DUT_DOWN),
112 (_REBOOT_FAILURE_MESSAGE, 'Reboot failed'),
113 ]
114
Richard Barnette9d43e562018-06-05 17:20:10 +0000115 def __init__(self, hostname, msg):
116 super(HostUpdateError, self).__init__(
117 'Error on %s prior to update' % hostname, msg)
118
119
Richard Barnette9d43e562018-06-05 17:20:10 +0000120class ImageInstallError(_AttributedUpdateError):
121 """Failure updating a DUT when installing from the devserver.
122
123 This class of exception should be raised when the target DUT fails
124 to download and install the target image from the devserver, and
125 either the devserver or the DUT might be at fault.
126 """
127
Richard Barnette5adb6d42018-06-28 15:52:32 -0700128 _SUMMARY = 'Image failed to download and install'
129 _CLASSIFIERS = []
130
Richard Barnette9d43e562018-06-05 17:20:10 +0000131 def __init__(self, hostname, devserver, msg):
132 super(ImageInstallError, self).__init__(
133 'Download and install failed from %s onto %s'
134 % (devserver, hostname), msg)
135
136
137class NewBuildUpdateError(_AttributedUpdateError):
138 """Failure updating a DUT attributable to the target build.
139
140 This class of exception should be raised when updating to a new
141 build fails, and the most likely cause of the failure is a bug in
142 the newly installed target build.
143 """
144
Richard Barnette5adb6d42018-06-28 15:52:32 -0700145 CHROME_FAILURE = 'Chrome failed to reach login screen'
Richard Barnette5adb6d42018-06-28 15:52:32 -0700146 ROLLBACK_FAILURE = 'System rolled back to previous build'
147
148 _SUMMARY = 'New build failed'
149 _CLASSIFIERS = [
150 (CHROME_FAILURE, 'Chrome did not start'),
Richard Barnette5adb6d42018-06-28 15:52:32 -0700151 (ROLLBACK_FAILURE, ROLLBACK_FAILURE),
152 ]
153
Richard Barnette9d43e562018-06-05 17:20:10 +0000154 def __init__(self, update_version, msg):
155 super(NewBuildUpdateError, self).__init__(
156 'Failure in build %s' % update_version, msg)
157
Richard Barnette621a8e42018-06-25 17:34:11 -0700158 @property
159 def failure_summary(self):
160 #pylint: disable=missing-docstring
161 return 'Build failed to work after installing'
162
Richard Barnette9d43e562018-06-05 17:20:10 +0000163
Richard Barnette3e8b2282018-05-15 20:42:20 +0000164def _url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -0800165 """Return the version based on update_url.
166
167 @param update_url: url to the image to update to.
168
169 """
Dale Curtisddfdb942011-07-14 13:59:24 -0700170 # The Chrome OS version is generally the last element in the URL. The only
171 # exception is delta update URLs, which are rooted under the version; e.g.,
172 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
173 # strip off the au section of the path before reading the version.
Dan Shi5002cfc2013-04-29 10:45:05 -0700174 return re.sub('/au/.*', '',
Derek Beckett5fb683c2020-08-19 15:24:13 -0700175 six.moves.urllib.parse.urlparse(update_url).path).split('/')[-1].strip()
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200176
177
Scott Zawalskieadbf702013-03-14 09:23:06 -0400178def url_to_image_name(update_url):
179 """Return the image name based on update_url.
180
181 From a URL like:
182 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
183 return lumpy-release/R27-3837.0.0
184
185 @param update_url: url to the image to update to.
186 @returns a string representing the image name in the update_url.
187
188 """
Derek Beckett5fb683c2020-08-19 15:24:13 -0700189 return six.moves.urllib.parse.urlparse(update_url).path[len('/update/'):]
Scott Zawalskieadbf702013-03-14 09:23:06 -0400190
191
Richard Barnette4c81b972018-07-18 12:35:16 -0700192def get_update_failure_reason(exception):
193 """Convert an exception into a failure reason for metrics.
194
195 The passed in `exception` should be one raised by failure of
196 `ChromiumOSUpdater.run_update`. The returned string will describe
197 the failure. If the input exception value is not a truish value
198 the return value will be `None`.
199
200 The number of possible return strings is restricted to a limited
201 enumeration of values so that the string may be safely used in
202 Monarch metrics without worrying about cardinality of the range of
203 string values.
204
205 @param exception Exception to be converted to a failure reason.
206
207 @return A string suitable for use in Monarch metrics, or `None`.
208 """
209 if exception:
210 if isinstance(exception, _AttributedUpdateError):
211 return exception.failure_summary
212 else:
213 return 'Unknown Error: %s' % type(exception).__name__
214 return None
215
216
Richard Barnette621a8e42018-06-25 17:34:11 -0700217def _get_metric_fields(update_url):
218 """Return a dict of metric fields.
219
220 This is used for sending autoupdate metrics for the given update URL.
221
222 @param update_url Metrics fields will be calculated from this URL.
223 """
224 build_name = url_to_image_name(update_url)
225 try:
226 board, build_type, milestone, _ = server_utils.ParseBuildName(
227 build_name)
228 except server_utils.ParseBuildNameException:
229 logging.warning('Unable to parse build name %s for metrics. '
230 'Continuing anyway.', build_name)
231 board, build_type, milestone = ('', '', '')
232 return {
233 'dev_server': dev_server.get_resolved_hostname(update_url),
234 'board': board,
235 'build_type': build_type,
236 'milestone': milestone,
237 }
238
239
Richard Barnette3e8b2282018-05-15 20:42:20 +0000240class ChromiumOSUpdater(object):
241 """Chromium OS specific DUT update functionality."""
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700242
Richard Barnette60e759e2018-07-21 20:56:59 -0700243 def __init__(self, update_url, host=None, interactive=True,
Amin Hassani18e39882020-08-10 15:32:10 -0700244 is_release_bucket=None, is_servohost=False):
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700245 """Initializes the object.
246
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700247 @param update_url: The URL we want the update to use.
248 @param host: A client.common_lib.hosts.Host implementation.
David Haddock76a4c882017-12-13 18:50:09 -0800249 @param interactive: Bool whether we are doing an interactive update.
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700250 @param is_release_bucket: If True, use release bucket
251 gs://chromeos-releases.
Garry Wang01a1d482020-08-02 20:46:53 -0700252 @param is_servohost: Bool whether the update target is a servohost.
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700253 """
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700254 self.update_url = update_url
255 self.host = host
David Haddock76a4c882017-12-13 18:50:09 -0800256 self.interactive = interactive
Richard Barnette3e8b2282018-05-15 20:42:20 +0000257 self.update_version = _url_to_version(update_url)
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700258 self._is_release_bucket = is_release_bucket
Garry Wang01a1d482020-08-02 20:46:53 -0700259 self._is_servohost = is_servohost
260
Richard Barnette3e8b2282018-05-15 20:42:20 +0000261
262 def _run(self, cmd, *args, **kwargs):
263 """Abbreviated form of self.host.run(...)"""
264 return self.host.run(cmd, *args, **kwargs)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700265
266
Richard Barnette55d1af82018-05-22 23:40:14 +0000267 def _rootdev(self, options=''):
268 """Returns the stripped output of rootdev <options>.
269
270 @param options: options to run rootdev.
271
272 """
273 return self._run('rootdev %s' % options).stdout.strip()
274
275
Richard Barnette55d1af82018-05-22 23:40:14 +0000276 def _reset_update_engine(self):
277 """Resets the host to prepare for a clean update regardless of state."""
278 self._run('stop ui || true')
Amin Hassani18e39882020-08-10 15:32:10 -0700279 self._run('restart update-engine')
Luigi Semenzatof15c8fc2017-03-03 14:12:40 -0800280
Richard Barnette55d1af82018-05-22 23:40:14 +0000281
282 def _reset_stateful_partition(self):
283 """Clear any pending stateful update request."""
Amin Hassani5cda21d2020-08-10 15:24:44 -0700284 cmd = ['rm', '-rf']
285 for f in ('var_new', 'dev_image_new', '.update_available'):
286 cmd += [os.path.join('/mnt/stateful_partition', f)]
Amin Hassani7f68fea2020-08-17 13:52:10 -0700287 # TODO(b/165024723): This is a temporary measure until we figure out the
288 # root cause of this bug.
289 cmd += ['/mnt/stateful_partition/dev_image/share/tast/data/chromiumos/'
290 'tast/local/bundles/']
Amin Hassani5cda21d2020-08-10 15:24:44 -0700291 cmd += [_TARGET_VERSION, '2>&1']
292 self._run(cmd)
Richard Barnette3ef29a82018-06-28 13:52:54 -0700293
294
295 def _set_target_version(self):
296 """Set the "target version" for the update."""
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700297 # Version strings that come from release buckets do not have RXX- at the
298 # beginning. So remove this prefix only if the version has it.
299 version_number = (self.update_version.split('-')[1]
300 if '-' in self.update_version
301 else self.update_version)
Richard Barnette3ef29a82018-06-28 13:52:54 -0700302 self._run('echo %s > %s' % (version_number, _TARGET_VERSION))
Richard Barnette55d1af82018-05-22 23:40:14 +0000303
304
305 def _revert_boot_partition(self):
306 """Revert the boot partition."""
307 part = self._rootdev('-s')
308 logging.warning('Reverting update; Boot partition will be %s', part)
309 return self._run('/postinst %s 2>&1' % part)
Gilad Arnoldd6adeb82015-09-21 07:10:03 -0700310
311
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700312 def _get_remote_script(self, script_name):
313 """Ensure that `script_name` is present on the DUT.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700314
Amin Hassani18e39882020-08-10 15:32:10 -0700315 The given script (e.g. `quick-provision`) may be present in the
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700316 stateful partition under /usr/local/bin, or we may have to
317 download it from the devserver.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700318
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700319 Determine whether the script is present or must be downloaded
320 and download if necessary. Then, return a command fragment
321 sufficient to run the script from whereever it now lives on the
322 DUT.
Richard Barnette9d43e562018-06-05 17:20:10 +0000323
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700324 @param script_name The name of the script as expected in
325 /usr/local/bin and on the devserver.
326 @return A string with the command (minus arguments) that will
327 run the target script.
Gwendal Grignou3e96cc22017-06-07 16:22:51 -0700328 """
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700329 remote_script = '/usr/local/bin/%s' % script_name
330 if self.host.path_exists(remote_script):
331 return remote_script
Laurence Goodby06fb42c2020-02-29 17:14:42 -0800332 self.host.run('mkdir -p -m 1777 /usr/local/tmp')
333 remote_tmp_script = '/usr/local/tmp/%s' % script_name
Derek Beckett5fb683c2020-08-19 15:24:13 -0700334 server_name = six.moves.urllib.parse.urlparse(self.update_url)[1]
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700335 script_url = 'http://%s/static/%s' % (server_name, script_name)
Dana Goyette353d1d92019-06-27 10:43:59 -0700336 fetch_script = 'curl -Ss -o %s %s && head -1 %s' % (
337 remote_tmp_script, script_url, remote_tmp_script)
Chris Sosa5e4246b2012-05-22 18:05:22 -0700338
Dana Goyette353d1d92019-06-27 10:43:59 -0700339 first_line = self._run(fetch_script).stdout.strip()
340
341 if first_line and first_line.startswith('#!'):
342 script_interpreter = first_line.lstrip('#!')
343 if script_interpreter:
344 return '%s %s' % (script_interpreter, remote_tmp_script)
345 return None
Richard Barnettef00a2ee2018-06-08 11:51:38 -0700346
Richard Barnette14ee84c2018-05-18 20:23:42 +0000347 def _prepare_host(self):
348 """Make sure the target DUT is working and ready for update.
349
350 Initially, the target DUT's state is unknown. The DUT is
351 expected to be online, but we strive to be forgiving if Chrome
352 and/or the update engine aren't fully functional.
353 """
354 # Summary of work, and the rationale:
355 # 1. Reboot, because it's a good way to clear out problems.
356 # 2. Touch the PROVISION_FAILED file, to allow repair to detect
357 # failure later.
358 # 3. Run the hook for host class specific preparation.
359 # 4. Stop Chrome, because the system is designed to eventually
360 # reboot if Chrome is stuck in a crash loop.
361 # 5. Force `update-engine` to start, because if Chrome failed
362 # to start properly, the status of the `update-engine` job
363 # will be uncertain.
Richard Barnette5adb6d42018-06-28 15:52:32 -0700364 if not self.host.is_up():
365 raise HostUpdateError(self.host.hostname,
366 HostUpdateError.DUT_DOWN)
Richard Barnette14ee84c2018-05-18 20:23:42 +0000367 self._reset_stateful_partition()
Garry Wang01a1d482020-08-02 20:46:53 -0700368 # Servohost reboot logic is handled by themselves.
369 if not self._is_servohost:
370 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT)
371 self._run('touch %s' % PROVISION_FAILED)
Richard Barnette14ee84c2018-05-18 20:23:42 +0000372 self.host.prepare_for_update()
Garry Wang01a1d482020-08-02 20:46:53 -0700373 # Servohost will only update via quick provision.
374 if not self._is_servohost:
375 self._reset_update_engine()
Richard Barnette14ee84c2018-05-18 20:23:42 +0000376 logging.info('Updating from version %s to %s.',
377 self.host.get_release_version(),
378 self.update_version)
379
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700380 def _quick_provision_with_gs_cache(self, provision_command, devserver_name,
381 image_name):
382 """Run quick_provision using GsCache server.
383
384 @param provision_command: The path of quick_provision command.
385 @param devserver_name: The devserver name and port (optional).
386 @param image_name: The image to be installed.
387 """
388 logging.info('Try quick provision with gs_cache.')
389 # If enabled, GsCache server listion on different port on the
390 # devserver.
391 gs_cache_server = devserver_name.replace(DEVSERVER_PORT, GS_CACHE_PORT)
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700392 gs_cache_url = ('http://%s/download/%s'
393 % (gs_cache_server,
394 'chromeos-releases' if self._is_release_bucket
395 else 'chromeos-image-archive'))
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700396
397 # Check if GS_Cache server is enabled on the server.
Congbin Guo4a2a6642019-08-12 15:03:01 -0700398 self._run('curl -s -o /dev/null %s' % gs_cache_url)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700399
400 command = '%s --noreboot %s %s' % (provision_command, image_name,
401 gs_cache_url)
402 self._run(command)
403 metrics.Counter(_metric_name('quick_provision')).increment(
404 fields={'devserver': devserver_name, 'gs_cache': True})
405
406
407 def _quick_provision_with_devserver(self, provision_command,
408 devserver_name, image_name):
409 """Run quick_provision using legacy devserver.
410
411 @param provision_command: The path of quick_provision command.
412 @param devserver_name: The devserver name and port (optional).
413 @param image_name: The image to be installed.
414 """
Congbin Guo63ae0302019-08-12 16:37:49 -0700415 logging.info('Try quick provision with devserver.')
416 ds = dev_server.ImageServer('http://%s' % devserver_name)
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700417 archive_url = ('gs://chromeos-releases/%s' % image_name
418 if self._is_release_bucket else None)
Congbin Guo63ae0302019-08-12 16:37:49 -0700419 try:
Amin Hassani95f86e02020-07-14 13:06:03 -0700420 ds.stage_artifacts(image_name, ['quick_provision', 'stateful',
Amin Hassani1d6d3a72020-07-09 09:50:26 -0700421 'autotest_packages'],
422 archive_url=archive_url)
Congbin Guo63ae0302019-08-12 16:37:49 -0700423 except dev_server.DevServerException as e:
Gregory Nisbetcf8c2ed2020-07-14 18:35:49 -0700424 six.reraise(error.TestFail, str(e), sys.exc_info()[2])
Congbin Guo63ae0302019-08-12 16:37:49 -0700425
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700426 static_url = 'http://%s/static' % devserver_name
427 command = '%s --noreboot %s %s' % (provision_command, image_name,
428 static_url)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700429 self._run(command)
430 metrics.Counter(_metric_name('quick_provision')).increment(
431 fields={'devserver': devserver_name, 'gs_cache': False})
432
433
Amin Hassani18e39882020-08-10 15:32:10 -0700434 def _install_update(self):
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700435 """Install an updating using the `quick-provision` script.
436
437 This uses the `quick-provision` script to download and install
438 a root FS, kernel and stateful filesystem content.
439
440 @return The kernel expected to be booted next.
441 """
Amin Hassani18e39882020-08-10 15:32:10 -0700442 logging.info('Installing image at %s onto %s',
443 self.update_url, self.host.hostname)
Derek Beckett5fb683c2020-08-19 15:24:13 -0700444 server_name = six.moves.urllib.parse.urlparse(self.update_url)[1]
Amin Hassani18e39882020-08-10 15:32:10 -0700445 image_name = url_to_image_name(self.update_url)
446
Amin Hassanib04420b2020-07-08 18:46:11 +0000447 logging.info('Installing image using quick-provision.')
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700448 provision_command = self._get_remote_script(_QUICK_PROVISION_SCRIPT)
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700449 try:
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700450 try:
451 self._quick_provision_with_gs_cache(provision_command,
452 server_name, image_name)
Amin Hassani95f86e02020-07-14 13:06:03 -0700453 except Exception as e:
454 logging.error('Failed to quick-provision with gscache with '
455 'error %s', e)
Congbin Guoeb7aa2d2019-07-15 16:10:44 -0700456 self._quick_provision_with_devserver(provision_command,
457 server_name, image_name)
458
Richard Barnette3ef29a82018-06-28 13:52:54 -0700459 self._set_target_version()
David Haddock77b75c32020-05-14 01:56:32 -0700460 return kernel_utils.verify_kernel_state_after_update(self.host)
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700461 except Exception:
462 # N.B. We handle only `Exception` here. Non-Exception
463 # classes (such as KeyboardInterrupt) are handled by our
464 # caller.
Amin Hassani18e39882020-08-10 15:32:10 -0700465 logging.exception('quick-provision script failed;')
Richard Barnettee86b1ce2018-06-07 10:37:23 -0700466 self._revert_boot_partition()
467 self._reset_stateful_partition()
468 self._reset_update_engine()
469 return None
470
471
Richard Barnette14ee84c2018-05-18 20:23:42 +0000472 def _complete_update(self, expected_kernel):
473 """Finish the update, and confirm that it succeeded.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000474
Richard Barnette14ee84c2018-05-18 20:23:42 +0000475 Initial condition is that the target build has been downloaded
476 and installed on the DUT, but has not yet been booted. This
477 function is responsible for rebooting the DUT, and checking that
478 the new build is running successfully.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000479
Richard Barnette14ee84c2018-05-18 20:23:42 +0000480 @param expected_kernel: kernel expected to be active after reboot.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000481 """
Richard Barnette14ee84c2018-05-18 20:23:42 +0000482 # Regarding the 'crossystem' command below: In some cases,
483 # the update flow puts the TPM into a state such that it
484 # fails verification. We don't know why. However, this
485 # call papers over the problem by clearing the TPM during
486 # the reboot.
487 #
488 # We ignore failures from 'crossystem'. Although failure
489 # here is unexpected, and could signal a bug, the point of
490 # the exercise is to paper over problems; allowing this to
491 # fail would defeat the purpose.
492 self._run('crossystem clear_tpm_owner_request=1',
493 ignore_status=True)
494 self.host.reboot(timeout=self.host.REBOOT_TIMEOUT)
495
Richard Barnette0beb14b2018-05-15 18:07:52 +0000496 # Touch the lab machine file to leave a marker that
497 # distinguishes this image from other test images.
498 # Afterwards, we must re-run the autoreboot script because
499 # it depends on the _LAB_MACHINE_FILE.
500 autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
501 '( touch "$FILE" ; start autoreboot )')
Richard Barnette3e8b2282018-05-15 20:42:20 +0000502 self._run(autoreboot_cmd % _LAB_MACHINE_FILE)
Sanika Kulkarnia9c4c332020-08-18 15:56:28 -0700503 try:
504 kernel_utils.verify_boot_expectations(
505 expected_kernel, NewBuildUpdateError.ROLLBACK_FAILURE,
506 self.host)
507 except Exception:
508 # When the system is rolled back, the provision_failed file is
509 # removed. So add it back here and re-raise the exception.
510 self._run('touch %s' % PROVISION_FAILED)
511 raise
Richard Barnette0beb14b2018-05-15 18:07:52 +0000512
513 logging.debug('Cleaning up old autotest directories.')
514 try:
515 installed_autodir = autotest.Autotest.get_installed_autodir(
516 self.host)
517 self._run('rm -rf ' + installed_autodir)
518 except autotest.AutodirNotFoundError:
519 logging.debug('No autotest installed directory found.')
520
521
Richard Barnette4c81b972018-07-18 12:35:16 -0700522 def run_update(self):
523 """Perform a full update of a DUT in the test lab.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000524
Richard Barnette4c81b972018-07-18 12:35:16 -0700525 This downloads and installs the root FS and stateful partition
526 content needed for the update specified in `self.host` and
527 `self.update_url`. The update is performed according to the
528 requirements for provisioning a DUT for testing the requested
529 build.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000530
Richard Barnette4c81b972018-07-18 12:35:16 -0700531 At the end of the procedure, metrics are reported describing the
532 outcome of the operation.
533
534 @returns A tuple of the form `(image_name, attributes)`, where
535 `image_name` is the name of the image installed, and
536 `attributes` is new attributes to be applied to the DUT.
Richard Barnette0beb14b2018-05-15 18:07:52 +0000537 """
Richard Barnette4c81b972018-07-18 12:35:16 -0700538 server_name = dev_server.get_resolved_hostname(self.update_url)
539 metrics.Counter(_metric_name('install')).increment(
540 fields={'devserver': server_name})
541
Richard Barnette9d43e562018-06-05 17:20:10 +0000542 try:
543 self._prepare_host()
544 except _AttributedUpdateError:
545 raise
546 except Exception as e:
547 logging.exception('Failure preparing host prior to update.')
548 raise HostUpdateError(self.host.hostname, str(e))
549
550 try:
551 expected_kernel = self._install_update()
552 except _AttributedUpdateError:
553 raise
554 except Exception as e:
555 logging.exception('Failure during download and install.')
556 raise ImageInstallError(self.host.hostname, server_name, str(e))
557
Garry Wang01a1d482020-08-02 20:46:53 -0700558 # Servohost will handle post update process themselves.
559 if not self._is_servohost:
560 try:
561 self._complete_update(expected_kernel)
562 except _AttributedUpdateError:
563 raise
564 except Exception as e:
565 logging.exception('Failure from build after update.')
566 raise NewBuildUpdateError(self.update_version, str(e))
Richard Barnette0beb14b2018-05-15 18:07:52 +0000567
Richard Barnette0beb14b2018-05-15 18:07:52 +0000568 image_name = url_to_image_name(self.update_url)
569 # update_url is different from devserver url needed to stage autotest
570 # packages, therefore, resolve a new devserver url here.
571 devserver_url = dev_server.ImageServer.resolve(
572 image_name, self.host.hostname).url()
573 repo_url = tools.get_package_url(devserver_url, image_name)
574 return image_name, {ds_constants.JOB_REPO_URL: repo_url}