Home | History | Annotate | Download | only in hosts
      1 # Copyright 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 """This class defines the CrosHost Label class."""
      6 
      7 import logging
      8 import os
      9 import re
     10 
     11 import common
     12 
     13 from autotest_lib.client.bin import utils
     14 from autotest_lib.client.common_lib import global_config
     15 from autotest_lib.client.cros.audio import cras_utils
     16 from autotest_lib.client.cros.video import constants as video_test_constants
     17 from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
     18 from autotest_lib.server.hosts import base_label
     19 from autotest_lib.server.hosts import common_label
     20 from autotest_lib.server.hosts import servo_host
     21 from autotest_lib.site_utils import hwid_lib
     22 
     23 # pylint: disable=missing-docstring
     24 
     25 class BoardLabel(base_label.StringPrefixLabel):
     26     """Determine the correct board label for the device."""
     27 
     28     _NAME = ds_constants.BOARD_PREFIX.rstrip(':')
     29 
     30     def generate_labels(self, host):
     31         # We only want to apply the board labels once, which is when they get
     32         # added to the AFE.  That way we don't have to worry about the board
     33         # label switching on us if the wrong builds get put on the devices.
     34         # crbug.com/624207 records one event of the board label switching
     35         # unexpectedly on us.
     36         for label in host._afe_host.labels:
     37             if label.startswith(self._NAME + ':'):
     38                 return [label.split(':')[-1]]
     39 
     40         # TODO(kevcheng): for now this will dup the code in CrosHost and a
     41         # separate cl will refactor the get_board in CrosHost to just return the
     42         # board without the BOARD_PREFIX and all the other callers will be
     43         # updated to not need to clear it out and this code will be replaced to
     44         # just call the host's get_board() method.
     45         release_info = utils.parse_cmd_output('cat /etc/lsb-release',
     46                                               run_method=host.run)
     47         return [release_info['CHROMEOS_RELEASE_BOARD']]
     48 
     49 
     50 class LightSensorLabel(base_label.BaseLabel):
     51     """Label indicating if a light sensor is detected."""
     52 
     53     _NAME = 'lightsensor'
     54     _LIGHTSENSOR_SEARCH_DIR = '/sys/bus/iio/devices'
     55     _LIGHTSENSOR_FILES = [
     56         "in_illuminance0_input",
     57         "in_illuminance_input",
     58         "in_illuminance0_raw",
     59         "in_illuminance_raw",
     60         "illuminance0_input",
     61     ]
     62 
     63     def exists(self, host):
     64         search_cmd = "find -L %s -maxdepth 4 | egrep '%s'" % (
     65             self._LIGHTSENSOR_SEARCH_DIR, '|'.join(self._LIGHTSENSOR_FILES))
     66         # Run the search cmd following the symlinks. Stderr_tee is set to
     67         # None as there can be a symlink loop, but the command will still
     68         # execute correctly with a few messages printed to stderr.
     69         result = host.run(search_cmd, stdout_tee=None, stderr_tee=None,
     70                           ignore_status=True)
     71 
     72         return result.exit_status == 0
     73 
     74 
     75 class BluetoothLabel(base_label.BaseLabel):
     76     """Label indicating if bluetooth is detected."""
     77 
     78     _NAME = 'bluetooth'
     79 
     80     def exists(self, host):
     81         result = host.run('test -d /sys/class/bluetooth/hci0',
     82                           ignore_status=True)
     83 
     84         return result.exit_status == 0
     85 
     86 
     87 class ECLabel(base_label.BaseLabel):
     88     """Label to determine the type of EC on this host."""
     89 
     90     _NAME = 'ec:cros'
     91 
     92     def exists(self, host):
     93         cmd = 'mosys ec info'
     94         # The output should look like these, so that the last field should
     95         # match our EC version scheme:
     96         #
     97         #   stm | stm32f100 | snow_v1.3.139-375eb9f
     98         #   ti | Unknown-10de | peppy_v1.5.114-5d52788
     99         #
    100         # Non-Chrome OS ECs will look like these:
    101         #
    102         #   ENE | KB932 | 00BE107A00
    103         #   ite | it8518 | 3.08
    104         #
    105         # And some systems don't have ECs at all (Lumpy, for example).
    106         regexp = r'^.*\|\s*(\S+_v\d+\.\d+\.\d+-[0-9a-f]+)\s*$'
    107 
    108         ecinfo = host.run(command=cmd, ignore_status=True)
    109         if ecinfo.exit_status == 0:
    110             res = re.search(regexp, ecinfo.stdout)
    111             if res:
    112                 logging.info("EC version is %s", res.groups()[0])
    113                 return True
    114             logging.info("%s got: %s", cmd, ecinfo.stdout)
    115             # Has an EC, but it's not a Chrome OS EC
    116         logging.info("%s exited with status %d", cmd, ecinfo.exit_status)
    117         return False
    118 
    119 
    120 class AccelsLabel(base_label.BaseLabel):
    121     """Determine the type of accelerometers on this host."""
    122 
    123     _NAME = 'accel:cros-ec'
    124 
    125     def exists(self, host):
    126         # Check to make sure we have ectool
    127         rv = host.run('which ectool', ignore_status=True)
    128         if rv.exit_status:
    129             logging.info("No ectool cmd found; assuming no EC accelerometers")
    130             return False
    131 
    132         # Check that the EC supports the motionsense command
    133         rv = host.run('ectool motionsense', ignore_status=True)
    134         if rv.exit_status:
    135             logging.info("EC does not support motionsense command; "
    136                          "assuming no EC accelerometers")
    137             return False
    138 
    139         # Check that EC motion sensors are active
    140         active = host.run('ectool motionsense active').stdout.split('\n')
    141         if active[0] == "0":
    142             logging.info("Motion sense inactive; assuming no EC accelerometers")
    143             return False
    144 
    145         logging.info("EC accelerometers found")
    146         return True
    147 
    148 
    149 class ChameleonLabel(base_label.BaseLabel):
    150     """Determine if a Chameleon is connected to this host."""
    151 
    152     _NAME = 'chameleon'
    153 
    154     def exists(self, host):
    155         return host._chameleon_host is not None
    156 
    157 
    158 class ChameleonConnectionLabel(base_label.StringPrefixLabel):
    159     """Return the Chameleon connection label."""
    160 
    161     _NAME = 'chameleon'
    162 
    163     def exists(self, host):
    164         return host._chameleon_host is not None
    165 
    166 
    167     def generate_labels(self, host):
    168         return [host.chameleon.get_label()]
    169 
    170 
    171 class ChameleonPeripheralsLabel(base_label.StringPrefixLabel):
    172     """Return the Chameleon peripherals labels.
    173 
    174     The 'chameleon:bt_hid' label is applied if the bluetooth
    175     classic hid device, i.e, RN-42 emulation kit, is detected.
    176 
    177     Any peripherals plugged into the chameleon board would be
    178     detected and applied proper labels in this class.
    179     """
    180 
    181     _NAME = 'chameleon'
    182 
    183     def exists(self, host):
    184         return host._chameleon_host is not None
    185 
    186 
    187     def generate_labels(self, host):
    188         bt_hid_device = host.chameleon.get_bluetooh_hid_mouse()
    189         return ['bt_hid'] if bt_hid_device.CheckSerialConnection() else []
    190 
    191 
    192 class AudioLoopbackDongleLabel(base_label.BaseLabel):
    193     """Return the label if an audio loopback dongle is plugged in."""
    194 
    195     _NAME = 'audio_loopback_dongle'
    196 
    197     def exists(self, host):
    198         nodes_info = host.run(command=cras_utils.get_cras_nodes_cmd(),
    199                               ignore_status=True).stdout
    200         if (cras_utils.node_type_is_plugged('HEADPHONE', nodes_info) and
    201             cras_utils.node_type_is_plugged('MIC', nodes_info)):
    202                 return True
    203         return False
    204 
    205 
    206 class PowerSupplyLabel(base_label.StringPrefixLabel):
    207     """
    208     Return the label describing the power supply type.
    209 
    210     Labels representing this host's power supply.
    211          * `power:battery` when the device has a battery intended for
    212                 extended use
    213          * `power:AC_primary` when the device has a battery not intended
    214                 for extended use (for moving the machine, etc)
    215          * `power:AC_only` when the device has no battery at all.
    216     """
    217 
    218     _NAME = 'power'
    219 
    220     def __init__(self):
    221         self.psu_cmd_result = None
    222 
    223 
    224     def exists(self, host):
    225         self.psu_cmd_result = host.run(command='mosys psu type',
    226                                        ignore_status=True)
    227         return self.psu_cmd_result.stdout.strip() != 'unknown'
    228 
    229 
    230     def generate_labels(self, host):
    231         if self.psu_cmd_result.exit_status:
    232             # The psu command for mosys is not included for all platforms. The
    233             # assumption is that the device will have a battery if the command
    234             # is not found.
    235             return ['battery']
    236         return [self.psu_cmd_result.stdout.strip()]
    237 
    238 
    239 class StorageLabel(base_label.StringPrefixLabel):
    240     """
    241     Return the label describing the storage type.
    242 
    243     Determine if the internal device is SCSI or dw_mmc device.
    244     Then check that it is SSD or HDD or eMMC or something else.
    245 
    246     Labels representing this host's internal device type:
    247              * `storage:ssd` when internal device is solid state drive
    248              * `storage:hdd` when internal device is hard disk drive
    249              * `storage:mmc` when internal device is mmc drive
    250              * None          When internal device is something else or
    251                              when we are unable to determine the type
    252     """
    253 
    254     _NAME = 'storage'
    255 
    256     def __init__(self):
    257         self.type_str = ''
    258 
    259 
    260     def exists(self, host):
    261         # The output should be /dev/mmcblk* for SD/eMMC or /dev/sd* for scsi
    262         rootdev_cmd = ' '.join(['. /usr/sbin/write_gpt.sh;',
    263                                 '. /usr/share/misc/chromeos-common.sh;',
    264                                 'load_base_vars;',
    265                                 'get_fixed_dst_drive'])
    266         rootdev = host.run(command=rootdev_cmd, ignore_status=True)
    267         if rootdev.exit_status:
    268             logging.info("Fail to run %s", rootdev_cmd)
    269             return False
    270         rootdev_str = rootdev.stdout.strip()
    271 
    272         if not rootdev_str:
    273             return False
    274 
    275         rootdev_base = os.path.basename(rootdev_str)
    276 
    277         mmc_pattern = '/dev/mmcblk[0-9]'
    278         if re.match(mmc_pattern, rootdev_str):
    279             # Use type to determine if the internal device is eMMC or somthing
    280             # else. We can assume that MMC is always an internal device.
    281             type_cmd = 'cat /sys/block/%s/device/type' % rootdev_base
    282             type = host.run(command=type_cmd, ignore_status=True)
    283             if type.exit_status:
    284                 logging.info("Fail to run %s", type_cmd)
    285                 return False
    286             type_str = type.stdout.strip()
    287 
    288             if type_str == 'MMC':
    289                 self.type_str = 'mmc'
    290                 return True
    291 
    292         scsi_pattern = '/dev/sd[a-z]+'
    293         if re.match(scsi_pattern, rootdev.stdout):
    294             # Read symlink for /sys/block/sd* to determine if the internal
    295             # device is connected via ata or usb.
    296             link_cmd = 'readlink /sys/block/%s' % rootdev_base
    297             link = host.run(command=link_cmd, ignore_status=True)
    298             if link.exit_status:
    299                 logging.info("Fail to run %s", link_cmd)
    300                 return False
    301             link_str = link.stdout.strip()
    302             if 'usb' in link_str:
    303                 return False
    304 
    305             # Read rotation to determine if the internal device is ssd or hdd.
    306             rotate_cmd = str('cat /sys/block/%s/queue/rotational'
    307                               % rootdev_base)
    308             rotate = host.run(command=rotate_cmd, ignore_status=True)
    309             if rotate.exit_status:
    310                 logging.info("Fail to run %s", rotate_cmd)
    311                 return False
    312             rotate_str = rotate.stdout.strip()
    313 
    314             rotate_dict = {'0':'ssd', '1':'hdd'}
    315             self.type_str = rotate_dict.get(rotate_str)
    316             return True
    317 
    318         # All other internal device / error case will always fall here
    319         return False
    320 
    321 
    322     def generate_labels(self, host):
    323         return [self.type_str]
    324 
    325 
    326 class ServoLabel(base_label.BaseLabel):
    327     """Label to apply if a servo is present."""
    328 
    329     _NAME = 'servo'
    330 
    331     def exists(self, host):
    332         """
    333         Check if the servo label should apply to the host or not.
    334 
    335         @returns True if a servo host is detected, False otherwise.
    336         """
    337         servo_args, _ = servo_host._get_standard_servo_args(host)
    338         servo_host_hostname = servo_args.get(servo_host.SERVO_HOST_ATTR)
    339         return (servo_host_hostname is not None
    340                 and servo_host.servo_host_is_up(servo_host_hostname))
    341 
    342 
    343 class VideoLabel(base_label.StringLabel):
    344     """Labels detailing video capabilities."""
    345 
    346     # List gathered from
    347     # https://chromium.googlesource.com/chromiumos/
    348     # platform2/+/master/avtest_label_detect/main.c#19
    349     _NAME = [
    350         'hw_jpeg_acc_dec',
    351         'hw_video_acc_h264',
    352         'hw_video_acc_vp8',
    353         'hw_video_acc_vp9',
    354         'hw_video_acc_enc_h264',
    355         'hw_video_acc_enc_vp8',
    356         'webcam',
    357     ]
    358 
    359     def generate_labels(self, host):
    360         result = host.run('/usr/local/bin/avtest_label_detect',
    361                           ignore_status=True).stdout
    362         return re.findall('^Detected label: (\w+)$', result, re.M)
    363 
    364 
    365 class CTSArchLabel(base_label.StringLabel):
    366     """Labels to determine CTS abi."""
    367 
    368     _NAME = ['cts_abi_arm', 'cts_abi_x86']
    369 
    370     def _get_cts_abis(self, host):
    371         """Return supported CTS ABIs.
    372 
    373         @return List of supported CTS bundle ABIs.
    374         """
    375         cts_abis = {'x86_64': ['arm', 'x86'], 'arm': ['arm']}
    376         return cts_abis.get(host.get_cpu_arch(), [])
    377 
    378 
    379     def generate_labels(self, host):
    380         return ['cts_abi_' + abi for abi in self._get_cts_abis(host)]
    381 
    382 
    383 class ArcLabel(base_label.BaseLabel):
    384     """Label indicates if host has ARC support."""
    385 
    386     _NAME = 'arc'
    387 
    388     @base_label.forever_exists_decorate
    389     def exists(self, host):
    390         return 0 == host.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release',
    391                              ignore_status=True).exit_status
    392 
    393 
    394 class VideoGlitchLabel(base_label.BaseLabel):
    395     """Label indicates if host supports video glitch detection tests."""
    396 
    397     _NAME = 'video_glitch_detection'
    398 
    399     def exists(self, host):
    400         board = host.get_board().replace(ds_constants.BOARD_PREFIX, '')
    401 
    402         return board in video_test_constants.SUPPORTED_BOARDS
    403 
    404 
    405 class InternalDisplayLabel(base_label.StringLabel):
    406     """Label that determines if the device has an internal display."""
    407 
    408     _NAME = 'internal_display'
    409 
    410     def generate_labels(self, host):
    411         from autotest_lib.client.cros.graphics import graphics_utils
    412         from autotest_lib.client.common_lib import utils as common_utils
    413 
    414         def __system_output(cmd):
    415             return host.run(cmd).stdout
    416 
    417         def __read_file(remote_path):
    418             return host.run('cat %s' % remote_path).stdout
    419 
    420         # Hijack the necessary client functions so that we can take advantage
    421         # of the client lib here.
    422         # FIXME: find a less hacky way than this
    423         original_system_output = utils.system_output
    424         original_read_file = common_utils.read_file
    425         utils.system_output = __system_output
    426         common_utils.read_file = __read_file
    427         try:
    428             return ([self._NAME]
    429                     if graphics_utils.has_internal_display()
    430                     else [])
    431         finally:
    432             utils.system_output = original_system_output
    433             common_utils.read_file = original_read_file
    434 
    435 
    436 class LucidSleepLabel(base_label.BaseLabel):
    437     """Label that determines if device has support for lucid sleep."""
    438 
    439     # TODO(kevcheng): See if we can determine if this label is applicable a
    440     # better way (crbug.com/592146).
    441     _NAME = 'lucidsleep'
    442     LUCID_SLEEP_BOARDS = ['samus', 'lulu']
    443 
    444     def exists(self, host):
    445         board = host.get_board().replace(ds_constants.BOARD_PREFIX, '')
    446         return board in self.LUCID_SLEEP_BOARDS
    447 
    448 
    449 class HWIDLabel(base_label.StringLabel):
    450     """Return all the labels generated from the hwid."""
    451 
    452     # We leave out _NAME because hwid_lib will generate everything for us.
    453 
    454     def __init__(self):
    455         # Grab the key file needed to access the hwid service.
    456         self.key_file = global_config.global_config.get_config_value(
    457                 'CROS', 'HWID_KEY', type=str)
    458 
    459 
    460     def generate_labels(self, host):
    461         hwid_labels = []
    462         hwid = host.run_output('crossystem hwid').strip()
    463         hwid_info_list = hwid_lib.get_hwid_info(hwid, hwid_lib.HWID_INFO_LABEL,
    464                                                 self.key_file).get('labels', [])
    465 
    466         for hwid_info in hwid_info_list:
    467             # If it's a prefix, we'll have:
    468             # {'name': prefix_label, 'value': postfix_label} and create
    469             # 'prefix_label:postfix_label'; otherwise it'll just be
    470             # {'name': label} which should just be 'label'.
    471             value = hwid_info.get('value', '')
    472             name = hwid_info.get('name', '')
    473             # There should always be a name but just in case there is not.
    474             if name:
    475                 hwid_labels.append(name if not value else
    476                                    '%s:%s' % (name, value))
    477         return hwid_labels
    478 
    479 
    480     def get_all_labels(self):
    481         """We need to try all labels as a prefix and as standalone.
    482 
    483         We don't know for sure which labels are prefix labels and which are
    484         standalone so we try all of them as both.
    485         """
    486         all_hwid_labels = []
    487         try:
    488             all_hwid_labels = hwid_lib.get_all_possible_dut_labels(
    489                     self.key_file)
    490         except IOError:
    491             logging.error('Can not open key file: %s', self.key_file)
    492         except hwid_lib.HwIdException as e:
    493             logging.error('hwid service: %s', e)
    494         return all_hwid_labels, all_hwid_labels
    495 
    496 
    497 CROS_LABELS = [
    498     AccelsLabel(),
    499     ArcLabel(),
    500     AudioLoopbackDongleLabel(),
    501     BluetoothLabel(),
    502     BoardLabel(),
    503     ChameleonConnectionLabel(),
    504     ChameleonLabel(),
    505     ChameleonPeripheralsLabel(),
    506     common_label.OSLabel(),
    507     CTSArchLabel(),
    508     ECLabel(),
    509     HWIDLabel(),
    510     InternalDisplayLabel(),
    511     LightSensorLabel(),
    512     LucidSleepLabel(),
    513     PowerSupplyLabel(),
    514     ServoLabel(),
    515     StorageLabel(),
    516     VideoGlitchLabel(),
    517     VideoLabel(),
    518 ]
    519