Home | History | Annotate | Download | only in afe
      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