1# Copyright 2020 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
5"""
6This file provides functions to implement bluetooth_PeerUpdate test
7which downloads chameleond bundle from google cloud storage and updates
8peer device associated with a DUT
9"""
10
11from __future__ import absolute_import
12
13import logging
14import os
15import sys
16import tempfile
17import time
18
19from datetime import datetime
20
21import common
22from autotest_lib.client.bin import utils
23from autotest_lib.client.common_lib import error
24
25
26# The location of the package in the cloud
27GS_PUBLIC = 'gs://chromeos-localmirror/distfiles/bluetooth_peer_bundle/'
28
29# NAME of the file that stores  commit info in the cloud
30COMMIT_FILENAME = 'latest_bluetooth_commit'
31
32# The following needs to be kept in sync with values chameleond code
33BUNDLE_TEMPLATE='chameleond-0.0.2-{}.tar.gz' # Name of the chamleond package
34BUNDLE_DIR = 'chameleond-0.0.2'
35BUNDLE_VERSION = '9999'
36CHAMELEON_BOARD = 'fpga_tio'
37
38
39def run_cmd(peer, cmd):
40    """A wrapper around host.run()."""
41    try:
42        logging.info('executing command %s on peer',cmd)
43        result = peer.host.run(cmd)
44        logging.info('exit_status is %s', result.exit_status)
45        logging.info('stdout is %s stderr is %s', result.stdout, result.stderr)
46        output = result.stderr if result.stderr else result.stdout
47        if result.exit_status == 0:
48            return True, output
49        else:
50            return False, output
51    except error.AutoservRunError as e:
52        logging.error('Error while running cmd %s %s', cmd, e)
53        return False, None
54
55
56def is_update_needed(peer, latest_commit):
57    """ Check if update is required
58
59    Update if the commit hash doesn't match
60
61    @returns: True/False
62    """
63    return not is_commit_hash_equal(peer, latest_commit)
64
65
66def is_commit_hash_equal(peer, latest_commit):
67    """ Check if chameleond commit hash is the expected one"""
68    try:
69        commit = peer.get_bt_commit_hash()
70    except:
71        logging.error('Getting the commit hash failed. Updating the peer %s',
72                      sys.exc_info())
73        return True
74
75    logging.debug('commit %s found on peer %s', commit, peer.host)
76    return commit == latest_commit
77
78
79def perform_update(peer, latest_commit):
80    """ Update the chameleond on the peer"""
81
82    logging.info('copy the file over to the peer')
83    try:
84        cur_dir = '/tmp/'
85        bundle = BUNDLE_TEMPLATE.format(latest_commit)
86        bundle_path = os.path.join(cur_dir, bundle)
87        logging.debug('package location is %s', bundle_path)
88
89        peer.host.send_file(bundle_path, '/tmp/')
90    except:
91        logging.error('copying the file failed %s ', sys.exc_info())
92        logging.error(str(os.listdir(cur_dir)))
93        return False
94
95    HOST_NOW = datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')
96    logging.info('running make on peer')
97    cmd = ('cd %s && rm -rf %s && tar zxf %s &&'
98           'cd %s && find -exec touch -c {} \; &&'
99           'make install REMOTE_INSTALL=TRUE '
100           'HOST_NOW="%s" BUNDLE_VERSION=%s '
101           'CHAMELEON_BOARD=%s && rm %s%s') % (cur_dir,BUNDLE_DIR, bundle,
102                                               BUNDLE_DIR, HOST_NOW,
103                                               BUNDLE_VERSION,
104                                               CHAMELEON_BOARD, cur_dir,
105                                               bundle)
106    logging.debug(cmd)
107    status, _ = run_cmd(peer, cmd)
108    if not status:
109        logging.info('make failed')
110        return False
111
112    logging.info('chameleond installed on peer')
113    return True
114
115
116def restart_check_chameleond(peer):
117    """restart chameleond and make sure it is running."""
118
119    restart_cmd = 'sudo /etc/init.d/chameleond restart'
120    start_cmd = 'sudo /etc/init.d/chameleond start'
121    status_cmd = 'sudo /etc/init.d/chameleond status'
122
123    status, _ = run_cmd(peer, restart_cmd)
124    if not status:
125        status, _ = run_cmd(peer, start_cmd)
126        if not status:
127            logging.error('restarting/starting chamleond failed')
128    #
129    #TODO: Refactor so that we wait for all peer devices all together.
130    #
131    # Wait till chameleond initialization is complete
132    time.sleep(5)
133
134    status, output = run_cmd(peer, status_cmd)
135    expected_output = 'chameleond is running'
136    return status and expected_output in output
137
138
139def update_peer(peer, latest_commit):
140    """Update the chameleond on peer devices if required
141
142    @params peer: btpeer to be updated
143    @params latest_commit: target git commit
144
145    @returns: (True, None) if update succeeded
146              (False, reason) if update failed
147    """
148
149    if peer.get_platform() != 'RASPI':
150        logging.error('Unsupported peer %s',str(peer.host))
151        return False, 'Unsupported peer'
152
153    if not perform_update(peer, latest_commit):
154        return False, 'Update failed'
155
156    if not restart_check_chameleond(peer):
157        return False, 'Unable to start chameleond'
158
159    if is_update_needed(peer, latest_commit):
160        return False, 'Commit not updated after upgrade'
161
162    logging.info('updating chameleond succeded')
163    return True, ''
164
165
166def update_peers(host, latest_commit):
167    """Update the chameleond on alll peer devices of an host"""
168
169    if host.btpeer_list == []:
170        raise error.TestError('Bluetooth Peer not present')
171
172    status = {}
173    for peer in host.btpeer_list:
174        #TODO(b:160782273) Make this parallel
175        status[peer] = {}
176        status[peer]['update_needed'] = is_update_needed(peer,latest_commit)
177
178    logging.debug(status)
179    if not any([v['update_needed'] for v in status.values()]):
180        logging.info("Update not needed on any of the peers")
181        return
182    for peer in host.btpeer_list:
183        if status[peer]['update_needed']:
184            status[peer]['updated'], status[peer]['reason'] = \
185            update_peer(peer, latest_commit)
186
187    logging.debug(status)
188    # If any of the peers failed update, raise failure with the reason
189    if not all([v['updated'] for v in status.values() if v['update_needed']]):
190        for peer, v in status.items():
191            if v['update_needed']:
192                if not v['updated']:
193                    logging.error('updating peer %s failed %s', str(peer.host),
194                                  v['reason'])
195        raise error.TestFail()
196
197    logging.info('%s peers updated',len([v['updated'] for v in status.values()
198                                         if v['update_needed']]))
199
200
201def get_latest_commit():
202    """ Get the latest commit
203
204    Download the file containing the latest commit and
205    parse it contents, and cleanup.
206    @returns (True,commit) in case of success ; (False, None) in case of failure
207    """
208    try:
209        commit = None
210        src = GS_PUBLIC + COMMIT_FILENAME
211
212        with tempfile.NamedTemporaryFile(suffix='bt_commit') as tmp_file:
213            tmp_filename = tmp_file.name
214            cmd = 'gsutil cp {} {}'.format(src, tmp_filename)
215            result = utils.run(cmd)
216            if result.exit_status != 0:
217                logging.error('Downloading commit file failed with %s',
218                              result.exit_status)
219                return (False, None)
220            with open(tmp_filename) as f:
221                content = f.read()
222                logging.debug('content of the file is %s', content)
223                commit = content.strip('\n').strip()
224
225        logging.info('latest commit is %s', commit)
226        if commit is None:
227            return (False, None)
228        else:
229            return (True, commit)
230    except Exception as e:
231        logging.error('exception %s in get_latest_commit', str(e))
232        return (False, None)
233
234
235def download_installation_files(host, commit):
236    """ Download the chameleond installation bundle"""
237    src_path = GS_PUBLIC + BUNDLE_TEMPLATE.format(commit)
238    dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit)
239    logging.debug('chamelond bundle path is %s', src_path)
240    logging.debug('bundle path in DUT is %s', dest_path)
241
242    cmd = 'gsutil cp {} {}'.format(src_path, dest_path)
243    try:
244        result = utils.run(cmd)
245        if result.exit_status != 0:
246            logging.error('Downloading the chameleond bundle failed with %d',
247                          result.exit_status)
248            return False
249        # Send file to DUT from the test server
250        host.send_file(dest_path, dest_path)
251        logging.debug('file send to %s %s',host, dest_path)
252        return True
253    except Exception as e:
254        logging.error('exception %s in download_installation_files', str(e))
255        return False
256
257
258def cleanup(host, commit):
259    """ Cleanup the installation file from server."""
260
261    dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit)
262    # remove file from test server
263    if not os.path.exists(dest_path):
264        logging.debug('File %s not found', dest_path)
265        return True
266
267    try:
268        logging.debug('Remove file %s', dest_path)
269        os.remove(dest_path)
270
271        # remove file from the DUT
272        result = host.run('rm {}'.format(dest_path))
273        if result.exit_status != 0:
274            logging.error('Unable to delete %s on dut', dest_path)
275            return False
276        return True
277    except Exception as e:
278        logging.error('Exception %s in cleanup', str(e))
279        return False
280