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