Home | History | Annotate | Download | only in hosts
      1 # Copyright (c) 2012 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 import logging
      6 import os
      7 import re
      8 import sys
      9 import time
     10 
     11 import common
     12 from autotest_lib.client.bin import utils
     13 from autotest_lib.client.common_lib import autotemp
     14 from autotest_lib.client.common_lib import error
     15 from autotest_lib.client.common_lib import global_config
     16 from autotest_lib.client.common_lib import hosts
     17 from autotest_lib.client.common_lib import lsbrelease_utils
     18 from autotest_lib.client.common_lib.cros import dev_server
     19 from autotest_lib.client.common_lib.cros import retry
     20 from autotest_lib.client.cros import constants as client_constants
     21 from autotest_lib.client.cros import cros_ui
     22 from autotest_lib.server import afe_utils
     23 from autotest_lib.server import utils as server_utils
     24 from autotest_lib.server.cros import provision
     25 from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
     26 from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers
     27 from autotest_lib.server.cros.servo import plankton
     28 from autotest_lib.server.hosts import abstract_ssh
     29 from autotest_lib.server.hosts import base_label
     30 from autotest_lib.server.hosts import chameleon_host
     31 from autotest_lib.server.hosts import cros_label
     32 from autotest_lib.server.hosts import cros_repair
     33 from autotest_lib.server.hosts import plankton_host
     34 from autotest_lib.server.hosts import servo_host
     35 from autotest_lib.site_utils.rpm_control_system import rpm_client
     36 
     37 # In case cros_host is being ran via SSP on an older Moblab version with an
     38 # older chromite version.
     39 try:
     40     from chromite.lib import metrics
     41 except ImportError:
     42     metrics = utils.metrics_mock
     43 
     44 
     45 CONFIG = global_config.global_config
     46 
     47 
     48 class FactoryImageCheckerException(error.AutoservError):
     49     """Exception raised when an image is a factory image."""
     50     pass
     51 
     52 
     53 class CrosHost(abstract_ssh.AbstractSSHHost):
     54     """Chromium OS specific subclass of Host."""
     55 
     56     VERSION_PREFIX = provision.CROS_VERSION_PREFIX
     57 
     58     _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
     59 
     60     # Timeout values (in seconds) associated with various Chrome OS
     61     # state changes.
     62     #
     63     # In general, a good rule of thumb is that the timeout can be up
     64     # to twice the typical measured value on the slowest platform.
     65     # The times here have not necessarily been empirically tested to
     66     # meet this criterion.
     67     #
     68     # SLEEP_TIMEOUT:  Time to allow for suspend to memory.
     69     # RESUME_TIMEOUT: Time to allow for resume after suspend, plus
     70     #   time to restart the netwowrk.
     71     # SHUTDOWN_TIMEOUT: Time to allow for shut down.
     72     # BOOT_TIMEOUT: Time to allow for boot from power off.  Among
     73     #   other things, this must account for the 30 second dev-mode
     74     #   screen delay, time to start the network on the DUT, and the
     75     #   ssh timeout of 120 seconds.
     76     # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device,
     77     #   including the 30 second dev-mode delay and time to start the
     78     #   network.
     79     # INSTALL_TIMEOUT: Time to allow for chromeos-install.
     80     # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that
     81     #   includes powerwash.
     82 
     83     SLEEP_TIMEOUT = 2
     84     RESUME_TIMEOUT = 10
     85     SHUTDOWN_TIMEOUT = 10
     86     BOOT_TIMEOUT = 150
     87     USB_BOOT_TIMEOUT = 300
     88     INSTALL_TIMEOUT = 480
     89     POWERWASH_BOOT_TIMEOUT = 60
     90 
     91     # Minimum OS version that supports server side packaging. Older builds may
     92     # not have server side package built or with Autotest code change to support
     93     # server-side packaging.
     94     MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
     95             'AUTOSERV', 'min_version_support_ssp', type=int)
     96 
     97     # REBOOT_TIMEOUT: How long to wait for a reboot.
     98     #
     99     # We have a long timeout to ensure we don't flakily fail due to other
    100     # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
    101     # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
    102     # return from reboot' bug is solved.
    103     REBOOT_TIMEOUT = 480
    104 
    105     # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
    106     # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
    107     _USB_POWER_TIMEOUT = 5
    108     _POWER_CYCLE_TIMEOUT = 10
    109 
    110     _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
    111                            '-host(\d+)')
    112 
    113     # Constants used in ping_wait_up() and ping_wait_down().
    114     #
    115     # _PING_WAIT_COUNT is the approximate number of polling
    116     # cycles to use when waiting for a host state change.
    117     #
    118     # _PING_STATUS_DOWN and _PING_STATUS_UP are names used
    119     # for arguments to the internal _ping_wait_for_status()
    120     # method.
    121     _PING_WAIT_COUNT = 40
    122     _PING_STATUS_DOWN = False
    123     _PING_STATUS_UP = True
    124 
    125     # Allowed values for the power_method argument.
    126 
    127     # POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods.
    128     # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
    129     # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
    130     POWER_CONTROL_RPM = 'RPM'
    131     POWER_CONTROL_SERVO = 'servoj10'
    132     POWER_CONTROL_MANUAL = 'manual'
    133 
    134     POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
    135                                 POWER_CONTROL_SERVO,
    136                                 POWER_CONTROL_MANUAL)
    137 
    138     _RPM_OUTLET_CHANGED = 'outlet_changed'
    139 
    140     # URL pattern to download firmware image.
    141     _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
    142             'CROS', 'firmware_url_pattern', type=str)
    143 
    144 
    145     @staticmethod
    146     def check_host(host, timeout=10):
    147         """
    148         Check if the given host is a chrome-os host.
    149 
    150         @param host: An ssh host representing a device.
    151         @param timeout: The timeout for the run command.
    152 
    153         @return: True if the host device is chromeos.
    154 
    155         """
    156         try:
    157             result = host.run(
    158                     'grep -q CHROMEOS /etc/lsb-release && '
    159                     '! test -f /mnt/stateful_partition/.android_tester && '
    160                     '! grep -q moblab /etc/lsb-release',
    161                     ignore_status=True, timeout=timeout)
    162             if result.exit_status == 0:
    163                 lsb_release_content = host.run(
    164                     'grep CHROMEOS_RELEASE_BOARD /etc/lsb-release',
    165                     timeout=timeout).stdout
    166                 return not (
    167                     lsbrelease_utils.is_jetstream(
    168                         lsb_release_content=lsb_release_content) or
    169                     lsbrelease_utils.is_gce_board(
    170                         lsb_release_content=lsb_release_content))
    171 
    172         except (error.AutoservRunError, error.AutoservSSHTimeout):
    173             return False
    174 
    175         return False
    176 
    177 
    178     @staticmethod
    179     def get_chameleon_arguments(args_dict):
    180         """Extract chameleon options from `args_dict` and return the result.
    181 
    182         Recommended usage:
    183         ~~~~~~~~
    184             args_dict = utils.args_to_dict(args)
    185             chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
    186             host = hosts.create_host(machine, chameleon_args=chameleon_args)
    187         ~~~~~~~~
    188 
    189         @param args_dict Dictionary from which to extract the chameleon
    190           arguments.
    191         """
    192         return {key: args_dict[key]
    193                 for key in ('chameleon_host', 'chameleon_port')
    194                 if key in args_dict}
    195 
    196 
    197     @staticmethod
    198     def get_plankton_arguments(args_dict):
    199         """Extract chameleon options from `args_dict` and return the result.
    200 
    201         Recommended usage:
    202         ~~~~~~~~
    203             args_dict = utils.args_to_dict(args)
    204             plankton_args = hosts.CrosHost.get_plankton_arguments(args_dict)
    205             host = hosts.create_host(machine, plankton_args=plankton_args)
    206         ~~~~~~~~
    207 
    208         @param args_dict Dictionary from which to extract the plankton
    209           arguments.
    210         """
    211         return {key: args_dict[key]
    212                 for key in ('plankton_host', 'plankton_port')
    213                 if key in args_dict}
    214 
    215 
    216     @staticmethod
    217     def get_servo_arguments(args_dict):
    218         """Extract servo options from `args_dict` and return the result.
    219 
    220         Recommended usage:
    221         ~~~~~~~~
    222             args_dict = utils.args_to_dict(args)
    223             servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
    224             host = hosts.create_host(machine, servo_args=servo_args)
    225         ~~~~~~~~
    226 
    227         @param args_dict Dictionary from which to extract the servo
    228           arguments.
    229         """
    230         servo_attrs = (servo_host.SERVO_HOST_ATTR,
    231                        servo_host.SERVO_PORT_ATTR,
    232                        servo_host.SERVO_BOARD_ATTR,
    233                        servo_host.SERVO_MODEL_ATTR)
    234         servo_args = {key: args_dict[key]
    235                       for key in servo_attrs
    236                       if key in args_dict}
    237         return (
    238             None
    239             if servo_host.SERVO_HOST_ATTR in servo_args
    240                 and not servo_args[servo_host.SERVO_HOST_ATTR]
    241             else servo_args)
    242 
    243 
    244     def _initialize(self, hostname, chameleon_args=None, servo_args=None,
    245                     plankton_args=None, try_lab_servo=False,
    246                     try_servo_repair=False,
    247                     ssh_verbosity_flag='', ssh_options='',
    248                     *args, **dargs):
    249         """Initialize superclasses, |self.chameleon|, and |self.servo|.
    250 
    251         This method will attempt to create the test-assistant object
    252         (chameleon/servo) when it is needed by the test. Check
    253         the docstring of chameleon_host.create_chameleon_host and
    254         servo_host.create_servo_host for how this is determined.
    255 
    256         @param hostname: Hostname of the dut.
    257         @param chameleon_args: A dictionary that contains args for creating
    258                                a ChameleonHost. See chameleon_host for details.
    259         @param servo_args: A dictionary that contains args for creating
    260                            a ServoHost object. See servo_host for details.
    261         @param try_lab_servo: When true, indicates that an attempt should
    262                               be made to create a ServoHost for a DUT in
    263                               the test lab, even if not required by
    264                               `servo_args`. See servo_host for details.
    265         @param try_servo_repair: If a servo host is created, check it
    266                               with `repair()` rather than `verify()`.
    267                               See servo_host for details.
    268         @param ssh_verbosity_flag: String, to pass to the ssh command to control
    269                                    verbosity.
    270         @param ssh_options: String, other ssh options to pass to the ssh
    271                             command.
    272         """
    273         super(CrosHost, self)._initialize(hostname=hostname,
    274                                           *args, **dargs)
    275         self._repair_strategy = cros_repair.create_cros_repair_strategy()
    276         self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS)
    277         # self.env is a dictionary of environment variable settings
    278         # to be exported for commands run on the host.
    279         # LIBC_FATAL_STDERR_ can be useful for diagnosing certain
    280         # errors that might happen.
    281         self.env['LIBC_FATAL_STDERR_'] = '1'
    282         self._ssh_verbosity_flag = ssh_verbosity_flag
    283         self._ssh_options = ssh_options
    284         self.set_servo_host(
    285             servo_host.create_servo_host(
    286                 dut=self, servo_args=servo_args,
    287                 try_lab_servo=try_lab_servo,
    288                 try_servo_repair=try_servo_repair))
    289 
    290         # TODO(waihong): Do the simplication on Chameleon too.
    291         self._chameleon_host = chameleon_host.create_chameleon_host(
    292                 dut=self.hostname, chameleon_args=chameleon_args)
    293         # Add plankton host if plankton args were added on command line
    294         self._plankton_host = plankton_host.create_plankton_host(plankton_args)
    295 
    296         if self._chameleon_host:
    297             self.chameleon = self._chameleon_host.create_chameleon_board()
    298         else:
    299             self.chameleon = None
    300 
    301         if self._plankton_host:
    302             self.plankton_servo = self._plankton_host.get_servo()
    303             logging.info('plankton_servo: %r', self.plankton_servo)
    304             # Create the plankton object used to access the ec uart
    305             self.plankton = plankton.Plankton(self.plankton_servo,
    306                     self._plankton_host.get_servod_server_proxy())
    307         else:
    308             self.plankton = None
    309 
    310 
    311     def get_cros_repair_image_name(self):
    312         info = self.host_info_store.get()
    313         if not info.board:
    314             raise error.AutoservError('Cannot obtain repair image name. '
    315                                       'No board label value found')
    316         return afe_utils.get_stable_cros_image_name(info.board)
    317 
    318 
    319     def host_version_prefix(self, image):
    320         """Return version label prefix.
    321 
    322         In case the CrOS provisioning version is something other than the
    323         standard CrOS version e.g. CrOS TH version, this function will
    324         find the prefix from provision.py.
    325 
    326         @param image: The image name to find its version prefix.
    327         @returns: A prefix string for the image type.
    328         """
    329         return provision.get_version_label_prefix(image)
    330 
    331 
    332     def verify_job_repo_url(self, tag=''):
    333         """
    334         Make sure job_repo_url of this host is valid.
    335 
    336         Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\
    337         lumpy-release/R29-4279.0.0/autotest/packages" claims to have the
    338         autotest package for lumpy-release/R29-4279.0.0. If this isn't the case,
    339         download and extract it. If the devserver embedded in the url is
    340         unresponsive, update the job_repo_url of the host after staging it on
    341         another devserver.
    342 
    343         @param job_repo_url: A url pointing to the devserver where the autotest
    344             package for this build should be staged.
    345         @param tag: The tag from the server job, in the format
    346                     <job_id>-<user>/<hostname>, or <hostless> for a server job.
    347 
    348         @raises DevServerException: If we could not resolve a devserver.
    349         @raises AutoservError: If we're unable to save the new job_repo_url as
    350             a result of choosing a new devserver because the old one failed to
    351             respond to a health check.
    352         @raises urllib2.URLError: If the devserver embedded in job_repo_url
    353                                   doesn't respond within the timeout.
    354         """
    355         info = self.host_info_store.get()
    356         job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '')
    357         if not job_repo_url:
    358             logging.warning('No job repo url set on host %s', self.hostname)
    359             return
    360 
    361         logging.info('Verifying job repo url %s', job_repo_url)
    362         devserver_url, image_name = tools.get_devserver_build_from_package_url(
    363             job_repo_url)
    364 
    365         ds = dev_server.ImageServer(devserver_url)
    366 
    367         logging.info('Staging autotest artifacts for %s on devserver %s',
    368             image_name, ds.url())
    369 
    370         start_time = time.time()
    371         ds.stage_artifacts(image_name, ['autotest_packages'])
    372         stage_time = time.time() - start_time
    373 
    374         # Record how much of the verification time comes from a devserver
    375         # restage. If we're doing things right we should not see multiple
    376         # devservers for a given board/build/branch path.
    377         try:
    378             board, build_type, branch = server_utils.ParseBuildName(
    379                                                 image_name)[:3]
    380         except server_utils.ParseBuildNameException:
    381             pass
    382         else:
    383             devserver = devserver_url[
    384                 devserver_url.find('/') + 2:devserver_url.rfind(':')]
    385             stats_key = {
    386                 'board': board,
    387                 'build_type': build_type,
    388                 'branch': branch,
    389                 'devserver': devserver.replace('.', '_'),
    390             }
    391 
    392             monarch_fields = {
    393                 'board': board,
    394                 'build_type': build_type,
    395                 # TODO(akeshet): To be consistent with most other metrics,
    396                 # consider changing the following field to be named
    397                 # 'milestone'.
    398                 'branch': branch,
    399                 'dev_server': devserver,
    400             }
    401             metrics.Counter(
    402                     'chromeos/autotest/provision/verify_url'
    403                     ).increment(fields=monarch_fields)
    404             metrics.SecondsDistribution(
    405                     'chromeos/autotest/provision/verify_url_duration'
    406                     ).add(stage_time, fields=monarch_fields)
    407 
    408 
    409     def stage_server_side_package(self, image=None):
    410         """Stage autotest server-side package on devserver.
    411 
    412         @param image: Full path of an OS image to install or a build name.
    413 
    414         @return: A url to the autotest server-side package.
    415 
    416         @raise: error.AutoservError if fail to locate the build to test with, or
    417                 fail to stage server-side package.
    418         """
    419         # If enable_drone_in_restricted_subnet is False, do not set hostname
    420         # in devserver.resolve call, so a devserver in non-restricted subnet
    421         # is picked to stage autotest server package for drone to download.
    422         hostname = self.hostname
    423         if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
    424             hostname = None
    425         if image:
    426             image_name = tools.get_build_from_image(image)
    427             if not image_name:
    428                 raise error.AutoservError(
    429                         'Failed to parse build name from %s' % image)
    430             ds = dev_server.ImageServer.resolve(image_name, hostname)
    431         else:
    432             info = self.host_info_store.get()
    433             job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '')
    434             if job_repo_url:
    435                 devserver_url, image_name = (
    436                     tools.get_devserver_build_from_package_url(job_repo_url))
    437                 # If enable_drone_in_restricted_subnet is True, use the
    438                 # existing devserver. Otherwise, resolve a new one in
    439                 # non-restricted subnet.
    440                 if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
    441                     ds = dev_server.ImageServer(devserver_url)
    442                 else:
    443                     ds = dev_server.ImageServer.resolve(image_name)
    444             elif info.build is not None:
    445                 ds = dev_server.ImageServer.resolve(info.build, hostname)
    446                 image_name = info.build
    447             else:
    448                 raise error.AutoservError(
    449                         'Failed to stage server-side package. The host has '
    450                         'no job_report_url attribute or version label.')
    451 
    452         # Get the OS version of the build, for any build older than
    453         # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported.
    454         match = re.match('.*/R\d+-(\d+)\.', image_name)
    455         if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP:
    456             raise error.AutoservError(
    457                     'Build %s is older than %s. Server side packaging is '
    458                     'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP))
    459 
    460         ds.stage_artifacts(image_name, ['autotest_server_package'])
    461         return '%s/static/%s/%s' % (ds.url(), image_name,
    462                                     'autotest_server_package.tar.bz2')
    463 
    464 
    465     def stage_image_for_servo(self, image_name=None, artifact='test_image'):
    466         """Stage a build on a devserver and return the update_url.
    467 
    468         @param image_name: a name like lumpy-release/R27-3837.0.0
    469         @param artifact: a string like 'test_image'. Requests
    470             appropriate image to be staged.
    471         @returns an update URL like:
    472             http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
    473         """
    474         if not image_name:
    475             image_name = self.get_cros_repair_image_name()
    476         logging.info('Staging build for servo install: %s', image_name)
    477         devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
    478         devserver.stage_artifacts(image_name, [artifact])
    479         if artifact == 'test_image':
    480             return devserver.get_test_image_url(image_name)
    481         elif artifact == 'recovery_image':
    482             return devserver.get_recovery_image_url(image_name)
    483         else:
    484             raise error.AutoservError("Bad artifact!")
    485 
    486 
    487     def stage_factory_image_for_servo(self, image_name):
    488         """Stage a build on a devserver and return the update_url.
    489 
    490         @param image_name: a name like <baord>/4262.204.0
    491 
    492         @return: An update URL, eg:
    493             http://<devserver>/static/canary-channel/\
    494             <board>/4262.204.0/factory_test/chromiumos_factory_image.bin
    495 
    496         @raises: ValueError if the factory artifact name is missing from
    497                  the config.
    498 
    499         """
    500         if not image_name:
    501             logging.error('Need an image_name to stage a factory image.')
    502             return
    503 
    504         factory_artifact = CONFIG.get_config_value(
    505                 'CROS', 'factory_artifact', type=str, default='')
    506         if not factory_artifact:
    507             raise ValueError('Cannot retrieve the factory artifact name from '
    508                              'autotest config, and hence cannot stage factory '
    509                              'artifacts.')
    510 
    511         logging.info('Staging build for servo install: %s', image_name)
    512         devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
    513         devserver.stage_artifacts(
    514                 image_name,
    515                 [factory_artifact],
    516                 archive_url=None)
    517 
    518         return tools.factory_image_url_pattern() % (devserver.url(), image_name)
    519 
    520 
    521     def prepare_for_update(self):
    522         """Prepares the DUT for an update.
    523 
    524         Subclasses may override this to perform any special actions
    525         required before updating.
    526         """
    527         pass
    528 
    529 
    530     def _clear_fw_version_labels(self, rw_only):
    531         """Clear firmware version labels from the machine.
    532 
    533         @param rw_only: True to only clear fwrw_version; otherewise, clear
    534                         both fwro_version and fwrw_version.
    535         """
    536         labels = self._AFE.get_labels(
    537                 name__startswith=provision.FW_RW_VERSION_PREFIX,
    538                 host__hostname=self.hostname)
    539         if not rw_only:
    540             labels = labels + self._AFE.get_labels(
    541                     name__startswith=provision.FW_RO_VERSION_PREFIX,
    542                     host__hostname=self.hostname)
    543         for label in labels:
    544             label.remove_hosts(hosts=[self.hostname])
    545 
    546 
    547     def _add_fw_version_label(self, build, rw_only):
    548         """Add firmware version label to the machine.
    549 
    550         @param build: Build of firmware.
    551         @param rw_only: True to only add fwrw_version; otherwise, add both
    552                         fwro_version and fwrw_version.
    553 
    554         """
    555         fw_label = provision.fwrw_version_to_label(build)
    556         self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
    557         if not rw_only:
    558             fw_label = provision.fwro_version_to_label(build)
    559             self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
    560 
    561 
    562     def firmware_install(self, build=None, rw_only=False):
    563         """Install firmware to the DUT.
    564 
    565         Use stateful update if the DUT is already running the same build.
    566         Stateful update does not update kernel and tends to run much faster
    567         than a full reimage. If the DUT is running a different build, or it
    568         failed to do a stateful update, full update, including kernel update,
    569         will be applied to the DUT.
    570 
    571         Once a host enters firmware_install its fw[ro|rw]_version label will
    572         be removed. After the firmware is updated successfully, a new
    573         fw[ro|rw]_version label will be added to the host.
    574 
    575         @param build: The build version to which we want to provision the
    576                       firmware of the machine,
    577                       e.g. 'link-firmware/R22-2695.1.144'.
    578         @param rw_only: True to only install firmware to its RW portions. Keep
    579                         the RO portions unchanged.
    580 
    581         TODO(dshi): After bug 381718 is fixed, update here with corresponding
    582                     exceptions that could be raised.
    583 
    584         """
    585         if not self.servo:
    586             raise error.TestError('Host %s does not have servo.' %
    587                                   self.hostname)
    588 
    589         # Get the DUT board name from AFE.
    590         info = self.host_info_store.get()
    591         board = info.board
    592 
    593         if board is None or board == '':
    594             board = self.servo.get_board()
    595 
    596         # If build is not set, try to install firmware from stable CrOS.
    597         if not build:
    598             build = afe_utils.get_stable_faft_version(board)
    599             if not build:
    600                 raise error.TestError(
    601                         'Failed to find stable firmware build for %s.',
    602                         self.hostname)
    603             logging.info('Will install firmware from build %s.', build)
    604 
    605         ds = dev_server.ImageServer.resolve(build, self.hostname)
    606         ds.stage_artifacts(build, ['firmware'])
    607 
    608         tmpd = autotemp.tempdir(unique_id='fwimage')
    609         try:
    610             fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
    611             local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl))
    612             ds.download_file(fwurl, local_tarball)
    613 
    614             self._clear_fw_version_labels(rw_only)
    615             self.servo.program_firmware(board, local_tarball, rw_only)
    616             if utils.host_is_in_lab_zone(self.hostname):
    617                 self._add_fw_version_label(build, rw_only)
    618         finally:
    619             tmpd.clean()
    620 
    621 
    622     def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT,
    623                       install_timeout=INSTALL_TIMEOUT):
    624         """
    625         Re-install the OS on the DUT by:
    626         1) installing a test image on a USB storage device attached to the Servo
    627                 board,
    628         2) booting that image in recovery mode, and then
    629         3) installing the image with chromeos-install.
    630 
    631         @param image_url: If specified use as the url to install on the DUT.
    632                 otherwise boot the currently staged image on the USB stick.
    633         @param usb_boot_timeout: The usb_boot_timeout to use during reimage.
    634                 Factory images need a longer usb_boot_timeout than regular
    635                 cros images.
    636         @param install_timeout: The timeout to use when installing the chromeos
    637                 image. Factory images need a longer install_timeout.
    638 
    639         @raises AutoservError if the image fails to boot.
    640 
    641         """
    642         logging.info('Downloading image to USB, then booting from it. Usb boot '
    643                      'timeout = %s', usb_boot_timeout)
    644         with metrics.SecondsTimer(
    645                 'chromeos/autotest/provision/servo_install/boot_duration'):
    646             self.servo.install_recovery_image(image_url)
    647             if not self.wait_up(timeout=usb_boot_timeout):
    648                 raise hosts.AutoservRepairError(
    649                         'DUT failed to boot from USB after %d seconds' %
    650                         usb_boot_timeout, 'failed_to_reboot')
    651 
    652         # The new chromeos-tpm-recovery has been merged since R44-7073.0.0.
    653         # In old CrOS images, this command fails. Skip the error.
    654         logging.info('Resetting the TPM status')
    655         try:
    656             self.run('chromeos-tpm-recovery')
    657         except error.AutoservRunError:
    658             logging.warn('chromeos-tpm-recovery is too old.')
    659 
    660 
    661         with metrics.SecondsTimer(
    662                 'chromeos/autotest/provision/servo_install/install_duration'):
    663             logging.info('Installing image through chromeos-install.')
    664             self.run('chromeos-install --yes', timeout=install_timeout)
    665             self.halt()
    666 
    667         logging.info('Power cycling DUT through servo.')
    668         self.servo.get_power_state_controller().power_off()
    669         self.servo.switch_usbkey('off')
    670         # N.B. The Servo API requires that we use power_on() here
    671         # for two reasons:
    672         #  1) After turning on a DUT in recovery mode, you must turn
    673         #     it off and then on with power_on() once more to
    674         #     disable recovery mode (this is a Parrot specific
    675         #     requirement).
    676         #  2) After power_off(), the only way to turn on is with
    677         #     power_on() (this is a Storm specific requirement).
    678         self.servo.get_power_state_controller().power_on()
    679 
    680         logging.info('Waiting for DUT to come back up.')
    681         if not self.wait_up(timeout=self.BOOT_TIMEOUT):
    682             raise error.AutoservError('DUT failed to reboot installed '
    683                                       'test image after %d seconds' %
    684                                       self.BOOT_TIMEOUT)
    685 
    686 
    687     def set_servo_host(self, host):
    688         """Set our servo host member, and associated servo.
    689 
    690         @param host  Our new `ServoHost`.
    691         """
    692         self._servo_host = host
    693         if self._servo_host is not None:
    694             self.servo = self._servo_host.get_servo()
    695         else:
    696             self.servo = None
    697 
    698 
    699     def repair_servo(self):
    700         """
    701         Confirm that servo is initialized and verified.
    702 
    703         If the servo object is missing, attempt to repair the servo
    704         host.  Repair failures are passed back to the caller.
    705 
    706         @raise AutoservError: If there is no servo host for this CrOS
    707                               host.
    708         """
    709         if self.servo:
    710             return
    711         if not self._servo_host:
    712             raise error.AutoservError('No servo host for %s.' %
    713                                       self.hostname)
    714         self._servo_host.repair()
    715         self.servo = self._servo_host.get_servo()
    716 
    717 
    718     def repair(self):
    719         """Attempt to get the DUT to pass `self.verify()`.
    720 
    721         This overrides the base class function for repair; it does
    722         not call back to the parent class, but instead relies on
    723         `self._repair_strategy` to coordinate the verification and
    724         repair steps needed to get the DUT working.
    725         """
    726         message = 'Beginning repair for host %s board %s model %s'
    727         info = self.host_info_store.get()
    728         message %= (self.hostname, info.board, info.model)
    729         self.record('INFO', None, None, message)
    730         self._repair_strategy.repair(self)
    731 
    732 
    733     def close(self):
    734         """Close connection."""
    735         super(CrosHost, self).close()
    736         if self._chameleon_host:
    737             self._chameleon_host.close()
    738 
    739         if self._servo_host:
    740             self._servo_host.close()
    741 
    742 
    743     def get_power_supply_info(self):
    744         """Get the output of power_supply_info.
    745 
    746         power_supply_info outputs the info of each power supply, e.g.,
    747         Device: Line Power
    748           online:                  no
    749           type:                    Mains
    750           voltage (V):             0
    751           current (A):             0
    752         Device: Battery
    753           state:                   Discharging
    754           percentage:              95.9276
    755           technology:              Li-ion
    756 
    757         Above output shows two devices, Line Power and Battery, with details of
    758         each device listed. This function parses the output into a dictionary,
    759         with key being the device name, and value being a dictionary of details
    760         of the device info.
    761 
    762         @return: The dictionary of power_supply_info, e.g.,
    763                  {'Line Power': {'online': 'yes', 'type': 'main'},
    764                   'Battery': {'vendor': 'xyz', 'percentage': '100'}}
    765         @raise error.AutoservRunError if power_supply_info tool is not found in
    766                the DUT. Caller should handle this error to avoid false failure
    767                on verification.
    768         """
    769         result = self.run('power_supply_info').stdout.strip()
    770         info = {}
    771         device_name = None
    772         device_info = {}
    773         for line in result.split('\n'):
    774             pair = [v.strip() for v in line.split(':')]
    775             if len(pair) != 2:
    776                 continue
    777             if pair[0] == 'Device':
    778                 if device_name:
    779                     info[device_name] = device_info
    780                 device_name = pair[1]
    781                 device_info = {}
    782             else:
    783                 device_info[pair[0]] = pair[1]
    784         if device_name and not device_name in info:
    785             info[device_name] = device_info
    786         return info
    787 
    788 
    789     def get_battery_percentage(self):
    790         """Get the battery percentage.
    791 
    792         @return: The percentage of battery level, value range from 0-100. Return
    793                  None if the battery info cannot be retrieved.
    794         """
    795         try:
    796             info = self.get_power_supply_info()
    797             logging.info(info)
    798             return float(info['Battery']['percentage'])
    799         except (KeyError, ValueError, error.AutoservRunError):
    800             return None
    801 
    802 
    803     def is_ac_connected(self):
    804         """Check if the dut has power adapter connected and charging.
    805 
    806         @return: True if power adapter is connected and charging.
    807         """
    808         try:
    809             info = self.get_power_supply_info()
    810             return info['Line Power']['online'] == 'yes'
    811         except (KeyError, error.AutoservRunError):
    812             return None
    813 
    814 
    815     def _cleanup_poweron(self):
    816         """Special cleanup method to make sure hosts always get power back."""
    817         info = self.host_info_store.get()
    818         if self._RPM_OUTLET_CHANGED not in info.attributes:
    819             return
    820         logging.debug('This host has recently interacted with the RPM'
    821                       ' Infrastructure. Ensuring power is on.')
    822         try:
    823             self.power_on()
    824             self._remove_rpm_changed_tag()
    825         except rpm_client.RemotePowerException:
    826             logging.error('Failed to turn Power On for this host after '
    827                           'cleanup through the RPM Infrastructure.')
    828 
    829             battery_percentage = self.get_battery_percentage()
    830             if battery_percentage and battery_percentage < 50:
    831                 raise
    832             elif self.is_ac_connected():
    833                 logging.info('The device has power adapter connected and '
    834                              'charging. No need to try to turn RPM on '
    835                              'again.')
    836                 self._remove_rpm_changed_tag()
    837             logging.info('Battery level is now at %s%%. The device may '
    838                          'still have enough power to run test, so no '
    839                          'exception will be raised.', battery_percentage)
    840 
    841 
    842     def _remove_rpm_changed_tag(self):
    843         info = self.host_info_store.get()
    844         del info.attributes[self._RPM_OUTLET_CHANGED]
    845         self.host_info_store.commit(info)
    846 
    847 
    848     def _add_rpm_changed_tag(self):
    849         info = self.host_info_store.get()
    850         info.attributes[self._RPM_OUTLET_CHANGED] = 'true'
    851         self.host_info_store.commit(info)
    852 
    853 
    854 
    855     def _is_factory_image(self):
    856         """Checks if the image on the DUT is a factory image.
    857 
    858         @return: True if the image on the DUT is a factory image.
    859                  False otherwise.
    860         """
    861         result = self.run('[ -f /root/.factory_test ]', ignore_status=True)
    862         return result.exit_status == 0
    863 
    864 
    865     def _restart_ui(self):
    866         """Restart the Chrome UI.
    867 
    868         @raises: FactoryImageCheckerException for factory images, since
    869                  we cannot attempt to restart ui on them.
    870                  error.AutoservRunError for any other type of error that
    871                  occurs while restarting ui.
    872         """
    873         if self._is_factory_image():
    874             raise FactoryImageCheckerException('Cannot restart ui on factory '
    875                                                'images')
    876 
    877         # TODO(jrbarnette):  The command to stop/start the ui job
    878         # should live inside cros_ui, too.  However that would seem
    879         # to imply interface changes to the existing start()/restart()
    880         # functions, which is a bridge too far (for now).
    881         prompt = cros_ui.get_chrome_session_ident(self)
    882         self.run('stop ui; start ui')
    883         cros_ui.wait_for_chrome_ready(prompt, self)
    884 
    885 
    886     def _start_powerd_if_needed(self):
    887         """Start powerd if it isn't already running."""
    888         self.run('start powerd', ignore_status=True)
    889 
    890 
    891     def _get_lsb_release_content(self):
    892         """Return the content of lsb-release file of host."""
    893         return self.run(
    894                 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
    895 
    896 
    897     def get_release_version(self):
    898         """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
    899 
    900         @returns The version string in lsb-release, under attribute
    901                  CHROMEOS_RELEASE_VERSION.
    902         """
    903         return lsbrelease_utils.get_chromeos_release_version(
    904                 lsb_release_content=self._get_lsb_release_content())
    905 
    906 
    907     def get_release_builder_path(self):
    908         """Get the value of CHROMEOS_RELEASE_BUILDER_PATH from lsb-release.
    909 
    910         @returns The version string in lsb-release, under attribute
    911                  CHROMEOS_RELEASE_BUILDER_PATH.
    912         """
    913         return lsbrelease_utils.get_chromeos_release_builder_path(
    914                 lsb_release_content=self._get_lsb_release_content())
    915 
    916 
    917     def get_chromeos_release_milestone(self):
    918         """Get the value of attribute CHROMEOS_RELEASE_BUILD_TYPE
    919         from lsb-release.
    920 
    921         @returns The version string in lsb-release, under attribute
    922                  CHROMEOS_RELEASE_BUILD_TYPE.
    923         """
    924         return lsbrelease_utils.get_chromeos_release_milestone(
    925                 lsb_release_content=self._get_lsb_release_content())
    926 
    927 
    928     def verify_cros_version_label(self):
    929         """ Make sure host's cros-version label match the actual image in dut.
    930 
    931         Remove any cros-version: label that doesn't match that installed in
    932         the dut.
    933 
    934         @param raise_error: Set to True to raise exception if any mismatch found
    935 
    936         @raise error.AutoservError: If any mismatch between cros-version label
    937                                     and the build installed in dut is found.
    938         """
    939         labels = self._AFE.get_labels(
    940                 name__startswith=ds_constants.VERSION_PREFIX,
    941                 host__hostname=self.hostname)
    942         mismatch_found = False
    943         if labels:
    944             # Ask the DUT for its canonical image name.  This will be in
    945             # a form like this:  kevin-release/R66-10405.0.0
    946             release_builder_path = self.get_release_builder_path()
    947             host_list = [self.hostname]
    948             for label in labels:
    949                 # Remove any cros-version label that does not match
    950                 # the DUT's installed image.
    951                 #
    952                 # TODO(jrbarnette):  We make exceptions for certain
    953                 # known cases where the version label will not match the
    954                 # original CHROMEOS_RELEASE_BUILDER_PATH setting:
    955                 #  * Tests for the `arc-presubmit` pool append
    956                 #    "-cheetsth" to the label.
    957                 #  * Moblab use cases based on `cros stage` store images
    958                 #    under a name with the string "-custom" embedded.
    959                 #    It's not reliable to match such an image name to the
    960                 #    label.
    961                 label_version = label.name[len(ds_constants.VERSION_PREFIX):]
    962                 if '-custom' in label_version:
    963                     continue
    964                 if label_version.endswith('-cheetsth'):
    965                     label_version = label_version[:-len('-cheetsth')]
    966                 if label_version != release_builder_path:
    967                     logging.warn(
    968                         'cros-version label "%s" does not match '
    969                         'release_builder_path %s. Removing the label.',
    970                         label.name, release_builder_path)
    971                     label.remove_hosts(hosts=host_list)
    972                     mismatch_found = True
    973         if mismatch_found:
    974             raise error.AutoservError('The host has wrong cros-version label.')
    975 
    976 
    977     def cleanup_services(self):
    978         """Reinitializes the device for cleanup.
    979 
    980         Subclasses may override this to customize the cleanup method.
    981 
    982         To indicate failure of the reset, the implementation may raise
    983         any of:
    984             error.AutoservRunError
    985             error.AutotestRunError
    986             FactoryImageCheckerException
    987 
    988         @raises error.AutoservRunError
    989         @raises error.AutotestRunError
    990         @raises error.FactoryImageCheckerException
    991         """
    992         self._restart_ui()
    993         self._start_powerd_if_needed()
    994 
    995 
    996     def cleanup(self):
    997         """Cleanup state on device."""
    998         self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
    999         try:
   1000             self.cleanup_services()
   1001         except (error.AutotestRunError, error.AutoservRunError,
   1002                 FactoryImageCheckerException):
   1003             logging.warning('Unable to restart ui, rebooting device.')
   1004             # Since restarting the UI fails fall back to normal Autotest
   1005             # cleanup routines, i.e. reboot the machine.
   1006             super(CrosHost, self).cleanup()
   1007         # Check if the rpm outlet was manipulated.
   1008         if self.has_power():
   1009             self._cleanup_poweron()
   1010         self.verify_cros_version_label()
   1011 
   1012 
   1013     def reboot(self, **dargs):
   1014         """
   1015         This function reboots the site host. The more generic
   1016         RemoteHost.reboot() performs sync and sleeps for 5
   1017         seconds. This is not necessary for Chrome OS devices as the
   1018         sync should be finished in a short time during the reboot
   1019         command.
   1020         """
   1021         if 'reboot_cmd' not in dargs:
   1022             reboot_timeout = dargs.get('reboot_timeout', 10)
   1023             dargs['reboot_cmd'] = ('sleep 1; '
   1024                                    'reboot & sleep %d; '
   1025                                    'reboot -f' % reboot_timeout)
   1026         # Enable fastsync to avoid running extra sync commands before reboot.
   1027         if 'fastsync' not in dargs:
   1028             dargs['fastsync'] = True
   1029 
   1030         dargs['board'] = self.host_info_store.get().board
   1031         # Record who called us
   1032         orig = sys._getframe(1).f_code
   1033         metric_fields = {'board' : dargs['board'],
   1034                          'dut_host_name' : self.hostname,
   1035                          'success' : True}
   1036         metric_debug_fields = {'board' : dargs['board'],
   1037                                'caller' : "%s:%s" % (orig.co_filename,
   1038                                                      orig.co_name),
   1039                                'success' : True,
   1040                                'error' : ''}
   1041 
   1042         t0 = time.time()
   1043         try:
   1044             super(CrosHost, self).reboot(**dargs)
   1045         except Exception as e:
   1046             metric_fields['success'] = False
   1047             metric_debug_fields['success'] = False
   1048             metric_debug_fields['error'] = type(e).__name__
   1049             raise
   1050         finally:
   1051             duration = int(time.time() - t0)
   1052             metrics.Counter(
   1053                     'chromeos/autotest/autoserv/reboot_count').increment(
   1054                     fields=metric_fields)
   1055             metrics.Counter(
   1056                     'chromeos/autotest/autoserv/reboot_debug').increment(
   1057                     fields=metric_debug_fields)
   1058             metrics.SecondsDistribution(
   1059                     'chromeos/autotest/autoserv/reboot_duration').add(
   1060                     duration, fields=metric_fields)
   1061 
   1062 
   1063     def suspend(self, suspend_time=60,
   1064                 suspend_cmd=None, allow_early_resume=False):
   1065         """
   1066         This function suspends the site host.
   1067 
   1068         @param suspend_time: How long to suspend as integer seconds.
   1069         @param suspend_cmd: Suspend command to execute.
   1070         @param allow_early_resume: If False and if device resumes before
   1071                                    |suspend_time|, throw an error.
   1072 
   1073         @exception AutoservSuspendError Host resumed earlier than
   1074                                          |suspend_time|.
   1075         """
   1076 
   1077         if suspend_cmd is None:
   1078             suspend_cmd = ' && '.join([
   1079                 'echo 0 > /sys/class/rtc/rtc0/wakealarm',
   1080                 'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
   1081                 'powerd_dbus_suspend --delay=0'])
   1082         super(CrosHost, self).suspend(suspend_time, suspend_cmd,
   1083                                       allow_early_resume);
   1084 
   1085 
   1086     def upstart_status(self, service_name):
   1087         """Check the status of an upstart init script.
   1088 
   1089         @param service_name: Service to look up.
   1090 
   1091         @returns True if the service is running, False otherwise.
   1092         """
   1093         return 'start/running' in self.run('status %s' % service_name,
   1094                                            ignore_status=True).stdout
   1095 
   1096     def upstart_stop(self, service_name):
   1097         """Stops an upstart job if it's running.
   1098 
   1099         @param service_name: Service to stop
   1100 
   1101         @returns True if service has been stopped or was already stopped
   1102                  False otherwise.
   1103         """
   1104         if not self.upstart_status(service_name):
   1105             return True
   1106 
   1107         result = self.run('stop %s' % service_name, ignore_status=True)
   1108         if result.exit_status != 0:
   1109             return False
   1110         return True
   1111 
   1112     def upstart_restart(self, service_name):
   1113         """Restarts (or starts) an upstart job.
   1114 
   1115         @param service_name: Service to start/restart
   1116 
   1117         @returns True if service has been started/restarted, False otherwise.
   1118         """
   1119         cmd = 'start'
   1120         if self.upstart_status(service_name):
   1121             cmd = 'restart'
   1122         cmd = cmd + ' %s' % service_name
   1123         result = self.run(cmd)
   1124         if result.exit_status != 0:
   1125             return False
   1126         return True
   1127 
   1128     def verify_software(self):
   1129         """Verify working software on a Chrome OS system.
   1130 
   1131         Tests for the following conditions:
   1132          1. All conditions tested by the parent version of this
   1133             function.
   1134          2. Sufficient space in /mnt/stateful_partition.
   1135          3. Sufficient space in /mnt/stateful_partition/encrypted.
   1136          4. update_engine answers a simple status request over DBus.
   1137 
   1138         """
   1139         super(CrosHost, self).verify_software()
   1140         default_kilo_inodes_required = CONFIG.get_config_value(
   1141                 'SERVER', 'kilo_inodes_required', type=int, default=100)
   1142         board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
   1143         kilo_inodes_required = CONFIG.get_config_value(
   1144                 'SERVER', 'kilo_inodes_required_%s' % board,
   1145                 type=int, default=default_kilo_inodes_required)
   1146         self.check_inodes('/mnt/stateful_partition', kilo_inodes_required)
   1147         self.check_diskspace(
   1148             '/mnt/stateful_partition',
   1149             CONFIG.get_config_value(
   1150                 'SERVER', 'gb_diskspace_required', type=float,
   1151                 default=20.0))
   1152         encrypted_stateful_path = '/mnt/stateful_partition/encrypted'
   1153         # Not all targets build with encrypted stateful support.
   1154         if self.path_exists(encrypted_stateful_path):
   1155             self.check_diskspace(
   1156                 encrypted_stateful_path,
   1157                 CONFIG.get_config_value(
   1158                     'SERVER', 'gb_encrypted_diskspace_required', type=float,
   1159                     default=0.1))
   1160 
   1161         self.wait_for_system_services()
   1162 
   1163         # Factory images don't run update engine,
   1164         # goofy controls dbus on these DUTs.
   1165         if not self._is_factory_image():
   1166             self.run('update_engine_client --status')
   1167 
   1168         self.verify_cros_version_label()
   1169 
   1170 
   1171     @retry.retry(error.AutoservError, timeout_min=5, delay_sec=10)
   1172     def wait_for_system_services(self):
   1173         """Waits for system-services to be running.
   1174 
   1175         Sometimes, update_engine will take a while to update firmware, so we
   1176         should give this some time to finish. See crbug.com/765686#c38 for
   1177         details.
   1178         """
   1179         if not self.upstart_status('system-services'):
   1180             raise error.AutoservError('Chrome failed to reach login. '
   1181                                       'System services not running.')
   1182 
   1183 
   1184     def verify(self):
   1185         """Verify Chrome OS system is in good state."""
   1186         message = 'Beginning verify for host %s board %s model %s'
   1187         info = self.host_info_store.get()
   1188         message %= (self.hostname, info.board, info.model)
   1189         self.record('INFO', None, None, message)
   1190         self._repair_strategy.verify(self)
   1191 
   1192 
   1193     def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
   1194                          connect_timeout=None, alive_interval=None,
   1195                          alive_count_max=None, connection_attempts=None):
   1196         """Override default make_ssh_command to use options tuned for Chrome OS.
   1197 
   1198         Tuning changes:
   1199           - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
   1200           connection failure.  Consistency with remote_access.sh.
   1201 
   1202           - ServerAliveInterval=900; which causes SSH to ping connection every
   1203           900 seconds. In conjunction with ServerAliveCountMax ensures
   1204           that if the connection dies, Autotest will bail out.
   1205           Originally tried 60 secs, but saw frequent job ABORTS where
   1206           the test completed successfully. Later increased from 180 seconds to
   1207           900 seconds to account for tests where the DUT is suspended for
   1208           longer periods of time.
   1209 
   1210           - ServerAliveCountMax=3; consistency with remote_access.sh.
   1211 
   1212           - ConnectAttempts=4; reduce flakiness in connection errors;
   1213           consistency with remote_access.sh.
   1214 
   1215           - UserKnownHostsFile=/dev/null; we don't care about the keys.
   1216           Host keys change with every new installation, don't waste
   1217           memory/space saving them.
   1218 
   1219           - SSH protocol forced to 2; needed for ServerAliveInterval.
   1220 
   1221         @param user User name to use for the ssh connection.
   1222         @param port Port on the target host to use for ssh connection.
   1223         @param opts Additional options to the ssh command.
   1224         @param hosts_file Ignored.
   1225         @param connect_timeout Ignored.
   1226         @param alive_interval Ignored.
   1227         @param alive_count_max Ignored.
   1228         @param connection_attempts Ignored.
   1229         """
   1230         options = ' '.join([opts, '-o Protocol=2'])
   1231         return super(CrosHost, self).make_ssh_command(
   1232             user=user, port=port, opts=options, hosts_file='/dev/null',
   1233             connect_timeout=30, alive_interval=900, alive_count_max=3,
   1234             connection_attempts=4)
   1235 
   1236 
   1237     def syslog(self, message, tag='autotest'):
   1238         """Logs a message to syslog on host.
   1239 
   1240         @param message String message to log into syslog
   1241         @param tag String tag prefix for syslog
   1242 
   1243         """
   1244         self.run('logger -t "%s" "%s"' % (tag, message))
   1245 
   1246 
   1247     def _ping_check_status(self, status):
   1248         """Ping the host once, and return whether it has a given status.
   1249 
   1250         @param status Check the ping status against this value.
   1251         @return True iff `status` and the result of ping are the same
   1252                 (i.e. both True or both False).
   1253 
   1254         """
   1255         ping_val = utils.ping(self.hostname, tries=1, deadline=1)
   1256         return not (status ^ (ping_val == 0))
   1257 
   1258     def _ping_wait_for_status(self, status, timeout):
   1259         """Wait for the host to have a given status (UP or DOWN).
   1260 
   1261         Status is checked by polling.  Polling will not last longer
   1262         than the number of seconds in `timeout`.  The polling
   1263         interval will be long enough that only approximately
   1264         _PING_WAIT_COUNT polling cycles will be executed, subject
   1265         to a maximum interval of about one minute.
   1266 
   1267         @param status Waiting will stop immediately if `ping` of the
   1268                       host returns this status.
   1269         @param timeout Poll for at most this many seconds.
   1270         @return True iff the host status from `ping` matched the
   1271                 requested status at the time of return.
   1272 
   1273         """
   1274         # _ping_check_status() takes about 1 second, hence the
   1275         # "- 1" in the formula below.
   1276         # FIXME: if the ping command errors then _ping_check_status()
   1277         # returns instantly. If timeout is also smaller than twice
   1278         # _PING_WAIT_COUNT then the while loop below forks many
   1279         # thousands of ping commands (see /tmp/test_that_results_XXXXX/
   1280         # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one
   1281         # CPU core for 60 seconds.
   1282         poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1
   1283         end_time = time.time() + timeout
   1284         while time.time() <= end_time:
   1285             if self._ping_check_status(status):
   1286                 return True
   1287             if poll_interval > 0:
   1288                 time.sleep(poll_interval)
   1289 
   1290         # The last thing we did was sleep(poll_interval), so it may
   1291         # have been too long since the last `ping`.  Check one more
   1292         # time, just to be sure.
   1293         return self._ping_check_status(status)
   1294 
   1295     def ping_wait_up(self, timeout):
   1296         """Wait for the host to respond to `ping`.
   1297 
   1298         N.B.  This method is not a reliable substitute for
   1299         `wait_up()`, because a host that responds to ping will not
   1300         necessarily respond to ssh.  This method should only be used
   1301         if the target DUT can be considered functional even if it
   1302         can't be reached via ssh.
   1303 
   1304         @param timeout Minimum time to allow before declaring the
   1305                        host to be non-responsive.
   1306         @return True iff the host answered to ping before the timeout.
   1307 
   1308         """
   1309         return self._ping_wait_for_status(self._PING_STATUS_UP, timeout)
   1310 
   1311     def ping_wait_down(self, timeout):
   1312         """Wait until the host no longer responds to `ping`.
   1313 
   1314         This function can be used as a slightly faster version of
   1315         `wait_down()`, by avoiding potentially long ssh timeouts.
   1316 
   1317         @param timeout Minimum time to allow for the host to become
   1318                        non-responsive.
   1319         @return True iff the host quit answering ping before the
   1320                 timeout.
   1321 
   1322         """
   1323         return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout)
   1324 
   1325     def test_wait_for_sleep(self, sleep_timeout=None):
   1326         """Wait for the client to enter low-power sleep mode.
   1327 
   1328         The test for "is asleep" can't distinguish a system that is
   1329         powered off; to confirm that the unit was asleep, it is
   1330         necessary to force resume, and then call
   1331         `test_wait_for_resume()`.
   1332 
   1333         This function is expected to be called from a test as part
   1334         of a sequence like the following:
   1335 
   1336         ~~~~~~~~
   1337             boot_id = host.get_boot_id()
   1338             # trigger sleep on the host
   1339             host.test_wait_for_sleep()
   1340             # trigger resume on the host
   1341             host.test_wait_for_resume(boot_id)
   1342         ~~~~~~~~
   1343 
   1344         @param sleep_timeout time limit in seconds to allow the host sleep.
   1345 
   1346         @exception TestFail The host did not go to sleep within
   1347                             the allowed time.
   1348         """
   1349         if sleep_timeout is None:
   1350             sleep_timeout = self.SLEEP_TIMEOUT
   1351 
   1352         if not self.ping_wait_down(timeout=sleep_timeout):
   1353             raise error.TestFail(
   1354                 'client failed to sleep after %d seconds' % sleep_timeout)
   1355 
   1356 
   1357     def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
   1358         """Wait for the client to resume from low-power sleep mode.
   1359 
   1360         The `old_boot_id` parameter should be the value from
   1361         `get_boot_id()` obtained prior to entering sleep mode.  A
   1362         `TestFail` exception is raised if the boot id changes.
   1363 
   1364         See @ref test_wait_for_sleep for more on this function's
   1365         usage.
   1366 
   1367         @param old_boot_id A boot id value obtained before the
   1368                                target host went to sleep.
   1369         @param resume_timeout time limit in seconds to allow the host up.
   1370 
   1371         @exception TestFail The host did not respond within the
   1372                             allowed time.
   1373         @exception TestFail The host responded, but the boot id test
   1374                             indicated a reboot rather than a sleep
   1375                             cycle.
   1376         """
   1377         if resume_timeout is None:
   1378             resume_timeout = self.RESUME_TIMEOUT
   1379 
   1380         if not self.wait_up(timeout=resume_timeout):
   1381             raise error.TestFail(
   1382                 'client failed to resume from sleep after %d seconds' %
   1383                     resume_timeout)
   1384         else:
   1385             new_boot_id = self.get_boot_id()
   1386             if new_boot_id != old_boot_id:
   1387                 logging.error('client rebooted (old boot %s, new boot %s)',
   1388                               old_boot_id, new_boot_id)
   1389                 raise error.TestFail(
   1390                     'client rebooted, but sleep was expected')
   1391 
   1392 
   1393     def test_wait_for_shutdown(self, shutdown_timeout=None):
   1394         """Wait for the client to shut down.
   1395 
   1396         The test for "has shut down" can't distinguish a system that
   1397         is merely asleep; to confirm that the unit was down, it is
   1398         necessary to force boot, and then call test_wait_for_boot().
   1399 
   1400         This function is expected to be called from a test as part
   1401         of a sequence like the following:
   1402 
   1403         ~~~~~~~~
   1404             boot_id = host.get_boot_id()
   1405             # trigger shutdown on the host
   1406             host.test_wait_for_shutdown()
   1407             # trigger boot on the host
   1408             host.test_wait_for_boot(boot_id)
   1409         ~~~~~~~~
   1410 
   1411         @param shutdown_timeout time limit in seconds to allow the host down.
   1412         @exception TestFail The host did not shut down within the
   1413                             allowed time.
   1414         """
   1415         if shutdown_timeout is None:
   1416             shutdown_timeout = self.SHUTDOWN_TIMEOUT
   1417 
   1418         if not self.ping_wait_down(timeout=shutdown_timeout):
   1419             raise error.TestFail(
   1420                 'client failed to shut down after %d seconds' %
   1421                     shutdown_timeout)
   1422 
   1423 
   1424     def test_wait_for_boot(self, old_boot_id=None):
   1425         """Wait for the client to boot from cold power.
   1426 
   1427         The `old_boot_id` parameter should be the value from
   1428         `get_boot_id()` obtained prior to shutting down.  A
   1429         `TestFail` exception is raised if the boot id does not
   1430         change.  The boot id test is omitted if `old_boot_id` is not
   1431         specified.
   1432 
   1433         See @ref test_wait_for_shutdown for more on this function's
   1434         usage.
   1435 
   1436         @param old_boot_id A boot id value obtained before the
   1437                                shut down.
   1438 
   1439         @exception TestFail The host did not respond within the
   1440                             allowed time.
   1441         @exception TestFail The host responded, but the boot id test
   1442                             indicated that there was no reboot.
   1443         """
   1444         if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
   1445             raise error.TestFail(
   1446                 'client failed to reboot after %d seconds' %
   1447                     self.REBOOT_TIMEOUT)
   1448         elif old_boot_id:
   1449             if self.get_boot_id() == old_boot_id:
   1450                 logging.error('client not rebooted (boot %s)',
   1451                               old_boot_id)
   1452                 raise error.TestFail(
   1453                     'client is back up, but did not reboot')
   1454 
   1455 
   1456     @staticmethod
   1457     def check_for_rpm_support(hostname):
   1458         """For a given hostname, return whether or not it is powered by an RPM.
   1459 
   1460         @param hostname: hostname to check for rpm support.
   1461 
   1462         @return None if this host does not follows the defined naming format
   1463                 for RPM powered DUT's in the lab. If it does follow the format,
   1464                 it returns a regular expression MatchObject instead.
   1465         """
   1466         return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
   1467 
   1468 
   1469     def has_power(self):
   1470         """For this host, return whether or not it is powered by an RPM.
   1471 
   1472         @return True if this host is in the CROS lab and follows the defined
   1473                 naming format.
   1474         """
   1475         return CrosHost.check_for_rpm_support(self.hostname)
   1476 
   1477 
   1478     def _set_power(self, state, power_method):
   1479         """Sets the power to the host via RPM, Servo or manual.
   1480 
   1481         @param state Specifies which power state to set to DUT
   1482         @param power_method Specifies which method of power control to
   1483                             use. By default "RPM" will be used. Valid values
   1484                             are the strings "RPM", "manual", "servoj10".
   1485 
   1486         """
   1487         ACCEPTABLE_STATES = ['ON', 'OFF']
   1488 
   1489         if state.upper() not in ACCEPTABLE_STATES:
   1490             raise error.TestError('State must be one of: %s.'
   1491                                    % (ACCEPTABLE_STATES,))
   1492 
   1493         if power_method == self.POWER_CONTROL_SERVO:
   1494             logging.info('Setting servo port J10 to %s', state)
   1495             self.servo.set('prtctl3_pwren', state.lower())
   1496             time.sleep(self._USB_POWER_TIMEOUT)
   1497         elif power_method == self.POWER_CONTROL_MANUAL:
   1498             logging.info('You have %d seconds to set the AC power to %s.',
   1499                          self._POWER_CYCLE_TIMEOUT, state)
   1500             time.sleep(self._POWER_CYCLE_TIMEOUT)
   1501         else:
   1502             if not self.has_power():
   1503                 raise error.TestFail('DUT does not have RPM connected.')
   1504             self._add_rpm_changed_tag()
   1505             rpm_client.set_power(self, state.upper(), timeout_mins=5)
   1506 
   1507 
   1508     def power_off(self, power_method=POWER_CONTROL_RPM):
   1509         """Turn off power to this host via RPM, Servo or manual.
   1510 
   1511         @param power_method Specifies which method of power control to
   1512                             use. By default "RPM" will be used. Valid values
   1513                             are the strings "RPM", "manual", "servoj10".
   1514 
   1515         """
   1516         self._set_power('OFF', power_method)
   1517 
   1518 
   1519     def power_on(self, power_method=POWER_CONTROL_RPM):
   1520         """Turn on power to this host via RPM, Servo or manual.
   1521 
   1522         @param power_method Specifies which method of power control to
   1523                             use. By default "RPM" will be used. Valid values
   1524                             are the strings "RPM", "manual", "servoj10".
   1525 
   1526         """
   1527         self._set_power('ON', power_method)
   1528 
   1529 
   1530     def power_cycle(self, power_method=POWER_CONTROL_RPM):
   1531         """Cycle power to this host by turning it OFF, then ON.
   1532 
   1533         @param power_method Specifies which method of power control to
   1534                             use. By default "RPM" will be used. Valid values
   1535                             are the strings "RPM", "manual", "servoj10".
   1536 
   1537         """
   1538         if power_method in (self.POWER_CONTROL_SERVO,
   1539                             self.POWER_CONTROL_MANUAL):
   1540             self.power_off(power_method=power_method)
   1541             time.sleep(self._POWER_CYCLE_TIMEOUT)
   1542             self.power_on(power_method=power_method)
   1543         else:
   1544             self._add_rpm_changed_tag()
   1545             rpm_client.set_power(self, 'CYCLE')
   1546 
   1547 
   1548     def get_platform(self):
   1549         """Determine the correct platform label for this host.
   1550 
   1551         @returns a string representing this host's platform.
   1552         """
   1553         release_info = utils.parse_cmd_output('cat /etc/lsb-release',
   1554                                               run_method=self.run)
   1555         unibuild = release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1'
   1556         platform = ''
   1557         if unibuild:
   1558             cmd = 'mosys platform model'
   1559             result = self.run(command=cmd, ignore_status=True)
   1560             if result.exit_status == 0:
   1561                 platform = result.stdout.strip()
   1562 
   1563         if not platform:
   1564             # Look at the firmware for non-unibuild cases or if mosys fails.
   1565             crossystem = utils.Crossystem(self)
   1566             crossystem.init()
   1567             # Extract fwid value and use the leading part as the platform id.
   1568             # fwid generally follow the format of {platform}.{firmware version}
   1569             # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z
   1570             platform = crossystem.fwid().split('.')[0].lower()
   1571             # Newer platforms start with 'Google_' while the older ones do not.
   1572             platform = platform.replace('google_', '')
   1573         return platform
   1574 
   1575 
   1576     def get_architecture(self):
   1577         """Determine the correct architecture label for this host.
   1578 
   1579         @returns a string representing this host's architecture.
   1580         """
   1581         crossystem = utils.Crossystem(self)
   1582         crossystem.init()
   1583         return crossystem.arch()
   1584 
   1585 
   1586     def get_chrome_version(self):
   1587         """Gets the Chrome version number and milestone as strings.
   1588 
   1589         Invokes "chrome --version" to get the version number and milestone.
   1590 
   1591         @return A tuple (chrome_ver, milestone) where "chrome_ver" is the
   1592             current Chrome version number as a string (in the form "W.X.Y.Z")
   1593             and "milestone" is the first component of the version number
   1594             (the "W" from "W.X.Y.Z").  If the version number cannot be parsed
   1595             in the "W.X.Y.Z" format, the "chrome_ver" will be the full output
   1596             of "chrome --version" and the milestone will be the empty string.
   1597 
   1598         """
   1599         version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout
   1600         return utils.parse_chrome_version(version_string)
   1601 
   1602 
   1603     def get_ec_version(self):
   1604         """Get the ec version as strings.
   1605 
   1606         @returns a string representing this host's ec version.
   1607         """
   1608         command = 'mosys ec info -s fw_version'
   1609         result = self.run(command, ignore_status=True)
   1610         if result.exit_status != 0:
   1611             return ''
   1612         return result.stdout.strip()
   1613 
   1614 
   1615     def get_firmware_version(self):
   1616         """Get the firmware version as strings.
   1617 
   1618         @returns a string representing this host's firmware version.
   1619         """
   1620         crossystem = utils.Crossystem(self)
   1621         crossystem.init()
   1622         return crossystem.fwid()
   1623 
   1624 
   1625     def get_hardware_revision(self):
   1626         """Get the hardware revision as strings.
   1627 
   1628         @returns a string representing this host's hardware revision.
   1629         """
   1630         command = 'mosys platform version'
   1631         result = self.run(command, ignore_status=True)
   1632         if result.exit_status != 0:
   1633             return ''
   1634         return result.stdout.strip()
   1635 
   1636 
   1637     def get_kernel_version(self):
   1638         """Get the kernel version as strings.
   1639 
   1640         @returns a string representing this host's kernel version.
   1641         """
   1642         return self.run('uname -r').stdout.strip()
   1643 
   1644 
   1645     def get_cpu_name(self):
   1646         """Get the cpu name as strings.
   1647 
   1648         @returns a string representing this host's cpu name.
   1649         """
   1650 
   1651         # Try get cpu name from device tree first
   1652         if self.path_exists('/proc/device-tree/compatible'):
   1653             command = ' | '.join(
   1654                     ["sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible",
   1655                      'tail -1'])
   1656             return self.run(command).stdout.strip().replace(',', ' ')
   1657 
   1658         # Get cpu name from uname -p
   1659         command = 'uname -p'
   1660         ret = self.run(command).stdout.strip()
   1661 
   1662         # 'uname -p' return variant of unknown or amd64 or x86_64 or i686
   1663         # Try get cpu name from /proc/cpuinfo instead
   1664         if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE):
   1665             command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1"
   1666             self = self.run(command).stdout.strip()
   1667 
   1668         # Remove bloat from CPU name, for example
   1669         # Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz       -> Intel Core i5-7Y57
   1670         # Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz     -> Intel Xeon E5-2690 v4
   1671         # AMD A10-7850K APU with Radeon(TM) R7 Graphics -> AMD A10-7850K
   1672         # AMD GX-212JC SOC with Radeon(TM) R2E Graphics -> AMD GX-212JC
   1673         trim_re = r' (@|processor|apu|soc|radeon).*|\(.*?\)| cpu'
   1674         return re.sub(trim_re, '', ret, flags=re.IGNORECASE)
   1675 
   1676 
   1677     def get_screen_resolution(self):
   1678         """Get the screen(s) resolution as strings.
   1679         In case of more than 1 monitor, return resolution for each monitor
   1680         separate with plus sign.
   1681 
   1682         @returns a string representing this host's screen(s) resolution.
   1683         """
   1684         command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done'
   1685         ret = self.run(command, ignore_status=True)
   1686         # We might have Chromebox without a screen
   1687         if ret.exit_status != 0:
   1688             return ''
   1689         return ret.stdout.strip().replace('\n', '+')
   1690 
   1691 
   1692     def get_mem_total_gb(self):
   1693         """Get total memory available in the system in GiB (2^20).
   1694 
   1695         @returns an integer representing total memory
   1696         """
   1697         mem_total_kb = self.read_from_meminfo('MemTotal')
   1698         kb_in_gb = float(2 ** 20)
   1699         return int(round(mem_total_kb / kb_in_gb))
   1700 
   1701 
   1702     def get_disk_size_gb(self):
   1703         """Get size of disk in GB (10^9)
   1704 
   1705         @returns an integer representing  size of disk, 0 in Error Case
   1706         """
   1707         command = 'grep $(rootdev -s -d | cut -f3 -d/)$ /proc/partitions'
   1708         result = self.run(command, ignore_status=True)
   1709         if result.exit_status != 0:
   1710             return 0
   1711         _, _, block, _ = re.split(r' +', result.stdout.strip())
   1712         byte_per_block = 1024.0
   1713         disk_kb_in_gb = 1e9
   1714         return int(int(block) * byte_per_block / disk_kb_in_gb + 0.5)
   1715 
   1716 
   1717     def get_battery_size(self):
   1718         """Get size of battery in Watt-hour via sysfs
   1719 
   1720         This method assumes that battery support voltage_min_design and
   1721         charge_full_design sysfs.
   1722 
   1723         @returns a float representing Battery size, 0 if error.
   1724         """
   1725         # sysfs report data in micro scale
   1726         battery_scale = 1e6
   1727 
   1728         command = 'cat /sys/class/power_supply/*/voltage_min_design'
   1729         result = self.run(command, ignore_status=True)
   1730         if result.exit_status != 0:
   1731             return 0
   1732         voltage = float(result.stdout.strip()) / battery_scale
   1733 
   1734         command = 'cat /sys/class/power_supply/*/charge_full_design'
   1735         result = self.run(command, ignore_status=True)
   1736         if result.exit_status != 0:
   1737             return 0
   1738         amphereHour = float(result.stdout.strip()) / battery_scale
   1739 
   1740         return voltage * amphereHour
   1741 
   1742 
   1743     def get_low_battery_shutdown_percent(self):
   1744         """Get the percent-based low-battery shutdown threshold.
   1745 
   1746         @returns a float representing low-battery shutdown percent, 0 if error.
   1747         """
   1748         ret = 0.0
   1749         try:
   1750             command = 'check_powerd_config --low_battery_shutdown_percent'
   1751             ret = float(self.run(command).stdout)
   1752         except error.CmdError:
   1753             logging.debug("Can't run %s", command)
   1754         except ValueError:
   1755             logging.debug("Didn't get number from %s", command)
   1756 
   1757         return ret
   1758 
   1759 
   1760     def has_hammer(self):
   1761         """Check whether DUT has hammer device or not.
   1762 
   1763         @returns boolean whether device has hammer or not
   1764         """
   1765         command = 'grep Hammer /sys/bus/usb/devices/*/product'
   1766         return self.run(command, ignore_status=True).exit_status == 0
   1767 
   1768 
   1769     def is_chrome_switch_present(self, switch):
   1770         """Returns True if the specified switch was provided to Chrome.
   1771 
   1772         @param switch The chrome switch to search for.
   1773         """
   1774 
   1775         command = 'pgrep -x -f -c "/opt/google/chrome/chrome.*%s.*"' % switch
   1776         return self.run(command, ignore_status=True).exit_status == 0
   1777 
   1778 
   1779     def oobe_triggers_update(self):
   1780         """Returns True if this host has an OOBE flow during which
   1781         it will perform an update check and perhaps an update.
   1782         One example of such a flow is Hands-Off Zero-Touch Enrollment.
   1783         As more such flows are developed, code handling them needs
   1784         to be added here.
   1785 
   1786         @return Boolean indicating whether this host's OOBE triggers an update.
   1787         """
   1788         return self.is_chrome_switch_present(
   1789             '--enterprise-enable-zero-touch-enrollment=hands-off')
   1790 
   1791 
   1792     # TODO(kevcheng): change this to just return the board without the
   1793     # 'board:' prefix and fix up all the callers.  Also look into removing the
   1794     # need for this method.
   1795     def get_board(self):
   1796         """Determine the correct board label for this host.
   1797 
   1798         @returns a string representing this host's board.
   1799         """
   1800         release_info = utils.parse_cmd_output('cat /etc/lsb-release',
   1801                                               run_method=self.run)
   1802         return (ds_constants.BOARD_PREFIX +
   1803                 release_info['CHROMEOS_RELEASE_BOARD'])
   1804 
   1805     def get_channel(self):
   1806         """Determine the correct channel label for this host.
   1807 
   1808         @returns: a string represeting this host's build channel.
   1809                   (stable, dev, beta). None on fail.
   1810         """
   1811         return lsbrelease_utils.get_chromeos_channel(
   1812                 lsb_release_content=self._get_lsb_release_content())
   1813 
   1814     def get_power_supply(self):
   1815         """
   1816         Determine what type of power supply the host has
   1817 
   1818         @returns a string representing this host's power supply.
   1819                  'power:battery' when the device has a battery intended for
   1820                         extended use
   1821                  'power:AC_primary' when the device has a battery not intended
   1822                         for extended use (for moving the machine, etc)
   1823                  'power:AC_only' when the device has no battery at all.
   1824         """
   1825         psu = self.run(command='mosys psu type', ignore_status=True)
   1826         if psu.exit_status:
   1827             # The psu command for mosys is not included for all platforms. The
   1828             # assumption is that the device will have a battery if the command
   1829             # is not found.
   1830             return 'power:battery'
   1831 
   1832         psu_str = psu.stdout.strip()
   1833         if psu_str == 'unknown':
   1834             return None
   1835 
   1836         return 'power:%s' % psu_str
   1837 
   1838 
   1839     def has_battery(self):
   1840         """Determine if DUT has a battery.
   1841 
   1842         Returns:
   1843             Boolean, False if known not to have battery, True otherwise.
   1844         """
   1845         rv = True
   1846         power_supply = self.get_power_supply()
   1847         if power_supply == 'power:battery':
   1848             _NO_BATTERY_BOARD_TYPE = ['CHROMEBOX', 'CHROMEBIT', 'CHROMEBASE']
   1849             board_type = self.get_board_type()
   1850             if board_type in _NO_BATTERY_BOARD_TYPE:
   1851                 logging.warn('Do NOT believe type %s has battery. '
   1852                              'See debug for mosys details', board_type)
   1853                 psu = self.system_output('mosys -vvvv psu type',
   1854                                          ignore_status=True)
   1855                 logging.debug(psu)
   1856                 rv = False
   1857         elif power_supply == 'power:AC_only':
   1858             rv = False
   1859 
   1860         return rv
   1861 
   1862 
   1863     def get_servo(self):
   1864         """Determine if the host has a servo attached.
   1865 
   1866         If the host has a working servo attached, it should have a servo label.
   1867 
   1868         @return: string 'servo' if the host has servo attached. Otherwise,
   1869                  returns None.
   1870         """
   1871         return 'servo' if self._servo_host else None
   1872 
   1873 
   1874     def has_internal_display(self):
   1875         """Determine if the device under test is equipped with an internal
   1876         display.
   1877 
   1878         @return: 'internal_display' if one is present; None otherwise.
   1879         """
   1880         from autotest_lib.client.cros.graphics import graphics_utils
   1881         from autotest_lib.client.common_lib import utils as common_utils
   1882 
   1883         def __system_output(cmd):
   1884             return self.run(cmd).stdout
   1885 
   1886         def __read_file(remote_path):
   1887             return self.run('cat %s' % remote_path).stdout
   1888 
   1889         # Hijack the necessary client functions so that we can take advantage
   1890         # of the client lib here.
   1891         # FIXME: find a less hacky way than this
   1892         original_system_output = utils.system_output
   1893         original_read_file = common_utils.read_file
   1894         utils.system_output = __system_output
   1895         common_utils.read_file = __read_file
   1896         try:
   1897             return ('internal_display' if graphics_utils.has_internal_display()
   1898                                    else None)
   1899         finally:
   1900             utils.system_output = original_system_output
   1901             common_utils.read_file = original_read_file
   1902 
   1903 
   1904     def is_boot_from_usb(self):
   1905         """Check if DUT is boot from USB.
   1906 
   1907         @return: True if DUT is boot from usb.
   1908         """
   1909         device = self.run('rootdev -s -d').stdout.strip()
   1910         removable = int(self.run('cat /sys/block/%s/removable' %
   1911                                  os.path.basename(device)).stdout.strip())
   1912         return removable == 1
   1913 
   1914 
   1915     def read_from_meminfo(self, key):
   1916         """Return the memory info from /proc/meminfo
   1917 
   1918         @param key: meminfo requested
   1919 
   1920         @return the memory value as a string
   1921 
   1922         """
   1923         meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip()
   1924         logging.debug('%s', meminfo)
   1925         return int(re.search(r'\d+', meminfo).group(0))
   1926 
   1927 
   1928     def get_cpu_arch(self):
   1929         """Returns CPU arch of the device.
   1930 
   1931         @return CPU architecture of the DUT.
   1932         """
   1933         # Add CPUs by following logic in client/bin/utils.py.
   1934         if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo",
   1935                 ignore_status=True).stdout:
   1936             return 'x86_64'
   1937         if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo",
   1938                 ignore_status=True).stdout:
   1939             return 'arm'
   1940         return 'i386'
   1941 
   1942 
   1943     def get_board_type(self):
   1944         """
   1945         Get the DUT's device type from /etc/lsb-release.
   1946         DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more.
   1947 
   1948         @return value of DEVICETYPE param from lsb-release.
   1949         """
   1950         device_type = self.run('grep DEVICETYPE /etc/lsb-release',
   1951                                ignore_status=True).stdout
   1952         if device_type:
   1953             return device_type.split('=')[-1].strip()
   1954         return ''
   1955 
   1956 
   1957     def get_arc_version(self):
   1958         """Return ARC version installed on the DUT.
   1959 
   1960         @returns ARC version as string if the CrOS build has ARC, else None.
   1961         """
   1962         arc_version = self.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release',
   1963                                ignore_status=True).stdout
   1964         if arc_version:
   1965             return arc_version.split('=')[-1].strip()
   1966         return None
   1967 
   1968 
   1969     def get_os_type(self):
   1970         return 'cros'
   1971 
   1972 
   1973     def get_labels(self):
   1974         """Return the detected labels on the host."""
   1975         return self.labels.get_labels(self)
   1976