311 lines
13 KiB
Python
311 lines
13 KiB
Python
# Copyright 2018 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.server.cros.dynamic_suite import tools
|
|
from autotest_lib.server.cros.update_engine import update_engine_test
|
|
from chromite.lib import retry_util
|
|
|
|
class autoupdate_P2P(update_engine_test.UpdateEngineTest):
|
|
"""Tests a peer to peer (P2P) autoupdate."""
|
|
|
|
version = 1
|
|
|
|
_CURRENT_RESPONSE_SIGNATURE_PREF = 'current-response-signature'
|
|
_CURRENT_URL_INDEX_PREF = 'current-url-index'
|
|
_P2P_FIRST_ATTEMPT_TIMESTAMP_PREF = 'p2p-first-attempt-timestamp'
|
|
_P2P_NUM_ATTEMPTS_PREF = 'p2p-num-attempts'
|
|
|
|
|
|
def cleanup(self):
|
|
logging.info('Disabling p2p_update on hosts.')
|
|
for host in self._hosts:
|
|
try:
|
|
cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--p2p_update=no']
|
|
retry_util.RetryException(error.AutoservRunError, 2, host.run,
|
|
cmd)
|
|
except Exception:
|
|
logging.info('Failed to disable P2P in cleanup.')
|
|
super(autoupdate_P2P, self).cleanup()
|
|
|
|
|
|
def _enable_p2p_update_on_hosts(self):
|
|
"""Turn on the option to enable p2p updating on both DUTs."""
|
|
logging.info('Enabling p2p_update on hosts.')
|
|
for host in self._hosts:
|
|
try:
|
|
cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--p2p_update=yes']
|
|
retry_util.RetryException(error.AutoservRunError, 2, host.run,
|
|
cmd)
|
|
except Exception:
|
|
raise error.TestFail('Failed to enable p2p on %s' % host)
|
|
|
|
|
|
def _setup_second_hosts_prefs(self):
|
|
"""The second DUT needs to be setup for the test."""
|
|
num_attempts = os.path.join(self._UPDATE_ENGINE_PREFS_DIR,
|
|
self._P2P_NUM_ATTEMPTS_PREF)
|
|
if self._too_many_attempts:
|
|
self._hosts[1].run('echo 11 > %s' % num_attempts)
|
|
else:
|
|
self._hosts[1].run('rm %s' % num_attempts, ignore_status=True)
|
|
|
|
first_attempt = os.path.join(self._UPDATE_ENGINE_PREFS_DIR,
|
|
self._P2P_FIRST_ATTEMPT_TIMESTAMP_PREF)
|
|
if self._deadline_expired:
|
|
self._hosts[1].run('echo 1 > %s' % first_attempt)
|
|
else:
|
|
self._hosts[1].run('rm %s' % first_attempt, ignore_status=True)
|
|
|
|
|
|
def _copy_payload_signature_between_hosts(self):
|
|
"""
|
|
Copies the current-payload-signature between hosts.
|
|
|
|
We copy the pref file from host one (that updated normally) to host two
|
|
(that will be updating via p2p). We do this because otherwise host two
|
|
would have to actually update and fail in order to get itself into
|
|
the error states (deadline expired and too many attempts).
|
|
|
|
"""
|
|
pref_file = os.path.join(self._UPDATE_ENGINE_PREFS_DIR,
|
|
self._CURRENT_RESPONSE_SIGNATURE_PREF)
|
|
self._hosts[0].get_file(pref_file, self.resultsdir)
|
|
result_pref_file = os.path.join(self.resultsdir,
|
|
self._CURRENT_RESPONSE_SIGNATURE_PREF)
|
|
self._hosts[1].send_file(result_pref_file,
|
|
self._UPDATE_ENGINE_PREFS_DIR)
|
|
|
|
|
|
def _reset_current_url_index(self):
|
|
"""
|
|
Reset current-url-index pref to 0.
|
|
|
|
Since we are copying the state from one DUT to the other we also need to
|
|
reset the current url index or UE will reset all of its state.
|
|
|
|
"""
|
|
current_url_index = os.path.join(self._UPDATE_ENGINE_PREFS_DIR,
|
|
self._CURRENT_URL_INDEX_PREF)
|
|
|
|
self._hosts[1].run('echo 0 > %s' % current_url_index)
|
|
|
|
|
|
def _update_dut(self, host, update_url):
|
|
"""
|
|
Update the first DUT normally and save the update engine logs.
|
|
|
|
@param host: the host object for the first DUT.
|
|
@param update_url: the url to call for updating the DUT.
|
|
|
|
"""
|
|
host.reboot()
|
|
# Sometimes update request is lost if checking right after reboot so
|
|
# make sure update_engine is ready.
|
|
self._set_active_p2p_host(self._hosts[0])
|
|
utils.poll_for_condition(condition=self._is_update_engine_idle,
|
|
desc='Waiting for update engine idle')
|
|
|
|
logging.info('Updating first DUT with a regular update.')
|
|
try:
|
|
self._check_for_update(update_url, wait_for_completion=True)
|
|
except error.AutoservRunError:
|
|
logging.exception('Failed to update the first DUT.')
|
|
raise error.TestFail('Updating the first DUT failed. Error: %s.' %
|
|
self._get_last_error_string())
|
|
finally:
|
|
logging.info('Saving update engine logs to results dir.')
|
|
host.get_file(self._UPDATE_ENGINE_LOG,
|
|
os.path.join(self.resultsdir,
|
|
'update_engine.log_first_dut'))
|
|
host.reboot()
|
|
|
|
|
|
def _check_p2p_still_enabled(self, host):
|
|
"""
|
|
Check that updating has not affected P2P status.
|
|
|
|
@param host: The host that we just updated.
|
|
|
|
"""
|
|
logging.info('Checking that p2p is still enabled after update.')
|
|
def _is_p2p_enabled():
|
|
p2p = host.run([self._UPDATE_ENGINE_CLIENT_CMD,
|
|
'--show_p2p_update'], ignore_status=True)
|
|
if p2p.stderr is not None and 'ENABLED' in p2p.stderr:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
err = 'P2P was disabled after the first DUT was updated. This is not ' \
|
|
'expected. Something probably went wrong with the update.'
|
|
|
|
utils.poll_for_condition(_is_p2p_enabled,
|
|
exception=error.TestFail(err))
|
|
|
|
|
|
def _update_via_p2p(self, host, update_url):
|
|
"""
|
|
Update the second DUT via P2P from the first DUT.
|
|
|
|
We perform a non-interactive update and update_engine will check
|
|
for other devices that have P2P enabled and download from them instead.
|
|
|
|
@param host: The second DUT.
|
|
@param update_url: the url to call for updating the DUT.
|
|
|
|
"""
|
|
host.reboot()
|
|
self._set_active_p2p_host(self._hosts[1])
|
|
utils.poll_for_condition(condition=self._is_update_engine_idle,
|
|
desc='Waiting for update engine idle')
|
|
|
|
logging.info('Updating second host via p2p.')
|
|
try:
|
|
self._check_for_update(update_url, wait_for_completion=True,
|
|
interactive=False)
|
|
except error.AutoservRunError:
|
|
logging.exception('Failed to update the second DUT via P2P.')
|
|
raise error.TestFail('Failed to update the second DUT. Error: %s' %
|
|
self._get_last_error_string())
|
|
finally:
|
|
logging.info('Saving update engine logs to results dir.')
|
|
host.get_file(self._UPDATE_ENGINE_LOG,
|
|
os.path.join(self.resultsdir,
|
|
'update_engine.log_second_dut'))
|
|
|
|
# Return the update_engine logs so we can check for p2p entries.
|
|
return self._get_update_engine_log()
|
|
|
|
|
|
def _check_for_p2p_entries_in_update_log(self, update_engine_log):
|
|
"""
|
|
Ensure that the second DUT actually updated via P2P.
|
|
|
|
We will check the update_engine log for entries that tell us that the
|
|
update was done via P2P.
|
|
|
|
@param update_engine_log: the update engine log for the p2p update.
|
|
|
|
"""
|
|
logging.info('Making sure we have p2p entries in update engine log.')
|
|
line1 = "Checking if payload is available via p2p, file_id=" \
|
|
"cros_update_size_(.*)_hash_(.*)"
|
|
line2 = "Lookup complete, p2p-client returned URL " \
|
|
"'http://(.*)/cros_update_size_(.*)_hash_(.*).cros_au'"
|
|
line3 = "Replacing URL (.*) with local URL " \
|
|
"http://(.*)/cros_update_size_(.*)_hash_(.*).cros_au " \
|
|
"since p2p is enabled."
|
|
errline = "Forcibly disabling use of p2p for downloading because no " \
|
|
"suitable peer could be found."
|
|
too_many_attempts_err_str = "Forcibly disabling use of p2p for " \
|
|
"downloading because of previous " \
|
|
"failures when using p2p."
|
|
|
|
if re.compile(errline).search(update_engine_log) is not None:
|
|
raise error.TestFail('P2P update was disabled because no suitable '
|
|
'peer DUT was found.')
|
|
if self._too_many_attempts or self._deadline_expired:
|
|
ue = re.compile(too_many_attempts_err_str)
|
|
if ue.search(update_engine_log) is None:
|
|
raise error.TestFail('We expected update_engine to complain '
|
|
'that there were too many p2p attempts '
|
|
'but it did not. Check the logs.')
|
|
return
|
|
for line in [line1, line2, line3]:
|
|
ue = re.compile(line)
|
|
if ue.search(update_engine_log) is None:
|
|
raise error.TestFail('We did not find p2p string "%s" in the '
|
|
'update_engine log for the second host. '
|
|
'Please check the update_engine logs in '
|
|
'the results directory.' % line)
|
|
|
|
|
|
def _get_build_from_job_repo_url(self, host):
|
|
"""
|
|
Gets the build string from a hosts job_repo_url.
|
|
|
|
@param host: Object representing host.
|
|
|
|
"""
|
|
info = host.host_info_store.get()
|
|
repo_url = info.attributes.get(host.job_repo_url_attribute, '')
|
|
if not repo_url:
|
|
raise error.TestFail('There was no job_repo_url for %s so we '
|
|
'cant get a payload to use.' % host.hostname)
|
|
return tools.get_devserver_build_from_package_url(repo_url)
|
|
|
|
|
|
def _verify_hosts(self, job_repo_url):
|
|
"""
|
|
Ensure that the hosts scheduled for the test are valid.
|
|
|
|
@param job_repo_url: URL to work out the current build.
|
|
|
|
"""
|
|
lab1 = self._hosts[0].hostname.partition('-')[0]
|
|
lab2 = self._hosts[1].hostname.partition('-')[0]
|
|
if lab1 != lab2:
|
|
raise error.TestNAError('Test was given DUTs in different labs so '
|
|
'P2P will not work. See crbug.com/807495.')
|
|
|
|
logging.info('Making sure hosts can ping each other.')
|
|
result = self._hosts[1].run('ping -c5 %s' % self._hosts[0].ip,
|
|
ignore_status=True)
|
|
logging.debug('Ping status: %s', result)
|
|
if result.exit_status != 0:
|
|
raise error.TestFail('Devices failed to ping each other.')
|
|
# Get the current build. e.g samus-release/R65-10200.0.0
|
|
if job_repo_url is None:
|
|
logging.info('Making sure hosts have the same build.')
|
|
_, build1 = self._get_build_from_job_repo_url(self._hosts[0])
|
|
_, build2 = self._get_build_from_job_repo_url(self._hosts[1])
|
|
if build1 != build2:
|
|
raise error.TestFail('The builds on the hosts did not match. '
|
|
'Host one: %s, Host two: %s' % (build1,
|
|
build2))
|
|
|
|
|
|
def run_once(self, job_repo_url=None, too_many_attempts=False,
|
|
deadline_expired=False):
|
|
"""
|
|
Testing autoupdate via P2P.
|
|
|
|
@param job_repo_url: A url linking to autotest packages.
|
|
@param too_many_attempts: True to test what happens with too many
|
|
failed update attempts.
|
|
@param deadline_expired: True to test what happens when the deadline
|
|
between peers has expired
|
|
|
|
"""
|
|
logging.info('Hosts for this test: %s', self._hosts)
|
|
|
|
self._too_many_attempts = too_many_attempts
|
|
self._deadline_expired = deadline_expired
|
|
self._verify_hosts(job_repo_url)
|
|
self._enable_p2p_update_on_hosts()
|
|
self._setup_second_hosts_prefs()
|
|
|
|
# Get an N-to-N delta payload update url to use for the test.
|
|
# P2P updates are very slow so we will only update with a delta payload.
|
|
update_url = self.get_update_url_for_test(job_repo_url,
|
|
full_payload=False)
|
|
|
|
# The first device just updates normally.
|
|
self._update_dut(self._hosts[0], update_url)
|
|
self._check_p2p_still_enabled(self._hosts[0])
|
|
|
|
if too_many_attempts or deadline_expired:
|
|
self._copy_payload_signature_between_hosts()
|
|
self._reset_current_url_index()
|
|
|
|
# Update the 2nd DUT with the delta payload via P2P from the 1st DUT.
|
|
update_engine_log = self._update_via_p2p(self._hosts[1], update_url)
|
|
self._check_for_p2p_entries_in_update_log(update_engine_log)
|