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