# Lint as: python2, python3 # Copyright 2020 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ This file provides functions to implement bluetooth_PeerUpdate test which downloads chameleond bundle from google cloud storage and updates peer device associated with a DUT """ from __future__ import absolute_import import logging import os import sys import tempfile import time import yaml from datetime import datetime import common from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error # The location of the package in the cloud GS_PUBLIC = 'gs://chromeos-localmirror/distfiles/bluetooth_peer_bundle/' # NAME of the file that stores python2 commits info in the cloud PYTHON2_COMMITS_FILENAME = 'bluetooth_python2_commits' # NAME of the file that stores commits info in the Google cloud storage. COMMITS_FILENAME = 'bluetooth_commits.yaml' # The following needs to be kept in sync with values chameleond code BUNDLE_TEMPLATE='chameleond-0.0.2-{}.tar.gz' # Name of the chamleond package BUNDLE_DIR = 'chameleond-0.0.2' BUNDLE_VERSION = '9999' CHAMELEON_BOARD = 'fpga_tio' def run_cmd(peer, cmd): """A wrapper around host.run().""" try: logging.info('executing command %s on peer',cmd) result = peer.host.run(cmd) logging.info('exit_status is %s', result.exit_status) logging.info('stdout is %s stderr is %s', result.stdout, result.stderr) output = result.stderr if result.stderr else result.stdout if result.exit_status == 0: return True, output else: return False, output except error.AutoservRunError as e: logging.error('Error while running cmd %s %s', cmd, e) return False, None def read_google_cloud_file(filename): """ Check if update is required Read the contents of the Googlle cloud file. @param filename: the filename of the Google cloud file @returns: the contexts of the file if successful; None otherwise. """ try: with tempfile.NamedTemporaryFile() as tmp_file: tmp_filename = tmp_file.name cmd = 'gsutil cp {} {}'.format(filename, tmp_filename) result = utils.run(cmd) if result.exit_status != 0: logging.error('Downloading file %s failed with %s', filename, result.exit_status) return None with open(tmp_filename) as f: content = f.read() logging.debug('content of the file %s: %s', filename, content) return content except Exception as e: logging.error('Error in reading %s', filename) return None def is_update_needed(peer, target_commit): """ Check if update is required Update if the commit hash doesn't match @returns: True/False """ return not is_commit_hash_equal(peer, target_commit) def is_commit_hash_equal(peer, target_commit): """ Check if chameleond commit hash is the expected one""" try: commit = peer.get_bt_commit_hash() except: logging.error('Getting the commit hash failed. Updating the peer %s', sys.exc_info()) return True logging.debug('commit %s found on peer %s', commit, peer.host) return commit == target_commit def is_chromeos_build_greater_or_equal(build1, build2): """ Check if build1 is greater or equal to the build2""" build1 = [int(key1) for key1 in build1.split('.')] build2 = [int(key2) for key2 in build2.split('.')] for key1, key2 in zip(build1, build2): if key1 > key2: return True elif key1 == key2: continue else: return False return True def perform_update(force_system_packages_update, peer, target_commit, latest_commit): """ Update the chameleond on the peer @param force_system_packages_update: True to update system packages of the peer. @param peer: btpeer to be updated @param target_commit: target git commit @param latest_commit: the latest git commit in the lab_commit_map, which is defined in the bluetooth_commits.yaml @returns: True if the update process is success, False otherwise """ # Only update the system when the target commit is the latest. # Since system packages are backward compatible so it's safe to keep # it the latest. needs_system_update = 'true' if force_system_packages_update: logging.info("Forced system packages update on the peer.") elif target_commit == latest_commit: logging.info( "Perform system packages update as the peer's " "target_commit is the latest one %s", target_commit) else: logging.info("Skip updating system packages on the peer.") needs_system_update = 'false' logging.info('copy the file over to the peer') try: cur_dir = '/tmp/' bundle = BUNDLE_TEMPLATE.format(target_commit) bundle_path = os.path.join(cur_dir, bundle) logging.debug('package location is %s', bundle_path) peer.host.send_file(bundle_path, '/tmp/') except: logging.error('copying the file failed %s ', sys.exc_info()) logging.error(str(os.listdir(cur_dir))) return False # Backward compatibility for deploying the chamleeon bundle: # use 'PY_VERSION=python3' only when the target_commit is not in # the specified python2 commits. When py_version_option is empty, # python2 will be used in the deployment. python2_commits_filename = GS_PUBLIC + PYTHON2_COMMITS_FILENAME python2_commits = read_google_cloud_file(python2_commits_filename) logging.info('target_commit %s python2_commits %s ', target_commit, python2_commits) if bool(python2_commits) and target_commit in python2_commits: py_version_option = '' else: py_version_option = 'PY_VERSION=python3' HOST_NOW = datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') logging.info('running make on peer') cmd = ('cd %s && rm -rf %s && tar zxf %s &&' 'cd %s && find -exec touch -c {} \; &&' 'make install REMOTE_INSTALL=TRUE ' 'HOST_NOW="%s" BUNDLE_VERSION=%s ' 'CHAMELEON_BOARD=%s NEEDS_SYSTEM_UPDATE=%s ' '%s && rm %s%s' % (cur_dir, BUNDLE_DIR, bundle, BUNDLE_DIR, HOST_NOW, BUNDLE_VERSION, CHAMELEON_BOARD, needs_system_update, py_version_option, cur_dir, bundle)) logging.info(cmd) status, _ = run_cmd(peer, cmd) if not status: logging.info('make failed') return False logging.info('chameleond installed on peer') return True def restart_check_chameleond(peer): """restart chameleond and make sure it is running.""" restart_cmd = 'sudo /etc/init.d/chameleond restart' start_cmd = 'sudo /etc/init.d/chameleond start' status_cmd = 'sudo /etc/init.d/chameleond status' status, _ = run_cmd(peer, restart_cmd) if not status: status, _ = run_cmd(peer, start_cmd) if not status: logging.error('restarting/starting chamleond failed') # #TODO: Refactor so that we wait for all peer devices all together. # # Wait till chameleond initialization is complete time.sleep(5) status, output = run_cmd(peer, status_cmd) expected_output = 'chameleond is running' return status and expected_output in output def update_peer(force_system_packages_update, peer, target_commit, latest_commit): """Update the chameleond on peer devices if required @param force_system_packages_update: True to update system packages of the peer @param peer: btpeer to be updated @param target_commit: target git commit @param latest_commit: the latest git commit in the lab_commit_map, which is defined in the bluetooth_commits.yaml @returns: (True, None) if update succeeded (False, reason) if update failed """ if peer.get_platform() != 'RASPI': logging.error('Unsupported peer %s',str(peer.host)) return False, 'Unsupported peer' if not perform_update(force_system_packages_update, peer, target_commit, latest_commit): return False, 'Update failed' if not restart_check_chameleond(peer): return False, 'Unable to start chameleond' if is_update_needed(peer, target_commit): return False, 'Commit not updated after upgrade' logging.info('updating chameleond succeded') return True, '' def update_all_peers(host, raise_error=False): """Update the chameleond on all peer devices of the given host @param host: the DUT, usually a Chromebook @param raise_error: set this to True to raise an error if any @returns: True if _update_all_peers success False if raise_error=False and _update_all_peers failed @raises: error.TestFail if raise_error=True and _update_all_peers failed """ fail_reason = _update_all_peers(host) if fail_reason: if raise_error: raise error.TestFail(fail_reason) logging.error(fail_reason) return False else: return True def _update_all_peers(host): """Update the chameleond on all peer devices of an host""" try: target_commit = get_target_commit(host) latest_commit = get_latest_commit(host) if target_commit is None: return 'Unable to get current commit' if latest_commit is None: return 'Unable to get latest commit' if host.btpeer_list == []: return 'Bluetooth Peer not present' peers_to_update = [ p for p in host.btpeer_list if is_update_needed(p, target_commit) ] if not peers_to_update: logging.info('No peer needed update') return logging.debug('At least one peer needs update') if not download_installation_files(host, target_commit): return 'Unable to download installation files' # TODO(b:160782273) Make this parallel failed_peers = [] host_is_in_lab_next_hosts = is_in_lab_next_hosts(host) for peer in peers_to_update: updated, reason = update_peer(host_is_in_lab_next_hosts, peer, target_commit, latest_commit) if updated: logging.info('peer %s updated successfully', str(peer.host)) else: failed_peers.append((str(peer.host), reason)) if failed_peers: return 'peer update failed (host, reason): %s' % failed_peers except Exception as e: return 'Exception raised in _update_all_peers: %s' % e finally: if not cleanup(host, target_commit): return 'Update peer cleanup failed' def get_bluetooth_commits_yaml(host, method='from_cloud'): """Get the bluetooth_commit.yaml file This function has the side effect that it will set the attribute, host.bluetooth_commits_yaml for caching. @param host: the DUT, usually a Chromebook @param method: from_cloud: download the YAML file from the Google Cloud Storage from_local: download the YAML file from local, this option is convienent for testing @returns: bluetooth_commits.yaml file if exists @raises: error.TestFail if failed to get the yaml file """ try: if not hasattr(host, 'bluetooth_commits_yaml'): if method == 'from_cloud': src = GS_PUBLIC + COMMITS_FILENAME host.bluetooth_commits_yaml = yaml.safe_load( read_google_cloud_file(src)) elif method == 'from_local': yaml_file_path = os.path.dirname(os.path.realpath(__file__)) yaml_file_path = os.path.join(yaml_file_path, 'bluetooth_commits.yaml') with open(yaml_file_path) as f: yaml_file = f.read() host.bluetooth_commits_yaml = yaml.safe_load(yaml_file) else: raise error.TestError('invalid YAML download method: %s', method) logging.info('content of yaml file: %s', host.bluetooth_commits_yaml) except Exception as e: logging.error('Error getting bluetooth_commits.yaml: %s', e) return host.bluetooth_commits_yaml def is_in_lab_next_hosts(host): """Check if the host is in the lab_next_hosts This function has the side effect that it will set the attribute, host.is_in_lab_next_hosts for caching. @param host: the DUT, usually a Chromebook @returns: True if the host is in the lab_next_hosts, False otherwise. """ if not hasattr(host, 'is_in_lab_next_hosts'): host_build = host.get_release_version() content = get_bluetooth_commits_yaml(host) if (host_name(host) in content.get('lab_next_hosts') and host_build == content.get('lab_next_build')): host.is_in_lab_next_hosts = True else: host.is_in_lab_next_hosts = False return host.is_in_lab_next_hosts def get_latest_commit(host): """ Get the latest_commmit in the bluetooth_commits.yaml @param host: the DUT, usually a Chromebook @returns: the latest commit hash if exists """ try: content = get_bluetooth_commits_yaml(host) latest_commit = content.get('lab_commit_map')[0]['chameleon_commit'] logging.info('The latest commit is: %s', latest_commit) except Exception as e: logging.error('Exception in get_latest_commit(): ', str(e)) return latest_commit def host_name(host): """ Get the name of a host @param host: the DUT, usually a Chromebook @returns: the hostname if exists, None otherwise """ if hasattr(host, 'hostname'): return host.hostname.rstrip('.cros') else: return None def get_target_commit(host): """ Get the target commit per the DUT Download the yaml file containing the commits, parse its contents, and cleanup. The yaml file looks like ------------------------ lab_curr_commit: d732343cf lab_next_build: 13721.0.0 lab_next_commit: 71be114 lab_next_hosts: - chromeos15-row8-rack5-host1 - chromeos15-row5-rack7-host7 - chromeos15-row5-rack1-host4 lab_commit_map: - build_version: 14461.0.0 chameleon_commit: 87bed79 - build_version: 00000.0.0 chameleon_commit: 881f0e0 The lab_next_commit will be used only when 3 conditions are satisfied - the lab_next_commit is non-empty - the hostname of the DUT can be found in lab_next_hosts - the host_build of the DUT is the same as lab_next_build Tests of next build will go back to the commits in the lab_commit_map automatically. The purpose is that in case lab_next_commit is not stable, the DUTs will go back to use the supposed stable commit according to the lab_commit_map. Test server will choose the biggest build_version in the lab_commit_map which is smaller than the host_build. On the other hand, if lab_next_commit is stable by juding from the lab dashboard, someone can then copy lab_next_build to lab_commit_map manually. @param host: the DUT, usually a Chromebook @returns commit in case of success; None in case of failure """ hostname = host_name(host) try: content = get_bluetooth_commits_yaml(host) lab_next_commit = content.get('lab_next_commit') if (is_in_lab_next_hosts(host) and bool(lab_next_commit)): commit = lab_next_commit logging.info( 'target commit of the host %s is: %s from the ' 'lab_next_commit', hostname, commit) else: host_build = host.get_release_version() lab_commit_map = content.get('lab_commit_map') for item in lab_commit_map: build = item['build_version'] if is_chromeos_build_greater_or_equal(host_build, build): commit = item['chameleon_commit'] break else: logging.error('lab_commit_map is corrupted') commit = None logging.info( 'target commit of the host %s is: %s from the ' 'lab_commit_map', hostname, commit) except Exception as e: logging.error('Exception %s in get_target_commit()', str(e)) commit = None return commit def download_installation_files(host, commit): """ Download the chameleond installation bundle""" src_path = GS_PUBLIC + BUNDLE_TEMPLATE.format(commit) dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit) logging.debug('chamelond bundle path is %s', src_path) logging.debug('bundle path in DUT is %s', dest_path) cmd = 'gsutil cp {} {}'.format(src_path, dest_path) try: result = utils.run(cmd) if result.exit_status != 0: logging.error('Downloading the chameleond bundle failed with %d', result.exit_status) return False # Send file to DUT from the test server host.send_file(dest_path, dest_path) logging.debug('file send to %s %s',host, dest_path) return True except Exception as e: logging.error('exception %s in download_installation_files', str(e)) return False def cleanup(host, commit): """ Cleanup the installation file from server.""" dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit) # remove file from test server if not os.path.exists(dest_path): logging.debug('File %s not found', dest_path) return True try: logging.debug('Remove file %s', dest_path) os.remove(dest_path) # remove file from the DUT result = host.run('rm {}'.format(dest_path)) if result.exit_status != 0: logging.error('Unable to delete %s on dut', dest_path) return False return True except Exception as e: logging.error('Exception %s in cleanup', str(e)) return False