1# Lint as: python2, python3 2# Copyright 2020 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 6""" 7This file provides functions to implement bluetooth_PeerUpdate test 8which downloads chameleond bundle from google cloud storage and updates 9peer device associated with a DUT 10""" 11 12from __future__ import absolute_import 13 14import logging 15import os 16import sys 17import tempfile 18import time 19import yaml 20 21from datetime import datetime 22 23import common 24from autotest_lib.client.bin import utils 25from autotest_lib.client.common_lib import error 26 27 28# The location of the package in the cloud 29GS_PUBLIC = 'gs://chromeos-localmirror/distfiles/bluetooth_peer_bundle/' 30 31# NAME of the file that stores python2 commits info in the cloud 32PYTHON2_COMMITS_FILENAME = 'bluetooth_python2_commits' 33 34# NAME of the file that stores commits info in the Google cloud storage. 35COMMITS_FILENAME = 'bluetooth_commits.yaml' 36 37 38# The following needs to be kept in sync with values chameleond code 39BUNDLE_TEMPLATE='chameleond-0.0.2-{}.tar.gz' # Name of the chamleond package 40BUNDLE_DIR = 'chameleond-0.0.2' 41BUNDLE_VERSION = '9999' 42CHAMELEON_BOARD = 'fpga_tio' 43 44 45def run_cmd(peer, cmd): 46 """A wrapper around host.run().""" 47 try: 48 logging.info('executing command %s on peer',cmd) 49 result = peer.host.run(cmd) 50 logging.info('exit_status is %s', result.exit_status) 51 logging.info('stdout is %s stderr is %s', result.stdout, result.stderr) 52 output = result.stderr if result.stderr else result.stdout 53 if result.exit_status == 0: 54 return True, output 55 else: 56 return False, output 57 except error.AutoservRunError as e: 58 logging.error('Error while running cmd %s %s', cmd, e) 59 return False, None 60 61 62def read_google_cloud_file(filename): 63 """ Check if update is required 64 65 Read the contents of the Googlle cloud file. 66 67 @param filename: the filename of the Google cloud file 68 69 @returns: the contexts of the file if successful; None otherwise. 70 """ 71 try: 72 with tempfile.NamedTemporaryFile() as tmp_file: 73 tmp_filename = tmp_file.name 74 cmd = 'gsutil cp {} {}'.format(filename, tmp_filename) 75 result = utils.run(cmd) 76 if result.exit_status != 0: 77 logging.error('Downloading file %s failed with %s', 78 filename, result.exit_status) 79 return None 80 with open(tmp_filename) as f: 81 content = f.read() 82 logging.debug('content of the file %s: %s', filename, content) 83 return content 84 except Exception as e: 85 logging.error('Error in reading %s', filename) 86 return None 87 88 89def is_update_needed(peer, target_commit): 90 """ Check if update is required 91 92 Update if the commit hash doesn't match 93 94 @returns: True/False 95 """ 96 return not is_commit_hash_equal(peer, target_commit) 97 98 99def is_commit_hash_equal(peer, target_commit): 100 """ Check if chameleond commit hash is the expected one""" 101 try: 102 commit = peer.get_bt_commit_hash() 103 except: 104 logging.error('Getting the commit hash failed. Updating the peer %s', 105 sys.exc_info()) 106 return True 107 108 logging.debug('commit %s found on peer %s', commit, peer.host) 109 return commit == target_commit 110 111 112def is_chromeos_build_greater_or_equal(build1, build2): 113 """ Check if build1 is greater or equal to the build2""" 114 build1 = [int(key1) for key1 in build1.split('.')] 115 build2 = [int(key2) for key2 in build2.split('.')] 116 for key1, key2 in zip(build1, build2): 117 if key1 > key2: 118 return True 119 elif key1 == key2: 120 continue 121 else: 122 return False 123 return True 124 125 126def perform_update(force_system_packages_update, peer, target_commit, 127 latest_commit): 128 """ Update the chameleond on the peer 129 130 @param force_system_packages_update: True to update system packages of the 131 peer. 132 @param peer: btpeer to be updated 133 @param target_commit: target git commit 134 @param latest_commit: the latest git commit in the lab_commit_map, which 135 is defined in the bluetooth_commits.yaml 136 137 @returns: True if the update process is success, False otherwise 138 """ 139 140 # Only update the system when the target commit is the latest. 141 # Since system packages are backward compatible so it's safe to keep 142 # it the latest. 143 needs_system_update = 'true' 144 if force_system_packages_update: 145 logging.info("Forced system packages update on the peer.") 146 elif target_commit == latest_commit: 147 logging.info( 148 "Perform system packages update as the peer's " 149 "target_commit is the latest one %s", target_commit) 150 else: 151 logging.info("Skip updating system packages on the peer.") 152 needs_system_update = 'false' 153 154 logging.info('copy the file over to the peer') 155 try: 156 cur_dir = '/tmp/' 157 bundle = BUNDLE_TEMPLATE.format(target_commit) 158 bundle_path = os.path.join(cur_dir, bundle) 159 logging.debug('package location is %s', bundle_path) 160 161 peer.host.send_file(bundle_path, '/tmp/') 162 except: 163 logging.error('copying the file failed %s ', sys.exc_info()) 164 logging.error(str(os.listdir(cur_dir))) 165 return False 166 167 # Backward compatibility for deploying the chamleeon bundle: 168 # use 'PY_VERSION=python3' only when the target_commit is not in 169 # the specified python2 commits. When py_version_option is empty, 170 # python2 will be used in the deployment. 171 python2_commits_filename = GS_PUBLIC + PYTHON2_COMMITS_FILENAME 172 python2_commits = read_google_cloud_file(python2_commits_filename) 173 logging.info('target_commit %s python2_commits %s ', 174 target_commit, python2_commits) 175 if bool(python2_commits) and target_commit in python2_commits: 176 py_version_option = '' 177 else: 178 py_version_option = 'PY_VERSION=python3' 179 180 HOST_NOW = datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') 181 logging.info('running make on peer') 182 cmd = ('cd %s && rm -rf %s && tar zxf %s &&' 183 'cd %s && find -exec touch -c {} \; &&' 184 'make install REMOTE_INSTALL=TRUE ' 185 'HOST_NOW="%s" BUNDLE_VERSION=%s ' 186 'CHAMELEON_BOARD=%s NEEDS_SYSTEM_UPDATE=%s ' 187 '%s && rm %s%s' % 188 (cur_dir, BUNDLE_DIR, bundle, BUNDLE_DIR, HOST_NOW, BUNDLE_VERSION, 189 CHAMELEON_BOARD, needs_system_update, py_version_option, cur_dir, 190 bundle)) 191 logging.info(cmd) 192 status, _ = run_cmd(peer, cmd) 193 if not status: 194 logging.info('make failed') 195 return False 196 197 logging.info('chameleond installed on peer') 198 return True 199 200 201def restart_check_chameleond(peer): 202 """restart chameleond and make sure it is running.""" 203 204 restart_cmd = 'sudo /etc/init.d/chameleond restart' 205 start_cmd = 'sudo /etc/init.d/chameleond start' 206 status_cmd = 'sudo /etc/init.d/chameleond status' 207 208 status, _ = run_cmd(peer, restart_cmd) 209 if not status: 210 status, _ = run_cmd(peer, start_cmd) 211 if not status: 212 logging.error('restarting/starting chamleond failed') 213 # 214 #TODO: Refactor so that we wait for all peer devices all together. 215 # 216 # Wait till chameleond initialization is complete 217 time.sleep(5) 218 219 status, output = run_cmd(peer, status_cmd) 220 expected_output = 'chameleond is running' 221 return status and expected_output in output 222 223 224def update_peer(force_system_packages_update, peer, target_commit, 225 latest_commit): 226 """Update the chameleond on peer devices if required 227 228 @param force_system_packages_update: True to update system packages of the 229 peer 230 @param peer: btpeer to be updated 231 @param target_commit: target git commit 232 @param latest_commit: the latest git commit in the lab_commit_map, which 233 is defined in the bluetooth_commits.yaml 234 235 @returns: (True, None) if update succeeded 236 (False, reason) if update failed 237 """ 238 239 if peer.get_platform() != 'RASPI': 240 logging.error('Unsupported peer %s',str(peer.host)) 241 return False, 'Unsupported peer' 242 243 if not perform_update(force_system_packages_update, peer, target_commit, 244 latest_commit): 245 return False, 'Update failed' 246 247 if not restart_check_chameleond(peer): 248 return False, 'Unable to start chameleond' 249 250 if is_update_needed(peer, target_commit): 251 return False, 'Commit not updated after upgrade' 252 253 logging.info('updating chameleond succeded') 254 return True, '' 255 256 257def update_all_peers(host, raise_error=False): 258 """Update the chameleond on all peer devices of the given host 259 260 @param host: the DUT, usually a Chromebook 261 @param raise_error: set this to True to raise an error if any 262 263 @returns: True if _update_all_peers success 264 False if raise_error=False and _update_all_peers failed 265 266 @raises: error.TestFail if raise_error=True and _update_all_peers failed 267 """ 268 fail_reason = _update_all_peers(host) 269 270 if fail_reason: 271 if raise_error: 272 raise error.TestFail(fail_reason) 273 logging.error(fail_reason) 274 return False 275 else: 276 return True 277 278 279def _update_all_peers(host): 280 """Update the chameleond on all peer devices of an host""" 281 try: 282 target_commit = get_target_commit(host) 283 latest_commit = get_latest_commit(host) 284 285 if target_commit is None: 286 return 'Unable to get current commit' 287 288 if latest_commit is None: 289 return 'Unable to get latest commit' 290 291 if host.btpeer_list == []: 292 return 'Bluetooth Peer not present' 293 294 peers_to_update = [ 295 p for p in host.btpeer_list 296 if is_update_needed(p, target_commit) 297 ] 298 299 if not peers_to_update: 300 logging.info('No peer needed update') 301 return 302 logging.debug('At least one peer needs update') 303 304 if not download_installation_files(host, target_commit): 305 return 'Unable to download installation files' 306 307 # TODO(b:160782273) Make this parallel 308 failed_peers = [] 309 host_is_in_lab_next_hosts = is_in_lab_next_hosts(host) 310 for peer in peers_to_update: 311 updated, reason = update_peer(host_is_in_lab_next_hosts, peer, 312 target_commit, latest_commit) 313 if updated: 314 logging.info('peer %s updated successfully', str(peer.host)) 315 else: 316 failed_peers.append((str(peer.host), reason)) 317 318 if failed_peers: 319 return 'peer update failed (host, reason): %s' % failed_peers 320 321 except Exception as e: 322 return 'Exception raised in _update_all_peers: %s' % e 323 finally: 324 if not cleanup(host, target_commit): 325 return 'Update peer cleanup failed' 326 327 328def get_bluetooth_commits_yaml(host, method='from_cloud'): 329 """Get the bluetooth_commit.yaml file 330 331 This function has the side effect that it will set the attribute, 332 host.bluetooth_commits_yaml for caching. 333 334 @param host: the DUT, usually a Chromebook 335 @param method: from_cloud: download the YAML file from the Google Cloud 336 Storage 337 from_local: download the YAML file from local, this option 338 is convienent for testing 339 @returns: bluetooth_commits.yaml file if exists 340 341 @raises: error.TestFail if failed to get the yaml file 342 """ 343 try: 344 if not hasattr(host, 'bluetooth_commits_yaml'): 345 if method == 'from_cloud': 346 src = GS_PUBLIC + COMMITS_FILENAME 347 host.bluetooth_commits_yaml = yaml.safe_load( 348 read_google_cloud_file(src)) 349 elif method == 'from_local': 350 yaml_file_path = os.path.dirname(os.path.realpath(__file__)) 351 yaml_file_path = os.path.join(yaml_file_path, 352 'bluetooth_commits.yaml') 353 with open(yaml_file_path) as f: 354 yaml_file = f.read() 355 host.bluetooth_commits_yaml = yaml.safe_load(yaml_file) 356 else: 357 raise error.TestError('invalid YAML download method: %s', 358 method) 359 logging.info('content of yaml file: %s', 360 host.bluetooth_commits_yaml) 361 except Exception as e: 362 logging.error('Error getting bluetooth_commits.yaml: %s', e) 363 364 return host.bluetooth_commits_yaml 365 366 367def is_in_lab_next_hosts(host): 368 """Check if the host is in the lab_next_hosts 369 370 This function has the side effect that it will set the attribute, 371 host.is_in_lab_next_hosts for caching. 372 373 @param host: the DUT, usually a Chromebook 374 375 @returns: True if the host is in the lab_next_hosts, False otherwise. 376 """ 377 if not hasattr(host, 'is_in_lab_next_hosts'): 378 host_build = host.get_release_version() 379 content = get_bluetooth_commits_yaml(host) 380 381 if (host_name(host) in content.get('lab_next_hosts') 382 and host_build == content.get('lab_next_build')): 383 host.is_in_lab_next_hosts = True 384 else: 385 host.is_in_lab_next_hosts = False 386 return host.is_in_lab_next_hosts 387 388 389def get_latest_commit(host): 390 """ Get the latest_commmit in the bluetooth_commits.yaml 391 392 @param host: the DUT, usually a Chromebook 393 394 @returns: the latest commit hash if exists 395 """ 396 try: 397 content = get_bluetooth_commits_yaml(host) 398 latest_commit = content.get('lab_commit_map')[0]['chameleon_commit'] 399 logging.info('The latest commit is: %s', latest_commit) 400 except Exception as e: 401 logging.error('Exception in get_latest_commit(): ', str(e)) 402 return latest_commit 403 404 405def host_name(host): 406 """ Get the name of a host 407 408 @param host: the DUT, usually a Chromebook 409 410 @returns: the hostname if exists, None otherwise 411 """ 412 if hasattr(host, 'hostname'): 413 return host.hostname.rstrip('.cros') 414 else: 415 return None 416 417 418def get_target_commit(host): 419 """ Get the target commit per the DUT 420 421 Download the yaml file containing the commits, parse its contents, 422 and cleanup. 423 424 The yaml file looks like 425 ------------------------ 426 lab_curr_commit: d732343cf 427 lab_next_build: 13721.0.0 428 lab_next_commit: 71be114 429 lab_next_hosts: 430 - chromeos15-row8-rack5-host1 431 - chromeos15-row5-rack7-host7 432 - chromeos15-row5-rack1-host4 433 lab_commit_map: 434 - build_version: 14461.0.0 435 chameleon_commit: 87bed79 436 - build_version: 00000.0.0 437 chameleon_commit: 881f0e0 438 439 The lab_next_commit will be used only when 3 conditions are satisfied 440 - the lab_next_commit is non-empty 441 - the hostname of the DUT can be found in lab_next_hosts 442 - the host_build of the DUT is the same as lab_next_build 443 444 Tests of next build will go back to the commits in the lab_commit_map 445 automatically. The purpose is that in case lab_next_commit is not stable, 446 the DUTs will go back to use the supposed stable commit according to the 447 lab_commit_map. Test server will choose the biggest build_version in the 448 lab_commit_map which is smaller than the host_build. 449 450 On the other hand, if lab_next_commit is stable by juding from the lab 451 dashboard, someone can then copy lab_next_build to lab_commit_map manually. 452 453 @param host: the DUT, usually a Chromebook 454 455 @returns commit in case of success; None in case of failure 456 """ 457 hostname = host_name(host) 458 459 try: 460 content = get_bluetooth_commits_yaml(host) 461 462 lab_next_commit = content.get('lab_next_commit') 463 if (is_in_lab_next_hosts(host) and bool(lab_next_commit)): 464 commit = lab_next_commit 465 logging.info( 466 'target commit of the host %s is: %s from the ' 467 'lab_next_commit', hostname, commit) 468 else: 469 host_build = host.get_release_version() 470 lab_commit_map = content.get('lab_commit_map') 471 for item in lab_commit_map: 472 build = item['build_version'] 473 if is_chromeos_build_greater_or_equal(host_build, build): 474 commit = item['chameleon_commit'] 475 break 476 else: 477 logging.error('lab_commit_map is corrupted') 478 commit = None 479 logging.info( 480 'target commit of the host %s is: %s from the ' 481 'lab_commit_map', hostname, commit) 482 483 except Exception as e: 484 logging.error('Exception %s in get_target_commit()', str(e)) 485 commit = None 486 return commit 487 488 489def download_installation_files(host, commit): 490 """ Download the chameleond installation bundle""" 491 src_path = GS_PUBLIC + BUNDLE_TEMPLATE.format(commit) 492 dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit) 493 logging.debug('chamelond bundle path is %s', src_path) 494 logging.debug('bundle path in DUT is %s', dest_path) 495 496 cmd = 'gsutil cp {} {}'.format(src_path, dest_path) 497 try: 498 result = utils.run(cmd) 499 if result.exit_status != 0: 500 logging.error('Downloading the chameleond bundle failed with %d', 501 result.exit_status) 502 return False 503 # Send file to DUT from the test server 504 host.send_file(dest_path, dest_path) 505 logging.debug('file send to %s %s',host, dest_path) 506 return True 507 except Exception as e: 508 logging.error('exception %s in download_installation_files', str(e)) 509 return False 510 511 512def cleanup(host, commit): 513 """ Cleanup the installation file from server.""" 514 515 dest_path = '/tmp/' + BUNDLE_TEMPLATE.format(commit) 516 # remove file from test server 517 if not os.path.exists(dest_path): 518 logging.debug('File %s not found', dest_path) 519 return True 520 521 try: 522 logging.debug('Remove file %s', dest_path) 523 os.remove(dest_path) 524 525 # remove file from the DUT 526 result = host.run('rm {}'.format(dest_path)) 527 if result.exit_status != 0: 528 logging.error('Unable to delete %s on dut', dest_path) 529 return False 530 return True 531 except Exception as e: 532 logging.error('Exception %s in cleanup', str(e)) 533 return False 534