• 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.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