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