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 """ 6 This module includes all moblab-related RPCs. These RPCs can only be run 7 on moblab. 8 """ 9 10 import ConfigParser 11 import common 12 import logging 13 import os 14 import re 15 import sys 16 import shutil 17 import socket 18 import StringIO 19 import subprocess 20 import time 21 import multiprocessing 22 import ctypes 23 24 from autotest_lib.client.common_lib import error 25 from autotest_lib.client.common_lib import global_config 26 from autotest_lib.client.common_lib import utils 27 from autotest_lib.frontend.afe import models 28 from autotest_lib.frontend.afe import rpc_utils 29 from autotest_lib.server import frontend 30 from autotest_lib.server.hosts import moblab_host 31 from chromite.lib import gs 32 33 _CONFIG = global_config.global_config 34 MOBLAB_BOTO_LOCATION = '/home/moblab/.boto' 35 CROS_CACHEDIR = '/mnt/moblab/cros_cache_apache' 36 37 # Google Cloud Storage bucket url regex pattern. The pattern is used to extract 38 # the bucket name from the bucket URL. For example, "gs://image_bucket/google" 39 # should result in a bucket name "image_bucket". 40 GOOGLE_STORAGE_BUCKET_URL_PATTERN = re.compile( 41 r'gs://(?P<bucket>[a-zA-Z][a-zA-Z0-9-_]*)/?.*') 42 43 # Contants used in Json RPC field names. 44 _IMAGE_STORAGE_SERVER = 'image_storage_server' 45 _GS_ACCESS_KEY_ID = 'gs_access_key_id' 46 _GS_SECRET_ACCESS_KEY = 'gs_secret_access_key' 47 _RESULT_STORAGE_SERVER = 'results_storage_server' 48 _USE_EXISTING_BOTO_FILE = 'use_existing_boto_file' 49 _CLOUD_NOTIFICATION_ENABLED = 'cloud_notification_enabled' 50 _WIFI_AP_NAME = 'wifi_dut_ap_name' 51 _WIFI_AP_PASS = 'wifi_dut_ap_pass' 52 53 # Location where dhcp leases are stored. 54 _DHCPD_LEASES = '/var/lib/dhcp/dhcpd.leases' 55 56 # File where information about the current device is stored. 57 _ETC_LSB_RELEASE = '/etc/lsb-release' 58 59 # ChromeOS update engine client binary location 60 _UPDATE_ENGINE_CLIENT = '/usr/bin/update_engine_client' 61 62 # Set the suite timeout per suite in minutes 63 # default is 24 hours 64 _DEFAULT_SUITE_TIMEOUT_MINS = 1440 65 _SUITE_TIMEOUT_MAP = { 66 'hardware_storagequal': 40320, 67 'hardware_storagequal_quick': 40320 68 } 69 70 # Full path to the correct gsutil command to run. 71 class GsUtil: 72 """Helper class to find correct gsutil command.""" 73 _GSUTIL_CMD = None 74 75 @classmethod 76 def get_gsutil_cmd(cls): 77 if not cls._GSUTIL_CMD: 78 cls._GSUTIL_CMD = gs.GSContext.GetDefaultGSUtilBin( 79 cache_dir=CROS_CACHEDIR) 80 81 return cls._GSUTIL_CMD 82 83 84 class BucketPerformanceTestException(Exception): 85 """Exception thrown when the command to test the bucket performance fails.""" 86 pass 87 88 @rpc_utils.moblab_only 89 def get_config_values(): 90 """Returns all config values parsed from global and shadow configs. 91 92 Config values are grouped by sections, and each section is composed of 93 a list of name value pairs. 94 """ 95 sections =_CONFIG.get_sections() 96 config_values = {} 97 for section in sections: 98 config_values[section] = _CONFIG.config.items(section) 99 return rpc_utils.prepare_for_serialization(config_values) 100 101 102 def _write_config_file(config_file, config_values, overwrite=False): 103 """Writes out a configuration file. 104 105 @param config_file: The name of the configuration file. 106 @param config_values: The ConfigParser object. 107 @param ovewrite: Flag on if overwriting is allowed. 108 """ 109 if not config_file: 110 raise error.RPCException('Empty config file name.') 111 if not overwrite and os.path.exists(config_file): 112 raise error.RPCException('Config file already exists.') 113 114 if config_values: 115 with open(config_file, 'w') as config_file: 116 config_values.write(config_file) 117 118 119 def _read_original_config(): 120 """Reads the orginal configuratino without shadow. 121 122 @return: A configuration object, see global_config_class. 123 """ 124 original_config = global_config.global_config_class() 125 original_config.set_config_files(shadow_file='') 126 return original_config 127 128 129 def _read_raw_config(config_file): 130 """Reads the raw configuration from a configuration file. 131 132 @param: config_file: The path of the configuration file. 133 134 @return: A ConfigParser object. 135 """ 136 shadow_config = ConfigParser.RawConfigParser() 137 shadow_config.read(config_file) 138 return shadow_config 139 140 141 def _get_shadow_config_from_partial_update(config_values): 142 """Finds out the new shadow configuration based on a partial update. 143 144 Since the input is only a partial config, we should not lose the config 145 data inside the existing shadow config file. We also need to distinguish 146 if the input config info overrides with a new value or reverts back to 147 an original value. 148 149 @param config_values: See get_moblab_settings(). 150 151 @return: The new shadow configuration as ConfigParser object. 152 """ 153 original_config = _read_original_config() 154 existing_shadow = _read_raw_config(_CONFIG.shadow_file) 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 existing_shadow.has_section(section): 161 existing_shadow.add_section(section) 162 existing_shadow.set(section, key, value) 163 elif existing_shadow.has_option(section, key): 164 existing_shadow.remove_option(section, key) 165 return existing_shadow 166 167 168 def _update_partial_config(config_values): 169 """Updates the shadow configuration file with a partial config udpate. 170 171 @param config_values: See get_moblab_settings(). 172 """ 173 existing_config = _get_shadow_config_from_partial_update(config_values) 174 _write_config_file(_CONFIG.shadow_file, existing_config, True) 175 176 177 @rpc_utils.moblab_only 178 def update_config_handler(config_values): 179 """Update config values and override shadow config. 180 181 @param config_values: See get_moblab_settings(). 182 """ 183 original_config = _read_original_config() 184 new_shadow = ConfigParser.RawConfigParser() 185 for section, config_value_list in config_values.iteritems(): 186 for key, value in config_value_list: 187 if original_config.get_config_value(section, key, 188 default='', 189 allow_blank=True) != value: 190 if not new_shadow.has_section(section): 191 new_shadow.add_section(section) 192 new_shadow.set(section, key, value) 193 194 if not _CONFIG.shadow_file or not os.path.exists(_CONFIG.shadow_file): 195 raise error.RPCException('Shadow config file does not exist.') 196 _write_config_file(_CONFIG.shadow_file, new_shadow, True) 197 198 # TODO (sbasi) crbug.com/403916 - Remove the reboot command and 199 # instead restart the services that rely on the config values. 200 os.system('sudo reboot') 201 202 203 @rpc_utils.moblab_only 204 def reset_config_settings(): 205 """Reset moblab shadow config.""" 206 with open(_CONFIG.shadow_file, 'w') as config_file: 207 pass 208 os.system('sudo reboot') 209 210 211 @rpc_utils.moblab_only 212 def reboot_moblab(): 213 """Simply reboot the device.""" 214 os.system('sudo reboot') 215 216 217 @rpc_utils.moblab_only 218 def set_boto_key(boto_key): 219 """Update the boto_key file. 220 221 @param boto_key: File name of boto_key uploaded through handle_file_upload. 222 """ 223 if not os.path.exists(boto_key): 224 raise error.RPCException('Boto key: %s does not exist!' % boto_key) 225 shutil.copyfile(boto_key, moblab_host.MOBLAB_BOTO_LOCATION) 226 227 228 @rpc_utils.moblab_only 229 def set_service_account_credential(service_account_filename): 230 """Update the service account credential file. 231 232 @param service_account_filename: Name of uploaded file through 233 handle_file_upload. 234 """ 235 if not os.path.exists(service_account_filename): 236 raise error.RPCException( 237 'Service account file: %s does not exist!' % 238 service_account_filename) 239 shutil.copyfile( 240 service_account_filename, 241 moblab_host.MOBLAB_SERVICE_ACCOUNT_LOCATION) 242 243 244 @rpc_utils.moblab_only 245 def set_launch_control_key(launch_control_key): 246 """Update the launch_control_key file. 247 248 @param launch_control_key: File name of launch_control_key uploaded through 249 handle_file_upload. 250 """ 251 if not os.path.exists(launch_control_key): 252 raise error.RPCException('Launch Control key: %s does not exist!' % 253 launch_control_key) 254 shutil.copyfile(launch_control_key, 255 moblab_host.MOBLAB_LAUNCH_CONTROL_KEY_LOCATION) 256 # Restart the devserver service. 257 os.system('sudo restart moblab-devserver-init') 258 259 260 ###########Moblab Config Wizard RPCs ####################### 261 def _get_public_ip_address(socket_handle): 262 """Gets the public IP address. 263 264 Connects to Google DNS server using a socket and gets the preferred IP 265 address from the connection. 266 267 @param: socket_handle: a unix socket. 268 269 @return: public ip address as string. 270 """ 271 try: 272 socket_handle.settimeout(1) 273 socket_handle.connect(('8.8.8.8', 53)) 274 socket_name = socket_handle.getsockname() 275 if socket_name is not None: 276 logging.info('Got socket name from UDP socket.') 277 return socket_name[0] 278 logging.warn('Created UDP socket but with no socket_name.') 279 except socket.error: 280 logging.warn('Could not get socket name from UDP socket.') 281 return None 282 283 284 def _get_network_info(): 285 """Gets the network information. 286 287 TCP socket is used to test the connectivity. If there is no connectivity, 288 try to get the public IP with UDP socket. 289 290 @return: a tuple as (public_ip_address, connected_to_internet). 291 """ 292 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 293 ip = _get_public_ip_address(s) 294 if ip is not None: 295 logging.info('Established TCP connection with well known server.') 296 return (ip, True) 297 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 298 return (_get_public_ip_address(s), False) 299 300 301 @rpc_utils.moblab_only 302 def get_network_info(): 303 """Returns the server ip addresses, and if the server connectivity. 304 305 The server ip addresses as an array of strings, and the connectivity as a 306 flag. 307 """ 308 network_info = {} 309 info = _get_network_info() 310 if info[0] is not None: 311 network_info['server_ips'] = [info[0]] 312 network_info['is_connected'] = info[1] 313 314 return rpc_utils.prepare_for_serialization(network_info) 315 316 317 # Gets the boto configuration. 318 def _get_boto_config(): 319 """Reads the boto configuration from the boto file. 320 321 @return: Boto configuration as ConfigParser object. 322 """ 323 boto_config = ConfigParser.ConfigParser() 324 boto_config.read(MOBLAB_BOTO_LOCATION) 325 return boto_config 326 327 328 @rpc_utils.moblab_only 329 def get_cloud_storage_info(): 330 """RPC handler to get the cloud storage access information. 331 """ 332 cloud_storage_info = {} 333 value =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER) 334 if value is not None: 335 cloud_storage_info[_IMAGE_STORAGE_SERVER] = value 336 value = _CONFIG.get_config_value('CROS', _RESULT_STORAGE_SERVER, 337 default=None) 338 if value is not None: 339 cloud_storage_info[_RESULT_STORAGE_SERVER] = value 340 341 boto_config = _get_boto_config() 342 sections = boto_config.sections() 343 344 if sections: 345 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = True 346 else: 347 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = False 348 if 'Credentials' in sections: 349 options = boto_config.options('Credentials') 350 if _GS_ACCESS_KEY_ID in options: 351 value = boto_config.get('Credentials', _GS_ACCESS_KEY_ID) 352 cloud_storage_info[_GS_ACCESS_KEY_ID] = value 353 if _GS_SECRET_ACCESS_KEY in options: 354 value = boto_config.get('Credentials', _GS_SECRET_ACCESS_KEY) 355 cloud_storage_info[_GS_SECRET_ACCESS_KEY] = value 356 357 return rpc_utils.prepare_for_serialization(cloud_storage_info) 358 359 360 def _get_bucket_name_from_url(bucket_url): 361 """Gets the bucket name from a bucket url. 362 363 @param: bucket_url: the bucket url string. 364 """ 365 if bucket_url: 366 match = GOOGLE_STORAGE_BUCKET_URL_PATTERN.match(bucket_url) 367 if match: 368 return match.group('bucket') 369 return None 370 371 372 def _is_valid_boto_key(key_id, key_secret, directory): 373 try: 374 _run_bucket_performance_test(key_id, key_secret, directory) 375 except BucketPerformanceTestException as e: 376 return(False, str(e)) 377 return(True, None) 378 379 380 def _validate_cloud_storage_info(cloud_storage_info): 381 """Checks if the cloud storage information is valid. 382 383 @param: cloud_storage_info: The JSON RPC object for cloud storage info. 384 385 @return: A tuple as (valid_boolean, details_string). 386 """ 387 valid = True 388 details = None 389 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]: 390 key_id = cloud_storage_info[_GS_ACCESS_KEY_ID] 391 key_secret = cloud_storage_info[_GS_SECRET_ACCESS_KEY] 392 valid, details = _is_valid_boto_key( 393 key_id, key_secret, cloud_storage_info[_IMAGE_STORAGE_SERVER]) 394 return (valid, details) 395 396 397 def _create_operation_status_response(is_ok, details): 398 """Helper method to create a operation status reponse. 399 400 @param: is_ok: Boolean for if the operation is ok. 401 @param: details: A detailed string. 402 403 @return: A serialized JSON RPC object. 404 """ 405 status_response = {'status_ok': is_ok} 406 if details: 407 status_response['status_details'] = details 408 return rpc_utils.prepare_for_serialization(status_response) 409 410 411 @rpc_utils.moblab_only 412 def validate_cloud_storage_info(cloud_storage_info): 413 """RPC handler to check if the cloud storage info is valid. 414 415 @param cloud_storage_info: The JSON RPC object for cloud storage info. 416 """ 417 valid, details = _validate_cloud_storage_info(cloud_storage_info) 418 return _create_operation_status_response(valid, details) 419 420 421 @rpc_utils.moblab_only 422 def submit_wizard_config_info(cloud_storage_info, wifi_info): 423 """RPC handler to submit the cloud storage info. 424 425 @param cloud_storage_info: The JSON RPC object for cloud storage info. 426 @param wifi_info: The JSON RPC object for DUT wifi info. 427 """ 428 config_update = {} 429 config_update['CROS'] = [ 430 (_IMAGE_STORAGE_SERVER, cloud_storage_info[_IMAGE_STORAGE_SERVER]), 431 (_RESULT_STORAGE_SERVER, cloud_storage_info[_RESULT_STORAGE_SERVER]) 432 ] 433 config_update['MOBLAB'] = [ 434 (_WIFI_AP_NAME, wifi_info.get(_WIFI_AP_NAME) or ''), 435 (_WIFI_AP_PASS, wifi_info.get(_WIFI_AP_PASS) or '') 436 ] 437 _update_partial_config(config_update) 438 439 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]: 440 boto_config = ConfigParser.RawConfigParser() 441 boto_config.add_section('Credentials') 442 boto_config.set('Credentials', _GS_ACCESS_KEY_ID, 443 cloud_storage_info[_GS_ACCESS_KEY_ID]) 444 boto_config.set('Credentials', _GS_SECRET_ACCESS_KEY, 445 cloud_storage_info[_GS_SECRET_ACCESS_KEY]) 446 _write_config_file(MOBLAB_BOTO_LOCATION, boto_config, True) 447 448 _CONFIG.parse_config_file() 449 _enable_notification_using_credentials_in_bucket() 450 services = ['moblab-devserver-init', 451 'moblab-devserver-cleanup-init', 'moblab-gsoffloader_s-init', 452 'moblab-scheduler-init', 'moblab-gsoffloader-init'] 453 cmd = 'export ATEST_RESULTS_DIR=/usr/local/autotest/results;' 454 cmd += 'sudo stop ' + ';sudo stop '.join(services) 455 cmd += ';sudo start ' + ';sudo start '.join(services) 456 cmd += ';sudo apache2 -k graceful' 457 logging.info(cmd) 458 try: 459 utils.run(cmd) 460 except error.CmdError as e: 461 logging.error(e) 462 # if all else fails reboot the device. 463 utils.run('sudo reboot') 464 465 return _create_operation_status_response(True, None) 466 467 468 @rpc_utils.moblab_only 469 def get_version_info(): 470 """ RPC handler to get informaiton about the version of the moblab. 471 472 @return: A serialized JSON RPC object. 473 """ 474 lines = open(_ETC_LSB_RELEASE).readlines() 475 version_response = { 476 x.split('=')[0]: x.split('=')[1] for x in lines if '=' in x} 477 version_response['MOBLAB_ID'] = utils.get_moblab_id(); 478 version_response['MOBLAB_SERIAL_NUMBER'] = ( 479 utils.get_moblab_serial_number()) 480 _check_for_system_update() 481 update_status = _get_system_update_status() 482 version_response['MOBLAB_UPDATE_VERSION'] = update_status['NEW_VERSION'] 483 version_response['MOBLAB_UPDATE_STATUS'] = update_status['CURRENT_OP'] 484 version_response['MOBLAB_UPDATE_PROGRESS'] = update_status['PROGRESS'] 485 return rpc_utils.prepare_for_serialization(version_response) 486 487 488 @rpc_utils.moblab_only 489 def update_moblab(): 490 """ RPC call to update and reboot moblab """ 491 _install_system_update() 492 493 494 def _check_for_system_update(): 495 """ Run the ChromeOS update client to check update server for an 496 update. If an update exists, the update client begins downloading it 497 in the background 498 """ 499 # sudo is required to run the update client 500 subprocess.call(['sudo', _UPDATE_ENGINE_CLIENT, '--check_for_update']) 501 # wait for update engine to finish checking 502 tries = 0 503 while ('CHECKING_FOR_UPDATE' in _get_system_update_status()['CURRENT_OP'] 504 and tries < 10): 505 time.sleep(.1) 506 tries = tries + 1 507 508 def _get_system_update_status(): 509 """ Run the ChromeOS update client to check status on a 510 pending/downloading update 511 512 @return: A dictionary containing { 513 PROGRESS: str containing percent progress of an update download 514 CURRENT_OP: str current status of the update engine, 515 ex UPDATE_STATUS_UPDATED_NEED_REBOOT 516 NEW_SIZE: str size of the update 517 NEW_VERSION: str version number for the update 518 LAST_CHECKED_TIME: str unix time stamp of the last update check 519 } 520 """ 521 # sudo is required to run the update client 522 cmd_out = subprocess.check_output( 523 ['sudo' ,_UPDATE_ENGINE_CLIENT, '--status']) 524 split_lines = [x.split('=') for x in cmd_out.strip().split('\n')] 525 status = dict((key, val) for [key, val] in split_lines) 526 return status 527 528 529 def _install_system_update(): 530 """ Installs a ChromeOS update, will cause the system to reboot 531 """ 532 # sudo is required to run the update client 533 # first run a blocking command to check, fetch, prepare an update 534 # then check if a reboot is needed 535 try: 536 subprocess.check_call(['sudo', _UPDATE_ENGINE_CLIENT, '--update']) 537 # --is_reboot_needed returns 0 if a reboot is required 538 subprocess.check_call( 539 ['sudo', _UPDATE_ENGINE_CLIENT, '--is_reboot_needed']) 540 subprocess.call(['sudo', _UPDATE_ENGINE_CLIENT, '--reboot']) 541 542 except subprocess.CalledProcessError as e: 543 update_error = subprocess.check_output( 544 ['sudo', _UPDATE_ENGINE_CLIENT, '--last_attempt_error']) 545 raise error.RPCException(update_error) 546 547 548 @rpc_utils.moblab_only 549 def get_connected_dut_info(): 550 """ RPC handler to get informaiton about the DUTs connected to the moblab. 551 552 @return: A serialized JSON RPC object. 553 """ 554 # Make a list of the connected DUT's 555 leases = _get_dhcp_dut_leases() 556 557 558 connected_duts = _test_all_dut_connections(leases) 559 560 # Get a list of the AFE configured DUT's 561 hosts = list(rpc_utils.get_host_query((), False, True, {})) 562 models.Host.objects.populate_relationships(hosts, models.Label, 563 'label_list') 564 configured_duts = {} 565 for host in hosts: 566 labels = [label.name for label in host.label_list] 567 labels.sort() 568 for host_attribute in host.hostattribute_set.all(): 569 labels.append("ATTR:(%s=%s)" % (host_attribute.attribute, 570 host_attribute.value)) 571 configured_duts[host.hostname] = ', '.join(labels) 572 573 return rpc_utils.prepare_for_serialization( 574 {'configured_duts': configured_duts, 575 'connected_duts': connected_duts}) 576 577 578 def _get_dhcp_dut_leases(): 579 """ Extract information about connected duts from the dhcp server. 580 581 @return: A dict of ipaddress to mac address for each device connected. 582 """ 583 lease_info = open(_DHCPD_LEASES).read() 584 585 leases = {} 586 for lease in lease_info.split('lease'): 587 if lease.find('binding state active;') != -1: 588 ipaddress = lease.split('\n')[0].strip(' {') 589 last_octet = int(ipaddress.split('.')[-1].strip()) 590 if last_octet > 150: 591 continue 592 mac_address_search = re.search('hardware ethernet (.*);', lease) 593 if mac_address_search: 594 leases[ipaddress] = mac_address_search.group(1) 595 return leases 596 597 def _test_all_dut_connections(leases): 598 """ Test ssh connection of all connected DUTs in parallel 599 600 @param leases: dict containing key value pairs of ip and mac address 601 602 @return: dict containing { 603 ip: {mac_address:[string], ssh_connection_ok:[boolean]} 604 } 605 """ 606 # target function for parallel process 607 def _test_dut(ip, result): 608 result.value = _test_dut_ssh_connection(ip) 609 610 processes = [] 611 for ip in leases: 612 # use a shared variable to get the ssh test result from child process 613 ssh_test_result = multiprocessing.Value(ctypes.c_bool) 614 # create a subprocess to test each DUT 615 process = multiprocessing.Process( 616 target=_test_dut, args=(ip, ssh_test_result)) 617 process.start() 618 619 processes.append({ 620 'ip': ip, 621 'ssh_test_result': ssh_test_result, 622 'process': process 623 }) 624 625 connected_duts = {} 626 for process in processes: 627 process['process'].join() 628 ip = process['ip'] 629 connected_duts[ip] = { 630 'mac_address': leases[ip], 631 'ssh_connection_ok': process['ssh_test_result'].value 632 } 633 634 return connected_duts 635 636 637 def _test_dut_ssh_connection(ip): 638 """ Test if a connected dut is accessible via ssh. 639 The primary use case is to verify that the dut has a test image. 640 641 @return: True if the ssh connection is good False else 642 """ 643 cmd = ('ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no ' 644 "root@%s 'timeout 2 cat /etc/lsb-release'") % ip 645 try: 646 release = subprocess.check_output(cmd, shell=True) 647 return 'CHROMEOS_RELEASE_APPID' in release 648 except: 649 return False 650 651 652 @rpc_utils.moblab_only 653 def add_moblab_dut(ipaddress): 654 """ RPC handler to add a connected DUT to autotest. 655 656 @param ipaddress: IP address of the DUT. 657 658 @return: A string giving information about the status. 659 """ 660 cmd = '/usr/local/autotest/cli/atest host create %s &' % ipaddress 661 subprocess.call(cmd, shell=True) 662 return (True, 'DUT %s added to Autotest' % ipaddress) 663 664 665 @rpc_utils.moblab_only 666 def remove_moblab_dut(ipaddress): 667 """ RPC handler to remove DUT entry from autotest. 668 669 @param ipaddress: IP address of the DUT. 670 671 @return: True if the command succeeds without an exception 672 """ 673 models.Host.smart_get(ipaddress).delete() 674 return (True, 'DUT %s deleted from Autotest' % ipaddress) 675 676 677 @rpc_utils.moblab_only 678 def add_moblab_label(ipaddress, label_name): 679 """ RPC handler to add a label in autotest to a DUT entry. 680 681 @param ipaddress: IP address of the DUT. 682 @param label_name: The label name. 683 684 @return: A string giving information about the status. 685 """ 686 # Try to create the label in case it does not already exist. 687 label = None 688 try: 689 label = models.Label.add_object(name=label_name) 690 except: 691 label = models.Label.smart_get(label_name) 692 if label.is_replaced_by_static(): 693 raise error.UnmodifiableLabelException( 694 'Failed to add label "%s" because it is a static label. ' 695 'Use go/chromeos-skylab-inventory-tools to add this ' 696 'label.' % label.name) 697 698 host_obj = models.Host.smart_get(ipaddress) 699 if label: 700 label.host_set.add(host_obj) 701 return (True, 'Added label %s to DUT %s' % (label_name, ipaddress)) 702 return (False, 703 'Failed to add label %s to DUT %s' % (label_name, ipaddress)) 704 705 706 @rpc_utils.moblab_only 707 def remove_moblab_label(ipaddress, label_name): 708 """ RPC handler to remove a label in autotest from a DUT entry. 709 710 @param ipaddress: IP address of the DUT. 711 @param label_name: The label name. 712 713 @return: A string giving information about the status. 714 """ 715 host_obj = models.Host.smart_get(ipaddress) 716 label = models.Label.smart_get(label_name) 717 if label.is_replaced_by_static(): 718 raise error.UnmodifiableLabelException( 719 'Failed to remove label "%s" because it is a static label. ' 720 'Use go/chromeos-skylab-inventory-tools to remove this ' 721 'label.' % label.name) 722 723 label.host_set.remove(host_obj) 724 return (True, 'Removed label %s from DUT %s' % (label_name, ipaddress)) 725 726 727 @rpc_utils.moblab_only 728 def set_host_attrib(ipaddress, attribute, value): 729 """ RPC handler to set an attribute of a host. 730 731 @param ipaddress: IP address of the DUT. 732 @param attribute: string name of attribute 733 @param value: string, or None to delete an attribute 734 735 @return: True if the command succeeds without an exception 736 """ 737 host_obj = models.Host.smart_get(ipaddress) 738 host_obj.set_or_delete_attribute(attribute, value) 739 return (True, 'Updated attribute %s to %s on DUT %s' % ( 740 attribute, value, ipaddress)) 741 742 743 @rpc_utils.moblab_only 744 def delete_host_attrib(ipaddress, attribute): 745 """ RPC handler to delete an attribute of a host. 746 747 @param ipaddress: IP address of the DUT. 748 @param attribute: string name of attribute 749 750 @return: True if the command succeeds without an exception 751 """ 752 host_obj = models.Host.smart_get(ipaddress) 753 host_obj.set_or_delete_attribute(attribute, None) 754 return (True, 'Deleted attribute %s from DUT %s' % ( 755 attribute, ipaddress)) 756 757 758 def _get_connected_dut_labels(requested_label, only_first_label=True): 759 """ Query the DUT's attached to the moblab and return a filtered list 760 of labels. 761 762 @param requested_label: the label name you are requesting. 763 @param only_first_label: if the device has the same label name multiple 764 times only return the first label value in the 765 list. 766 767 @return: A de-duped list of requested dut labels attached to the moblab. 768 """ 769 hosts = list(rpc_utils.get_host_query((), False, True, {})) 770 if not hosts: 771 return [] 772 models.Host.objects.populate_relationships(hosts, models.Label, 773 'label_list') 774 labels = set() 775 for host in hosts: 776 for label in host.label_list: 777 if requested_label in label.name: 778 labels.add(label.name.replace(requested_label, '')) 779 if only_first_label: 780 break 781 return list(labels) 782 783 def _get_connected_dut_board_models(): 784 """ Get the boards and their models of attached DUTs 785 786 @return: A de-duped list of dut board/model attached to the moblab 787 format: [ 788 { 789 "board": "carl", 790 "model": "bruce" 791 }, 792 { 793 "board": "veyron_minnie", 794 "model": "veyron_minnie" 795 } 796 ] 797 """ 798 hosts = list(rpc_utils.get_host_query((), False, True, {})) 799 if not hosts: 800 return [] 801 models.Host.objects.populate_relationships(hosts, models.Label, 802 'label_list') 803 model_board_map = dict() 804 for host in hosts: 805 model = '' 806 board = '' 807 for label in host.label_list: 808 if 'model:' in label.name: 809 model = label.name.replace('model:', '') 810 elif 'board:' in label.name: 811 board = label.name.replace('board:', '') 812 model_board_map[model] = board 813 814 board_models_list = [] 815 for model in sorted(model_board_map.keys()): 816 board_models_list.append({ 817 'model': model, 818 'board': model_board_map[model] 819 }) 820 return board_models_list 821 822 823 @rpc_utils.moblab_only 824 def get_connected_boards(): 825 """ RPC handler to get a list of the boards connected to the moblab. 826 827 @return: A de-duped list of board types attached to the moblab. 828 """ 829 return _get_connected_dut_board_models() 830 831 832 @rpc_utils.moblab_only 833 def get_connected_pools(): 834 """ RPC handler to get a list of the pools labels on the DUT's connected. 835 836 @return: A de-duped list of pool labels. 837 """ 838 pools = _get_connected_dut_labels("pool:", False) 839 pools.sort() 840 return pools 841 842 843 @rpc_utils.moblab_only 844 def get_builds_for_board(board_name): 845 """ RPC handler to find the most recent builds for a board. 846 847 848 @param board_name: The name of a connected board. 849 @return: A list of string with the most recent builds for the latest 850 three milestones. 851 """ 852 return _get_builds_for_in_directory(board_name + '-release', 853 milestone_limit=4) 854 855 856 @rpc_utils.moblab_only 857 def get_firmware_for_board(board_name): 858 """ RPC handler to find the most recent firmware for a board. 859 860 861 @param board_name: The name of a connected board. 862 @return: A list of strings with the most recent firmware builds for the 863 latest three milestones. 864 """ 865 return _get_builds_for_in_directory(board_name + '-firmware') 866 867 868 def _get_sortable_build_number(sort_key): 869 """ Converts a build number line cyan-release/R59-9460.27.0 into an integer. 870 871 To be able to sort a list of builds you need to convert the build number 872 into an integer so it can be compared correctly to other build. 873 874 cyan-release/R59-9460.27.0 => 5909460027000 875 876 If the sort key is not recognised as a build number 1 will be returned. 877 878 @param sort_key: A string that represents a build number like 879 cyan-release/R59-9460.27.0 880 @return: An integer that represents that build number or 1 if not recognised 881 as a build. 882 """ 883 build_number = re.search('.*/R([0-9]*)-([0-9]*)\.([0-9]*)\.([0-9]*)', 884 sort_key) 885 if not build_number or not len(build_number.groups()) == 4: 886 return 1 887 return int("%d%05d%03d%03d" % (int(build_number.group(1)), 888 int(build_number.group(2)), 889 int(build_number.group(3)), 890 int(build_number.group(4)))) 891 892 def _get_builds_for_in_directory(directory_name, milestone_limit=3, 893 build_limit=20): 894 """ Fetch the most recent builds for the last three milestones from gcs. 895 896 897 @param directory_name: The sub-directory under the configured GCS image 898 storage bucket to search. 899 900 901 @return: A string list no longer than <milestone_limit> x <build_limit> 902 items, containing the most recent <build_limit> builds from the 903 last milestone_limit milestones. 904 """ 905 output = StringIO.StringIO() 906 gs_image_location =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER) 907 try: 908 utils.run(GsUtil.get_gsutil_cmd(), 909 args=('ls', gs_image_location + directory_name), 910 stdout_tee=output) 911 except error.CmdError as e: 912 error_text = ('Failed to list builds from %s.\n' 913 'Did you configure your boto key? Try running the config ' 914 'wizard again.\n\n%s') % ((gs_image_location + directory_name), 915 e.result_obj.stderr) 916 raise error.RPCException(error_text) 917 lines = output.getvalue().split('\n') 918 output.close() 919 builds = [line.replace(gs_image_location,'').strip('/ ') 920 for line in lines if line != ''] 921 build_matcher = re.compile(r'^.*\/R([0-9]*)-.*') 922 build_map = {} 923 for build in builds: 924 match = build_matcher.match(build) 925 if match: 926 milestone = match.group(1) 927 if milestone not in build_map: 928 build_map[milestone] = [] 929 build_map[milestone].append(build) 930 milestones = build_map.keys() 931 milestones.sort() 932 milestones.reverse() 933 build_list = [] 934 for milestone in milestones[:milestone_limit]: 935 builds = build_map[milestone] 936 builds.sort(key=_get_sortable_build_number) 937 builds.reverse() 938 build_list.extend(builds[:build_limit]) 939 return build_list 940 941 942 def _run_bucket_performance_test(key_id, key_secret, bucket_name, 943 test_size='1M', iterations='1', 944 result_file='/tmp/gsutil_perf.json'): 945 """Run a gsutil perfdiag on a supplied bucket and output the results" 946 947 @param key_id: boto key of the bucket to be accessed 948 @param key_secret: boto secret of the bucket to be accessed 949 @param bucket_name: bucket to be tested. 950 @param test_size: size of file to use in test, see gsutil perfdiag help. 951 @param iterations: number of times each test is run. 952 @param result_file: name of file to write results out to. 953 954 @return None 955 @raises BucketPerformanceTestException if the command fails. 956 """ 957 try: 958 utils.run(GsUtil.get_gsutil_cmd(), args=( 959 '-o', 'Credentials:gs_access_key_id=%s' % key_id, 960 '-o', 'Credentials:gs_secret_access_key=%s' % key_secret, 961 'perfdiag', '-s', test_size, '-o', result_file, 962 '-n', iterations, 963 bucket_name)) 964 except error.CmdError as e: 965 logging.error(e) 966 # Extract useful error from the stacktrace 967 errormsg = str(e) 968 start_error_pos = errormsg.find("<Error>") 969 end_error_pos = errormsg.find("</Error>", start_error_pos) 970 extracted_error_msg = errormsg[start_error_pos:end_error_pos] 971 raise BucketPerformanceTestException( 972 extracted_error_msg if extracted_error_msg else errormsg) 973 # TODO(haddowk) send the results to the cloud console when that feature is 974 # enabled. 975 976 977 # TODO(haddowk) Change suite_args name to "test_filter_list" or similar. May 978 # also need to make changes at MoblabRpcHelper.java 979 @rpc_utils.moblab_only 980 def run_suite(board, build, suite, model=None, ro_firmware=None, 981 rw_firmware=None, pool=None, suite_args=None, test_args=None, 982 bug_id=None, part_id=None): 983 """ RPC handler to run a test suite. 984 985 @param board: a board name connected to the moblab. 986 @param build: a build name of a build in the GCS. 987 @param suite: the name of a suite to run 988 @param model: a board model name connected to the moblab. 989 @param ro_firmware: Optional ro firmware build number to use. 990 @param rw_firmware: Optional rw firmware build number to use. 991 @param pool: Optional pool name to run the suite in. 992 @param suite_args: Arguments to be used in the suite control file. 993 @param test_args: '\n' delimited key=val pairs passed to test control file. 994 @param bug_id: Optional bug ID used for AVL qualification process. 995 @param part_id: Optional part ID used for AVL qualification 996 process. 997 998 @return: None 999 """ 1000 builds = {'cros-version': build} 1001 # TODO(mattmallett b/92031054) Standardize bug id, part id passing for memory/storage qual 1002 processed_suite_args = dict() 1003 processed_test_args = dict() 1004 if rw_firmware: 1005 builds['fwrw-version'] = rw_firmware 1006 if ro_firmware: 1007 builds['fwro-version'] = ro_firmware 1008 if suite_args: 1009 processed_suite_args['tests'] = \ 1010 [s.strip() for s in suite_args.split(',')] 1011 if bug_id: 1012 processed_suite_args['bug_id'] = bug_id 1013 if part_id: 1014 processed_suite_args['part_id'] = part_id 1015 processed_test_args['bug_id'] = bug_id or '' 1016 processed_test_args['part_id'] = part_id or '' 1017 1018 1019 # set processed_suite_args to None instead of empty dict when there is no 1020 # argument in processed_suite_args 1021 if len(processed_suite_args) == 0: 1022 processed_suite_args = None 1023 1024 if test_args: 1025 try: 1026 processed_test_args['args'] = [test_args] 1027 for line in test_args.split('\n'): 1028 key, value = line.strip().split('=') 1029 processed_test_args[key] = value 1030 except: 1031 raise error.RPCException('Could not parse test args.') 1032 1033 1034 ap_name =_CONFIG.get_config_value('MOBLAB', _WIFI_AP_NAME, default=None) 1035 processed_test_args['ssid'] = ap_name 1036 ap_pass =_CONFIG.get_config_value('MOBLAB', _WIFI_AP_PASS, default='') 1037 processed_test_args['wifipass'] = ap_pass 1038 1039 suite_timeout_mins = _SUITE_TIMEOUT_MAP.get( 1040 suite, _DEFAULT_SUITE_TIMEOUT_MINS) 1041 1042 afe = frontend.AFE(user='moblab') 1043 afe.run('create_suite_job', board=board, builds=builds, name=suite, 1044 pool=pool, run_prod_code=False, test_source_build=build, 1045 wait_for_results=True, suite_args=processed_suite_args, 1046 test_args=processed_test_args, job_retry=True, 1047 max_retries=sys.maxint, model=model, 1048 timeout_mins=suite_timeout_mins, 1049 max_runtime_mins=suite_timeout_mins) 1050 1051 1052 def _enable_notification_using_credentials_in_bucket(): 1053 """ Check and enable cloud notification if a credentials file exits. 1054 @return: None 1055 """ 1056 gs_image_location =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER) 1057 try: 1058 utils.run(GsUtil.get_gsutil_cmd(), args=( 1059 'cp', gs_image_location + 'pubsub-key-do-not-delete.json', '/tmp')) 1060 # This runs the copy as moblab user 1061 shutil.copyfile('/tmp/pubsub-key-do-not-delete.json', 1062 moblab_host.MOBLAB_SERVICE_ACCOUNT_LOCATION) 1063 1064 except error.CmdError as e: 1065 logging.error(e) 1066 else: 1067 logging.info('Enabling cloud notifications') 1068 config_update = {} 1069 config_update['CROS'] = [(_CLOUD_NOTIFICATION_ENABLED, True)] 1070 _update_partial_config(config_update) 1071 1072 1073 @rpc_utils.moblab_only 1074 def get_dut_wifi_info(): 1075 """RPC handler to get the dut wifi AP information. 1076 """ 1077 dut_wifi_info = {} 1078 value =_CONFIG.get_config_value('MOBLAB', _WIFI_AP_NAME, 1079 default=None) 1080 if value is not None: 1081 dut_wifi_info[_WIFI_AP_NAME] = value 1082 value = _CONFIG.get_config_value('MOBLAB', _WIFI_AP_PASS, 1083 default=None) 1084 if value is not None: 1085 dut_wifi_info[_WIFI_AP_PASS] = value 1086 return rpc_utils.prepare_for_serialization(dut_wifi_info) 1087