1# Copyright (c) 2016 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 module includes all moblab-related RPCs. These RPCs can only be run 7on moblab. 8""" 9 10# The boto module is only available/used in Moblab for validation of cloud 11# storage access. The module is not available in the test lab environment, 12# and the import error is handled. 13try: 14 import boto 15except ImportError: 16 boto = None 17 18import ConfigParser 19import common 20import logging 21import os 22import re 23import shutil 24import socket 25import StringIO 26import subprocess 27 28from autotest_lib.client.common_lib import error 29from autotest_lib.client.common_lib import global_config, utils, site_utils 30from autotest_lib.frontend.afe import models 31from autotest_lib.frontend.afe import rpc_utils 32from autotest_lib.server import frontend 33from autotest_lib.server.hosts import moblab_host 34 35 36_CONFIG = global_config.global_config 37MOBLAB_BOTO_LOCATION = '/home/moblab/.boto' 38 39# Google Cloud Storage bucket url regex pattern. The pattern is used to extract 40# the bucket name from the bucket URL. For example, "gs://image_bucket/google" 41# should result in a bucket name "image_bucket". 42GOOGLE_STORAGE_BUCKET_URL_PATTERN = re.compile( 43 r'gs://(?P<bucket>[a-zA-Z][a-zA-Z0-9-_]*)/?.*') 44 45# Contants used in Json RPC field names. 46_IMAGE_STORAGE_SERVER = 'image_storage_server' 47_GS_ACCESS_KEY_ID = 'gs_access_key_id' 48_GS_SECRETE_ACCESS_KEY = 'gs_secret_access_key' 49_RESULT_STORAGE_SERVER = 'results_storage_server' 50_USE_EXISTING_BOTO_FILE = 'use_existing_boto_file' 51 52# Location where dhcp leases are stored. 53_DHCPD_LEASES = '/var/lib/dhcp/dhcpd.leases' 54 55# File where information about the current device is stored. 56_ETC_LSB_RELEASE = '/etc/lsb-release' 57 58@rpc_utils.moblab_only 59def get_config_values(): 60 """Returns all config values parsed from global and shadow configs. 61 62 Config values are grouped by sections, and each section is composed of 63 a list of name value pairs. 64 """ 65 sections =_CONFIG.get_sections() 66 config_values = {} 67 for section in sections: 68 config_values[section] = _CONFIG.config.items(section) 69 return rpc_utils.prepare_for_serialization(config_values) 70 71 72def _write_config_file(config_file, config_values, overwrite=False): 73 """Writes out a configuration file. 74 75 @param config_file: The name of the configuration file. 76 @param config_values: The ConfigParser object. 77 @param ovewrite: Flag on if overwriting is allowed. 78 """ 79 if not config_file: 80 raise error.RPCException('Empty config file name.') 81 if not overwrite and os.path.exists(config_file): 82 raise error.RPCException('Config file already exists.') 83 84 if config_values: 85 with open(config_file, 'w') as config_file: 86 config_values.write(config_file) 87 88 89def _read_original_config(): 90 """Reads the orginal configuratino without shadow. 91 92 @return: A configuration object, see global_config_class. 93 """ 94 original_config = global_config.global_config_class() 95 original_config.set_config_files(shadow_file='') 96 return original_config 97 98 99def _read_raw_config(config_file): 100 """Reads the raw configuration from a configuration file. 101 102 @param: config_file: The path of the configuration file. 103 104 @return: A ConfigParser object. 105 """ 106 shadow_config = ConfigParser.RawConfigParser() 107 shadow_config.read(config_file) 108 return shadow_config 109 110 111def _get_shadow_config_from_partial_update(config_values): 112 """Finds out the new shadow configuration based on a partial update. 113 114 Since the input is only a partial config, we should not lose the config 115 data inside the existing shadow config file. We also need to distinguish 116 if the input config info overrides with a new value or reverts back to 117 an original value. 118 119 @param config_values: See get_moblab_settings(). 120 121 @return: The new shadow configuration as ConfigParser object. 122 """ 123 original_config = _read_original_config() 124 existing_shadow = _read_raw_config(_CONFIG.shadow_file) 125 for section, config_value_list in config_values.iteritems(): 126 for key, value in config_value_list: 127 if original_config.get_config_value(section, key, 128 default='', 129 allow_blank=True) != value: 130 if not existing_shadow.has_section(section): 131 existing_shadow.add_section(section) 132 existing_shadow.set(section, key, value) 133 elif existing_shadow.has_option(section, key): 134 existing_shadow.remove_option(section, key) 135 return existing_shadow 136 137 138def _update_partial_config(config_values): 139 """Updates the shadow configuration file with a partial config udpate. 140 141 @param config_values: See get_moblab_settings(). 142 """ 143 existing_config = _get_shadow_config_from_partial_update(config_values) 144 _write_config_file(_CONFIG.shadow_file, existing_config, True) 145 146 147@rpc_utils.moblab_only 148def update_config_handler(config_values): 149 """Update config values and override shadow config. 150 151 @param config_values: See get_moblab_settings(). 152 """ 153 original_config = _read_original_config() 154 new_shadow = ConfigParser.RawConfigParser() 155 for section, config_value_list in config_values.iteritems(): 156 for key, value in config_value_list: 157 if original_config.get_config_value(section, key, 158 default='', 159 allow_blank=True) != value: 160 if not new_shadow.has_section(section): 161 new_shadow.add_section(section) 162 new_shadow.set(section, key, value) 163 164 if not _CONFIG.shadow_file or not os.path.exists(_CONFIG.shadow_file): 165 raise error.RPCException('Shadow config file does not exist.') 166 _write_config_file(_CONFIG.shadow_file, new_shadow, True) 167 168 # TODO (sbasi) crbug.com/403916 - Remove the reboot command and 169 # instead restart the services that rely on the config values. 170 os.system('sudo reboot') 171 172 173@rpc_utils.moblab_only 174def reset_config_settings(): 175 """Reset moblab shadow config.""" 176 with open(_CONFIG.shadow_file, 'w') as config_file: 177 pass 178 os.system('sudo reboot') 179 180 181@rpc_utils.moblab_only 182def reboot_moblab(): 183 """Simply reboot the device.""" 184 os.system('sudo reboot') 185 186 187@rpc_utils.moblab_only 188def set_boto_key(boto_key): 189 """Update the boto_key file. 190 191 @param boto_key: File name of boto_key uploaded through handle_file_upload. 192 """ 193 if not os.path.exists(boto_key): 194 raise error.RPCException('Boto key: %s does not exist!' % boto_key) 195 shutil.copyfile(boto_key, moblab_host.MOBLAB_BOTO_LOCATION) 196 197 198@rpc_utils.moblab_only 199def set_service_account_credential(service_account_filename): 200 """Update the service account credential file. 201 202 @param service_account_filename: Name of uploaded file through 203 handle_file_upload. 204 """ 205 if not os.path.exists(service_account_filename): 206 raise error.RPCException( 207 'Service account file: %s does not exist!' % 208 service_account_filename) 209 shutil.copyfile( 210 service_account_filename, 211 moblab_host.MOBLAB_SERVICE_ACCOUNT_LOCATION) 212 213 214@rpc_utils.moblab_only 215def set_launch_control_key(launch_control_key): 216 """Update the launch_control_key file. 217 218 @param launch_control_key: File name of launch_control_key uploaded through 219 handle_file_upload. 220 """ 221 if not os.path.exists(launch_control_key): 222 raise error.RPCException('Launch Control key: %s does not exist!' % 223 launch_control_key) 224 shutil.copyfile(launch_control_key, 225 moblab_host.MOBLAB_LAUNCH_CONTROL_KEY_LOCATION) 226 # Restart the devserver service. 227 os.system('sudo restart moblab-devserver-init') 228 229 230###########Moblab Config Wizard RPCs ####################### 231def _get_public_ip_address(socket_handle): 232 """Gets the public IP address. 233 234 Connects to Google DNS server using a socket and gets the preferred IP 235 address from the connection. 236 237 @param: socket_handle: a unix socket. 238 239 @return: public ip address as string. 240 """ 241 try: 242 socket_handle.settimeout(1) 243 socket_handle.connect(('8.8.8.8', 53)) 244 socket_name = socket_handle.getsockname() 245 if socket_name is not None: 246 logging.info('Got socket name from UDP socket.') 247 return socket_name[0] 248 logging.warn('Created UDP socket but with no socket_name.') 249 except socket.error: 250 logging.warn('Could not get socket name from UDP socket.') 251 return None 252 253 254def _get_network_info(): 255 """Gets the network information. 256 257 TCP socket is used to test the connectivity. If there is no connectivity, try to 258 get the public IP with UDP socket. 259 260 @return: a tuple as (public_ip_address, connected_to_internet). 261 """ 262 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 263 ip = _get_public_ip_address(s) 264 if ip is not None: 265 logging.info('Established TCP connection with well known server.') 266 return (ip, True) 267 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 268 return (_get_public_ip_address(s), False) 269 270 271@rpc_utils.moblab_only 272def get_network_info(): 273 """Returns the server ip addresses, and if the server connectivity. 274 275 The server ip addresses as an array of strings, and the connectivity as a 276 flag. 277 """ 278 network_info = {} 279 info = _get_network_info() 280 if info[0] is not None: 281 network_info['server_ips'] = [info[0]] 282 network_info['is_connected'] = info[1] 283 284 return rpc_utils.prepare_for_serialization(network_info) 285 286 287# Gets the boto configuration. 288def _get_boto_config(): 289 """Reads the boto configuration from the boto file. 290 291 @return: Boto configuration as ConfigParser object. 292 """ 293 boto_config = ConfigParser.ConfigParser() 294 boto_config.read(MOBLAB_BOTO_LOCATION) 295 return boto_config 296 297 298@rpc_utils.moblab_only 299def get_cloud_storage_info(): 300 """RPC handler to get the cloud storage access information. 301 """ 302 cloud_storage_info = {} 303 value =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER) 304 if value is not None: 305 cloud_storage_info[_IMAGE_STORAGE_SERVER] = value 306 value = _CONFIG.get_config_value('CROS', _RESULT_STORAGE_SERVER, 307 default=None) 308 if value is not None: 309 cloud_storage_info[_RESULT_STORAGE_SERVER] = value 310 311 boto_config = _get_boto_config() 312 sections = boto_config.sections() 313 314 if sections: 315 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = True 316 else: 317 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = False 318 if 'Credentials' in sections: 319 options = boto_config.options('Credentials') 320 if _GS_ACCESS_KEY_ID in options: 321 value = boto_config.get('Credentials', _GS_ACCESS_KEY_ID) 322 cloud_storage_info[_GS_ACCESS_KEY_ID] = value 323 if _GS_SECRETE_ACCESS_KEY in options: 324 value = boto_config.get('Credentials', _GS_SECRETE_ACCESS_KEY) 325 cloud_storage_info[_GS_SECRETE_ACCESS_KEY] = value 326 327 return rpc_utils.prepare_for_serialization(cloud_storage_info) 328 329 330def _get_bucket_name_from_url(bucket_url): 331 """Gets the bucket name from a bucket url. 332 333 @param: bucket_url: the bucket url string. 334 """ 335 if bucket_url: 336 match = GOOGLE_STORAGE_BUCKET_URL_PATTERN.match(bucket_url) 337 if match: 338 return match.group('bucket') 339 return None 340 341 342def _is_valid_boto_key(key_id, key_secret): 343 """Checks if the boto key is valid. 344 345 @param: key_id: The boto key id string. 346 @param: key_secret: The boto key string. 347 348 @return: A tuple as (valid_boolean, details_string). 349 """ 350 if not key_id or not key_secret: 351 return (False, "Empty key id or secret.") 352 conn = boto.connect_gs(key_id, key_secret) 353 try: 354 buckets = conn.get_all_buckets() 355 return (True, None) 356 except boto.exception.GSResponseError: 357 details = "The boto access key is not valid" 358 return (False, details) 359 finally: 360 conn.close() 361 362 363def _is_valid_bucket(key_id, key_secret, bucket_name): 364 """Checks if a bucket is valid and accessible. 365 366 @param: key_id: The boto key id string. 367 @param: key_secret: The boto key string. 368 @param: bucket name string. 369 370 @return: A tuple as (valid_boolean, details_string). 371 """ 372 if not key_id or not key_secret or not bucket_name: 373 return (False, "Server error: invalid argument") 374 conn = boto.connect_gs(key_id, key_secret) 375 bucket = conn.lookup(bucket_name) 376 conn.close() 377 if bucket: 378 return (True, None) 379 return (False, "Bucket %s does not exist." % bucket_name) 380 381 382def _is_valid_bucket_url(key_id, key_secret, bucket_url): 383 """Validates the bucket url is accessible. 384 385 @param: key_id: The boto key id string. 386 @param: key_secret: The boto key string. 387 @param: bucket url string. 388 389 @return: A tuple as (valid_boolean, details_string). 390 """ 391 bucket_name = _get_bucket_name_from_url(bucket_url) 392 if bucket_name: 393 return _is_valid_bucket(key_id, key_secret, bucket_name) 394 return (False, "Bucket url %s is not valid" % bucket_url) 395 396 397def _validate_cloud_storage_info(cloud_storage_info): 398 """Checks if the cloud storage information is valid. 399 400 @param: cloud_storage_info: The JSON RPC object for cloud storage info. 401 402 @return: A tuple as (valid_boolean, details_string). 403 """ 404 valid = True 405 details = None 406 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]: 407 key_id = cloud_storage_info[_GS_ACCESS_KEY_ID] 408 key_secret = cloud_storage_info[_GS_SECRETE_ACCESS_KEY] 409 valid, details = _is_valid_boto_key(key_id, key_secret) 410 411 if valid: 412 valid, details = _is_valid_bucket_url( 413 key_id, key_secret, cloud_storage_info[_IMAGE_STORAGE_SERVER]) 414 415 # allows result bucket to be empty. 416 if valid and cloud_storage_info[_RESULT_STORAGE_SERVER]: 417 valid, details = _is_valid_bucket_url( 418 key_id, key_secret, cloud_storage_info[_RESULT_STORAGE_SERVER]) 419 return (valid, details) 420 421 422def _create_operation_status_response(is_ok, details): 423 """Helper method to create a operation status reponse. 424 425 @param: is_ok: Boolean for if the operation is ok. 426 @param: details: A detailed string. 427 428 @return: A serialized JSON RPC object. 429 """ 430 status_response = {'status_ok': is_ok} 431 if details: 432 status_response['status_details'] = details 433 return rpc_utils.prepare_for_serialization(status_response) 434 435 436@rpc_utils.moblab_only 437def validate_cloud_storage_info(cloud_storage_info): 438 """RPC handler to check if the cloud storage info is valid. 439 440 @param cloud_storage_info: The JSON RPC object for cloud storage info. 441 """ 442 valid, details = _validate_cloud_storage_info(cloud_storage_info) 443 return _create_operation_status_response(valid, details) 444 445 446@rpc_utils.moblab_only 447def submit_wizard_config_info(cloud_storage_info): 448 """RPC handler to submit the cloud storage info. 449 450 @param cloud_storage_info: The JSON RPC object for cloud storage info. 451 """ 452 valid, details = _validate_cloud_storage_info(cloud_storage_info) 453 if not valid: 454 return _create_operation_status_response(valid, details) 455 config_update = {} 456 config_update['CROS'] = [ 457 (_IMAGE_STORAGE_SERVER, cloud_storage_info[_IMAGE_STORAGE_SERVER]), 458 (_RESULT_STORAGE_SERVER, cloud_storage_info[_RESULT_STORAGE_SERVER]) 459 ] 460 _update_partial_config(config_update) 461 462 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]: 463 boto_config = ConfigParser.RawConfigParser() 464 boto_config.add_section('Credentials') 465 boto_config.set('Credentials', _GS_ACCESS_KEY_ID, 466 cloud_storage_info[_GS_ACCESS_KEY_ID]) 467 boto_config.set('Credentials', _GS_SECRETE_ACCESS_KEY, 468 cloud_storage_info[_GS_SECRETE_ACCESS_KEY]) 469 _write_config_file(MOBLAB_BOTO_LOCATION, boto_config, True) 470 471 _CONFIG.parse_config_file() 472 services = ['moblab-devserver-init', 'moblab-apache-init', 473 'moblab-devserver-cleanup-init', ' moblab-gsoffloader_s-init', 474 'moblab-base-container-init', 'moblab-scheduler-init', 'moblab-gsoffloader-init'] 475 cmd = ';/sbin/restart '.join(services) 476 os.system(cmd) 477 478 return _create_operation_status_response(True, None) 479 480 481@rpc_utils.moblab_only 482def get_version_info(): 483 """ RPC handler to get informaiton about the version of the moblab. 484 485 @return: A serialized JSON RPC object. 486 """ 487 lines = open(_ETC_LSB_RELEASE).readlines() 488 version_response = { 489 x.split('=')[0]: x.split('=')[1] for x in lines if '=' in x} 490 version_response['MOBLAB_ID'] = site_utils.get_moblab_id(); 491 version_response['MOBLAB_MAC_ADDRESS'] = ( 492 site_utils.get_default_interface_mac_address()) 493 return rpc_utils.prepare_for_serialization(version_response) 494 495 496@rpc_utils.moblab_only 497def get_connected_dut_info(): 498 """ RPC handler to get informaiton about the DUTs connected to the moblab. 499 500 @return: A serialized JSON RPC object. 501 """ 502 # Make a list of the connected DUT's 503 leases = _get_dhcp_dut_leases() 504 505 # Get a list of the AFE configured DUT's 506 hosts = list(rpc_utils.get_host_query((), False, True, {})) 507 models.Host.objects.populate_relationships(hosts, models.Label, 508 'label_list') 509 configured_duts = {} 510 for host in hosts: 511 labels = [label.name for label in host.label_list] 512 labels.sort() 513 configured_duts[host.hostname] = ', '.join(labels) 514 515 return rpc_utils.prepare_for_serialization( 516 {'configured_duts': configured_duts, 517 'connected_duts': leases}) 518 519 520def _get_dhcp_dut_leases(): 521 """ Extract information about connected duts from the dhcp server. 522 523 @return: A dict of ipaddress to mac address for each device connected. 524 """ 525 lease_info = open(_DHCPD_LEASES).read() 526 527 leases = {} 528 for lease in lease_info.split('lease'): 529 if lease.find('binding state active;') != -1: 530 ipaddress = lease.split('\n')[0].strip(' {') 531 last_octet = int(ipaddress.split('.')[-1].strip()) 532 if last_octet > 150: 533 continue 534 mac_address_search = re.search('hardware ethernet (.*);', lease) 535 if mac_address_search: 536 leases[ipaddress] = mac_address_search.group(1) 537 return leases 538 539 540@rpc_utils.moblab_only 541def add_moblab_dut(ipaddress): 542 """ RPC handler to add a connected DUT to autotest. 543 544 @param ipaddress: IP address of the DUT. 545 546 @return: A string giving information about the status. 547 """ 548 cmd = '/usr/local/autotest/cli/atest host create %s &' % ipaddress 549 subprocess.call(cmd, shell=True) 550 return (True, 'DUT %s added to Autotest' % ipaddress) 551 552 553@rpc_utils.moblab_only 554def remove_moblab_dut(ipaddress): 555 """ RPC handler to remove DUT entry from autotest. 556 557 @param ipaddress: IP address of the DUT. 558 559 @return: True if the command succeeds without an exception 560 """ 561 models.Host.smart_get(ipaddress).delete() 562 return (True, 'DUT %s deleted from Autotest' % ipaddress) 563 564 565@rpc_utils.moblab_only 566def add_moblab_label(ipaddress, label_name): 567 """ RPC handler to add a label in autotest to a DUT entry. 568 569 @param ipaddress: IP address of the DUT. 570 @param label_name: The label name. 571 572 @return: A string giving information about the status. 573 """ 574 # Try to create the label in case it does not already exist. 575 label = None 576 try: 577 label = models.Label.add_object(name=label_name) 578 except: 579 label = models.Label.smart_get(label_name) 580 host_obj = models.Host.smart_get(ipaddress) 581 if label: 582 label.host_set.add(host_obj) 583 return (True, 'Added label %s to DUT %s' % (label_name, ipaddress)) 584 return (False, 'Failed to add label %s to DUT %s' % (label_name, ipaddress)) 585 586 587@rpc_utils.moblab_only 588def remove_moblab_label(ipaddress, label_name): 589 """ RPC handler to remove a label in autotest from a DUT entry. 590 591 @param ipaddress: IP address of the DUT. 592 @param label_name: The label name. 593 594 @return: A string giving information about the status. 595 """ 596 host_obj = models.Host.smart_get(ipaddress) 597 models.Label.smart_get(label_name).host_set.remove(host_obj) 598 return (True, 'Removed label %s from DUT %s' % (label_name, ipaddress)) 599 600 601def _get_connected_dut_labels(requested_label, only_first_label=True): 602 """ Query the DUT's attached to the moblab and return a filtered list of labels. 603 604 @param requested_label: the label name you are requesting. 605 @param only_first_label: if the device has the same label name multiple times only 606 return the first label value in the list. 607 608 @return: A de-duped list of requested dut labels attached to the moblab. 609 """ 610 hosts = list(rpc_utils.get_host_query((), False, True, {})) 611 if not hosts: 612 return [] 613 models.Host.objects.populate_relationships(hosts, models.Label, 614 'label_list') 615 labels = set() 616 for host in hosts: 617 for label in host.label_list: 618 if requested_label in label.name: 619 labels.add(label.name.replace(requested_label, '')) 620 if only_first_label: 621 break 622 return list(labels) 623 624 625@rpc_utils.moblab_only 626def get_connected_boards(): 627 """ RPC handler to get a list of the boards connected to the moblab. 628 629 @return: A de-duped list of board types attached to the moblab. 630 """ 631 boards = _get_connected_dut_labels("board:") 632 boards.sort() 633 return boards 634 635 636@rpc_utils.moblab_only 637def get_connected_pools(): 638 """ RPC handler to get a list of the pools labels on the DUT's connected. 639 640 @return: A de-duped list of pool labels. 641 """ 642 pools = _get_connected_dut_labels("pool:", False) 643 pools.sort() 644 return pools 645 646 647@rpc_utils.moblab_only 648def get_builds_for_board(board_name): 649 """ RPC handler to find the most recent builds for a board. 650 651 652 @param board_name: The name of a connected board. 653 @return: A list of string with the most recent builds for the latest 654 three milestones. 655 """ 656 return _get_builds_for_in_directory(board_name + '-release') 657 658 659@rpc_utils.moblab_only 660def get_firmware_for_board(board_name): 661 """ RPC handler to find the most recent firmware for a board. 662 663 664 @param board_name: The name of a connected board. 665 @return: A list of strings with the most recent firmware builds for the 666 latest three milestones. 667 """ 668 return _get_builds_for_in_directory(board_name + '-firmware') 669 670 671def _get_builds_for_in_directory(directory_name, milestone_limit=3, build_limit=20): 672 """ Fetch the most recent builds for the last three milestones from gcs. 673 674 675 @param directory_name: The sub-directory under the configured GCS image 676 storage bucket to search. 677 678 679 @return: A string list no longer than <milestone_limit> x <build_limit> items, 680 containing the most recent <build_limit> builds from the last 681 milestone_limit milestones. 682 """ 683 output = StringIO.StringIO() 684 gs_image_location =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER) 685 utils.run('gsutil', args=('ls', gs_image_location + directory_name), stdout_tee=output) 686 lines = output.getvalue().split('\n') 687 output.close() 688 builds = [line.replace(gs_image_location,'').strip('/ ') for line in lines if line != ''] 689 build_matcher = re.compile(r'^.*\/R([0-9]*)-.*') 690 build_map = {} 691 for build in builds: 692 match = build_matcher.match(build) 693 if match: 694 milestone = match.group(1) 695 if milestone not in build_map: 696 build_map[milestone] = [] 697 build_map[milestone].append(build) 698 milestones = build_map.keys() 699 milestones.sort() 700 milestones.reverse() 701 build_list = [] 702 for milestone in milestones[:milestone_limit]: 703 builds = build_map[milestone] 704 builds.reverse() 705 build_list.extend(builds[:build_limit]) 706 return build_list 707 708 709@rpc_utils.moblab_only 710def run_suite(board, build, suite, ro_firmware=None, rw_firmware=None, pool=None): 711 """ RPC handler to run a test suite. 712 713 @param board: a board name connected to the moblab. 714 @param build: a build name of a build in the GCS. 715 @param suite: the name of a suite to run 716 @param ro_firmware: Optional ro firmware build number to use. 717 @param rw_firmware: Optional rw firmware build number to use. 718 @param pool: Optional pool name to run the suite in. 719 720 @return: None 721 """ 722 builds = {'cros-version': build} 723 if rw_firmware: 724 builds['fwrw-version'] = rw_firmware 725 if ro_firmware: 726 builds['fwro-version'] = ro_firmware 727 afe = frontend.AFE(user='moblab') 728 afe.run('create_suite_job', board=board, builds=builds, name=suite, 729 pool=pool, run_prod_code=False, test_source_build=build, 730 wait_for_results=False) 731