• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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