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