1# Lint as: python2, python3 2# Copyright 2018 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import logging 7import os 8import re 9 10from autotest_lib.client.common_lib import error 11from autotest_lib.client.common_lib import utils 12from autotest_lib.server.cros.dynamic_suite import tools 13from autotest_lib.server.cros.update_engine import update_engine_test 14from autotest_lib.utils.frozen_chromite.lib import retry_util 15 16class autoupdate_P2P(update_engine_test.UpdateEngineTest): 17 """Tests a peer to peer (P2P) autoupdate.""" 18 19 version = 1 20 21 _CURRENT_RESPONSE_SIGNATURE_PREF = 'current-response-signature' 22 _CURRENT_URL_INDEX_PREF = 'current-url-index' 23 _P2P_FIRST_ATTEMPT_TIMESTAMP_PREF = 'p2p-first-attempt-timestamp' 24 _P2P_NUM_ATTEMPTS_PREF = 'p2p-num-attempts' 25 26 _CLIENT_TEST_WITH_DLC = 'autoupdate_InstallAndUpdateDLC' 27 _CLIENT_TEST = 'autoupdate_CannedOmahaUpdate' 28 29 def cleanup(self): 30 logging.info('Disabling p2p_update on hosts.') 31 for host in self._hosts: 32 try: 33 cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--p2p_update=no'] 34 retry_util.RetryException(error.AutoservRunError, 2, host.run, 35 cmd) 36 except Exception: 37 logging.info('Failed to disable P2P in cleanup.') 38 super(autoupdate_P2P, self).cleanup() 39 40 41 def _cleanup_dlcs(self): 42 """Remove all DLCs on the DUT before starting the test. """ 43 installed = self._dlc_util.list().keys() 44 for dlc_id in installed: 45 self._dlc_util.purge(dlc_id) 46 # DLCs may be present but not mounted, so they won't be purged above. 47 self._dlc_util.purge(self._dlc_util._SAMPLE_DLC_ID, ignore_status=True) 48 49 def _enable_p2p_update_on_hosts(self): 50 """Turn on the option to enable p2p updating on both DUTs.""" 51 logging.info('Enabling p2p_update on hosts.') 52 for host in self._hosts: 53 try: 54 cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--p2p_update=yes'] 55 retry_util.RetryException(error.AutoservRunError, 2, host.run, 56 cmd) 57 except Exception: 58 raise error.TestFail('Failed to enable p2p on %s' % host) 59 60 61 def _setup_second_hosts_prefs(self): 62 """The second DUT needs to be setup for the test.""" 63 num_attempts = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, 64 self._P2P_NUM_ATTEMPTS_PREF) 65 if self._too_many_attempts: 66 self._hosts[1].run('echo 11 > %s' % num_attempts) 67 else: 68 self._hosts[1].run('rm %s' % num_attempts, ignore_status=True) 69 70 first_attempt = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, 71 self._P2P_FIRST_ATTEMPT_TIMESTAMP_PREF) 72 if self._deadline_expired: 73 self._hosts[1].run('echo 1 > %s' % first_attempt) 74 else: 75 self._hosts[1].run('rm %s' % first_attempt, ignore_status=True) 76 77 78 def _copy_payload_signature_between_hosts(self): 79 """ 80 Copies the current-payload-signature between hosts. 81 82 We copy the pref file from host one (that updated normally) to host two 83 (that will be updating via p2p). We do this because otherwise host two 84 would have to actually update and fail in order to get itself into 85 the error states (deadline expired and too many attempts). 86 87 """ 88 pref_file = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, 89 self._CURRENT_RESPONSE_SIGNATURE_PREF) 90 self._hosts[0].get_file(pref_file, self.resultsdir) 91 result_pref_file = os.path.join(self.resultsdir, 92 self._CURRENT_RESPONSE_SIGNATURE_PREF) 93 self._hosts[1].send_file(result_pref_file, 94 self._UPDATE_ENGINE_PREFS_DIR) 95 96 97 def _reset_current_url_index(self): 98 """ 99 Reset current-url-index pref to 0. 100 101 Since we are copying the state from one DUT to the other we also need to 102 reset the current url index or UE will reset all of its state. 103 104 """ 105 current_url_index = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, 106 self._CURRENT_URL_INDEX_PREF) 107 108 self._hosts[1].run('echo 0 > %s' % current_url_index) 109 110 111 def _update_dut(self, host, tag, interactive=True): 112 """ 113 Update the first DUT normally and save the update engine logs. 114 115 @param host: the host object for the first DUT. 116 @param interactive: Whether the update should be interactive. 117 118 """ 119 self._host = host 120 self._set_active_p2p_host(host) 121 self._dlc_util.set_run(self._host.run) 122 if self._with_dlc: 123 self._cleanup_dlcs() 124 self._host.reboot() 125 # Sometimes update request is lost if checking right after reboot so 126 # make sure update_engine is ready. 127 utils.poll_for_condition(condition=self._is_update_engine_idle, 128 desc='Waiting for update engine idle') 129 130 logging.info('Updating %s (%s).', host, tag) 131 if self._with_dlc: 132 self._run_client_test_and_check_result( 133 self._CLIENT_TEST_WITH_DLC, 134 payload_urls=self._payload_urls, 135 tag=tag, 136 interactive=interactive) 137 else: 138 self._run_client_test_and_check_result( 139 self._CLIENT_TEST, 140 payload_url=self._payload_urls[0], 141 tag=tag, 142 interactive=interactive) 143 update_engine_log = self._get_update_engine_log() 144 host.reboot() 145 return update_engine_log 146 # TODO(ahassani): There is probably a race condition here. We should 147 # wait after the reboot to make sure p2p is up before kicking off the 148 # second host's update check. Otherwise, the second one is not going to 149 # use p2p updates. Another solution would be to not reboot here at all 150 # since p2p server is still running. 151 152 153 def _check_p2p_still_enabled(self, host): 154 """ 155 Check that updating has not affected P2P status. 156 157 @param host: The host that we just updated. 158 159 """ 160 logging.info('Checking that p2p is still enabled after update.') 161 def _is_p2p_enabled(): 162 p2p = host.run([self._UPDATE_ENGINE_CLIENT_CMD, 163 '--show_p2p_update'], ignore_status=True) 164 if p2p.stderr is not None and 'ENABLED' in p2p.stderr: 165 return True 166 else: 167 return False 168 169 err = 'P2P was disabled after the first DUT was updated. This is not ' \ 170 'expected. Something probably went wrong with the update.' 171 172 utils.poll_for_condition(_is_p2p_enabled, 173 exception=error.TestFail(err)) 174 175 176 def _check_for_p2p_entries_in_update_log(self, update_engine_log): 177 """ 178 Ensure that the second DUT actually updated via P2P. 179 180 We will check the update_engine log for entries that tell us that the 181 update was done via P2P. 182 183 @param update_engine_log: the update engine log for the p2p update. 184 185 """ 186 logging.info('Making sure we have p2p entries in update engine log.') 187 line1 = "Checking if payload is available via p2p, file_id=" \ 188 "cros_update_size_(.*)_hash_(.*)" 189 line2 = "Lookup complete, p2p-client returned URL " \ 190 "'http://(.*)/cros_update_size_(.*)_hash_(.*).cros_au'" 191 line3 = "Replacing URL (.*) with local URL " \ 192 "http://(.*)/cros_update_size_(.*)_hash_(.*).cros_au " \ 193 "since p2p is enabled." 194 errline = "Forcibly disabling use of p2p for downloading because no " \ 195 "suitable peer could be found." 196 too_many_attempts_err_str = "Forcibly disabling use of p2p for " \ 197 "downloading because of previous " \ 198 "failures when using p2p." 199 200 if re.compile(errline).search(update_engine_log) is not None: 201 raise error.TestFail('P2P update was disabled because no suitable ' 202 'peer DUT was found.') 203 if self._too_many_attempts or self._deadline_expired: 204 ue = re.compile(too_many_attempts_err_str) 205 if ue.search(update_engine_log) is None: 206 raise error.TestFail('We expected update_engine to complain ' 207 'that there were too many p2p attempts ' 208 'but it did not. Check the logs.') 209 return 210 for line in [line1, line2, line3]: 211 ue = re.compile(line) 212 if ue.search(update_engine_log) is None: 213 raise error.TestFail('We did not find p2p string "%s" in the ' 214 'update_engine log for the second host. ' 215 'Please check the update_engine logs in ' 216 'the results directory.' % line) 217 218 219 def _get_build_from_job_repo_url(self, host): 220 """ 221 Gets the build string from a hosts job_repo_url. 222 223 @param host: Object representing host. 224 225 """ 226 info = host.host_info_store.get() 227 repo_url = info.attributes.get(host.job_repo_url_attribute, '') 228 if not repo_url: 229 raise error.TestFail('There was no job_repo_url for %s so we ' 230 'cant get a payload to use.' % host.hostname) 231 return tools.get_devserver_build_from_package_url(repo_url) 232 233 234 def _verify_hosts(self, job_repo_url): 235 """ 236 Ensure that the hosts scheduled for the test are valid. 237 238 @param job_repo_url: URL to work out the current build. 239 240 """ 241 lab1 = self._hosts[0].hostname.partition('-')[0] 242 lab2 = self._hosts[1].hostname.partition('-')[0] 243 if lab1 != lab2: 244 raise error.TestNAError('Test was given DUTs in different labs so ' 245 'P2P will not work. See crbug.com/807495.') 246 247 logging.info('Making sure hosts can ping each other.') 248 result = self._hosts[1].run('ping -c5 %s' % self._hosts[0].ip, 249 ignore_status=True) 250 logging.debug('Ping status: %s', result) 251 if result.exit_status != 0: 252 raise error.TestFail('Devices failed to ping each other.') 253 # Get the current build. e.g samus-release/R65-10200.0.0 254 if job_repo_url is None: 255 logging.info('Making sure hosts have the same build.') 256 _, build1 = self._get_build_from_job_repo_url(self._hosts[0]) 257 _, build2 = self._get_build_from_job_repo_url(self._hosts[1]) 258 if build1 != build2: 259 raise error.TestFail('The builds on the hosts did not match. ' 260 'Host one: %s, Host two: %s' % (build1, 261 build2)) 262 263 264 def run_once(self, 265 companions, 266 job_repo_url=None, 267 too_many_attempts=False, 268 deadline_expired=False, 269 with_dlc=False, 270 running_at_desk=False): 271 """ 272 Testing autoupdate via P2P. 273 274 @param companions: List of other DUTs used in the test. 275 @param job_repo_url: A url linking to autotest packages. 276 @param too_many_attempts: True to test what happens with too many 277 failed update attempts. 278 @param deadline_expired: True to test what happens when the deadline 279 between peers has expired 280 @param with_dlc: Whether to include sample-dlc in the test. 281 @param running_at_desk: True to stage files on public bucket. Useful 282 for debugging locally. 283 284 """ 285 self._hosts = [self._host, companions[0]] 286 logging.info('Hosts for this test: %s', self._hosts) 287 288 self._too_many_attempts = too_many_attempts 289 self._deadline_expired = deadline_expired 290 self._with_dlc = with_dlc 291 292 self._verify_hosts(job_repo_url) 293 self._enable_p2p_update_on_hosts() 294 self._setup_second_hosts_prefs() 295 296 # Get an N-to-N delta payload update url to use for the test. P2P 297 # updates are very slow so we will only update with a delta payload. In 298 # addition we need the full DLC payload so we can perform its install. 299 self._payload_urls = [ 300 self.get_payload_for_nebraska(job_repo_url, 301 full_payload=False, 302 public_bucket=running_at_desk) 303 ] 304 if self._with_dlc: 305 self._payload_urls += [ 306 self.get_payload_for_nebraska( 307 job_repo_url=job_repo_url, 308 full_payload=True, 309 payload_type=self._PAYLOAD_TYPE.DLC, 310 public_bucket=running_at_desk), 311 self.get_payload_for_nebraska( 312 job_repo_url=job_repo_url, 313 full_payload=False, 314 payload_type=self._PAYLOAD_TYPE.DLC, 315 public_bucket=running_at_desk) 316 ] 317 318 # The first device just updates normally. 319 self._update_dut(self._hosts[0], 'host1') 320 self._check_p2p_still_enabled(self._hosts[0]) 321 322 if too_many_attempts or deadline_expired: 323 self._copy_payload_signature_between_hosts() 324 self._reset_current_url_index() 325 326 # Update the 2nd DUT with the delta payload via P2P from the 1st DUT. 327 update_engine_log = self._update_dut(self._hosts[1], 328 'host2', 329 interactive=False) 330 self._check_for_p2p_entries_in_update_log(update_engine_log) 331