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