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.client.common_lib.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 _P2P_ATTEMPTS_FILE = '/var/lib/update_engine/prefs/p2p-num-attempts' 22 _P2P_FIRST_ATTEMPT_FILE = '/var/lib/update_engine/prefs/p2p-first-attempt' \ 23 '-timestamp' 24 25 def setup(self): 26 self._omaha_devserver = None 27 28 29 def cleanup(self): 30 logging.info('Disabling p2p_update on hosts.') 31 for host in self._hosts: 32 try: 33 cmd = 'update_engine_client --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 def _enable_p2p_update_on_hosts(self): 41 """Turn on the option to enable p2p updating on both DUTs.""" 42 logging.info('Enabling p2p_update on hosts.') 43 for host in self._hosts: 44 try: 45 cmd = 'update_engine_client --p2p_update=yes' 46 retry_util.RetryException(error.AutoservRunError, 2, host.run, 47 cmd) 48 except Exception: 49 raise error.TestFail('Failed to enable p2p on %s' % host) 50 51 if self._too_many_attempts: 52 host.run('echo 11 > %s' % self._P2P_ATTEMPTS_FILE) 53 else: 54 host.run('rm %s' % self._P2P_ATTEMPTS_FILE, ignore_status=True) 55 56 if self._deadline_expired: 57 host.run('echo 1 > %s' % self._P2P_FIRST_ATTEMPT_FILE) 58 else: 59 host.run('rm %s' % self._P2P_FIRST_ATTEMPT_FILE, 60 ignore_status=True) 61 62 host.reboot() 63 64 65 def _update_dut(self, host, update_url): 66 """ 67 Update the first DUT normally and save the update engine logs. 68 69 @param host: the host object for the first DUT. 70 @param update_url: the url to call for updating the DUT. 71 72 """ 73 logging.info('Updating first DUT with a regular update.') 74 try: 75 updater = autoupdater.ChromiumOSUpdater(update_url, host) 76 updater.update_image() 77 except autoupdater.RootFSUpdateError: 78 logging.exception('Failed to update the first DUT.') 79 raise error.TestFail('Updating the first DUT failed. Please check ' 80 'the update_engine logs in the results dir.') 81 finally: 82 logging.info('Saving update engine logs to results dir.') 83 host.get_file('/var/log/update_engine.log', 84 os.path.join(self.resultsdir, 85 'update_engine.log_first_dut')) 86 host.reboot() 87 88 89 def _check_p2p_still_enabled(self, host): 90 """ 91 Check that updating has not affected P2P status. 92 93 @param host: The host that we just updated. 94 95 """ 96 logging.info('Checking that p2p is still enabled after update.') 97 def _is_p2p_enabled(): 98 p2p = host.run('update_engine_client --show_p2p_update', 99 ignore_status=True) 100 if p2p.stderr is not None and 'ENABLED' in p2p.stderr: 101 return True 102 else: 103 return False 104 105 err = 'P2P was disabled after the first DUT was updated. This is not ' \ 106 'expected. Something probably went wrong with the update.' 107 108 utils.poll_for_condition(_is_p2p_enabled, 109 exception=error.TestFail(err)) 110 111 112 def _update_via_p2p(self, host, update_url): 113 """ 114 Update the second DUT via P2P from the first DUT. 115 116 We perform a non-interactive update and update_engine will check 117 for other devices that have P2P enabled and download from them instead. 118 119 @param host: The second DUT. 120 @param update_url: the url to call for updating the DUT. 121 122 """ 123 logging.info('Updating second host via p2p.') 124 125 try: 126 # Start a non-interactive update which is required for p2p. 127 updater = autoupdater.ChromiumOSUpdater(update_url, host, 128 interactive=False) 129 updater.update_image() 130 except autoupdater.RootFSUpdateError: 131 logging.exception('Failed to update the second DUT via P2P.') 132 raise error.TestFail('Failed to update the second DUT. Please ' 133 'checkout update_engine logs in results dir.') 134 finally: 135 logging.info('Saving update engine logs to results dir.') 136 host.get_file('/var/log/update_engine.log', 137 os.path.join(self.resultsdir, 138 'update_engine.log_second_dut')) 139 140 # Return the update_engine logs so we can check for p2p entries. 141 return host.run('cat /var/log/update_engine.log').stdout 142 143 144 def _check_for_p2p_entries_in_update_log(self, update_engine_log): 145 """ 146 Ensure that the second DUT actually updated via P2P. 147 148 We will check the update_engine log for entries that tell us that the 149 update was done via P2P. 150 151 @param update_engine_log: the update engine log for the p2p update. 152 153 """ 154 logging.info('Making sure we have p2p entries in update engine log.') 155 line1 = "Checking if payload is available via p2p, file_id=" \ 156 "cros_update_size_(.*)_hash_(.*)" 157 line2 = "Lookup complete, p2p-client returned URL " \ 158 "'http://%s:(.*)/cros_update_size_(.*)_hash_(.*).cros_au'" % \ 159 self._hosts[0].ip 160 line3 = "Replacing URL (.*) with local URL " \ 161 "http://%s:(.*)/cros_update_size_(.*)_hash_(.*).cros_au " \ 162 "since p2p is enabled." % self._hosts[0].ip 163 errline = "Forcibly disabling use of p2p for downloading because no " \ 164 "suitable peer could be found." 165 too_many_attempts_err_str = "Forcibly disabling use of p2p for " \ 166 "downloading because of previous " \ 167 "failures when using p2p." 168 169 if re.compile(errline).search(update_engine_log) is not None: 170 raise error.TestFail('P2P update was disabled because no suitable ' 171 'peer DUT was found.') 172 if self._too_many_attempts or self._deadline_expired: 173 ue = re.compile(too_many_attempts_err_str) 174 if ue.search(update_engine_log) is None: 175 raise error.TestFail('We expected update_engine to complain ' 176 'that there were too many p2p attempts ' 177 'but it did not. Check the logs.') 178 return 179 for line in [line1, line2, line3]: 180 ue = re.compile(line) 181 if ue.search(update_engine_log) is None: 182 raise error.TestFail('We did not find p2p string "%s" in the ' 183 'update_engine log for the second host. ' 184 'Please check the update_engine logs in ' 185 'the results directory.' % line) 186 187 188 def _get_build_from_job_repo_url(self, host): 189 """ 190 Gets the build string from a hosts job_repo_url. 191 192 @param host: Object representing host. 193 194 """ 195 info = host.host_info_store.get() 196 repo_url = info.attributes.get(host.job_repo_url_attribute, '') 197 if not repo_url: 198 raise error.TestFail('There was no job_repo_url for %s so we ' 199 'cant get a payload to use.' % host.hostname) 200 return tools.get_devserver_build_from_package_url(repo_url) 201 202 203 def _verify_hosts(self, job_repo_url): 204 """ 205 Ensure that the hosts scheduled for the test are valid. 206 207 @param job_repo_url: URL to work out the current build. 208 209 """ 210 lab1 = self._hosts[0].hostname.partition('-')[0] 211 lab2 = self._hosts[1].hostname.partition('-')[0] 212 if lab1 != lab2: 213 raise error.TestNAError('Test was given DUTs in different labs so ' 214 'P2P will not work. See crbug.com/807495.') 215 216 logging.info('Making sure hosts can ping each other.') 217 result = self._hosts[1].run('ping -c5 %s' % self._hosts[0].ip, 218 ignore_status=True) 219 logging.debug('Ping status: %s', result) 220 if result.exit_status != 0: 221 raise error.TestFail('Devices failed to ping each other.') 222 # Get the current build. e.g samus-release/R65-10200.0.0 223 if job_repo_url is None: 224 logging.info('Making sure hosts have the same build.') 225 _, build1 = self._get_build_from_job_repo_url(self._hosts[0]) 226 _, build2 = self._get_build_from_job_repo_url(self._hosts[1]) 227 if build1 != build2: 228 raise error.TestFail('The builds on the hosts did not match. ' 229 'Host one: %s, Host two: %s' % (build1, 230 build2)) 231 232 233 def run_once(self, hosts, job_repo_url=None, too_many_attempts=False, 234 deadline_expired=False): 235 self._hosts = hosts 236 logging.info('Hosts for this test: %s', self._hosts) 237 238 self._too_many_attempts = too_many_attempts 239 self._deadline_expired = deadline_expired 240 self._verify_hosts(job_repo_url) 241 self._enable_p2p_update_on_hosts() 242 243 # Get an N-to-N delta payload update url to use for the test. 244 # P2P updates are very slow so we will only update with a delta payload. 245 update_url = self.get_update_url_for_test(job_repo_url, 246 full_payload=False, 247 critical_update=False, 248 max_updates=2) 249 250 # The first device just updates normally. 251 self._update_dut(self._hosts[0], update_url) 252 self._check_p2p_still_enabled(self._hosts[0]) 253 254 # Update the 2nd DUT with the delta payload via P2P from the 1st DUT. 255 update_engine_log = self._update_via_p2p(self._hosts[1], update_url) 256 self._check_for_p2p_entries_in_update_log(update_engine_log) 257