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
5import functools
6import json
7import logging
8import os
9import re
10import time
11
12import common
13from autotest_lib.client.bin import utils
14from autotest_lib.client.common_lib import autotemp
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.common_lib import global_config
17from autotest_lib.client.common_lib import lsbrelease_utils
18from autotest_lib.client.common_lib.cros import autoupdater
19from autotest_lib.client.common_lib.cros import dev_server
20from autotest_lib.client.common_lib.cros.graphite import autotest_es
21from autotest_lib.client.common_lib.cros.graphite import autotest_stats
22from autotest_lib.client.cros import constants as client_constants
23from autotest_lib.client.cros import cros_ui
24from autotest_lib.client.cros.audio import cras_utils
25from autotest_lib.client.cros.input_playback import input_playback
26from autotest_lib.client.cros.video import constants as video_test_constants
27from autotest_lib.server import afe_utils
28from autotest_lib.server import autoserv_parser
29from autotest_lib.server import autotest
30from autotest_lib.server import constants
31from autotest_lib.server import crashcollect
32from autotest_lib.server import utils as server_utils
33from autotest_lib.server.cros import provision
34from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
35from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers
36from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig
37from autotest_lib.server.cros.servo import plankton
38from autotest_lib.server.hosts import abstract_ssh
39from autotest_lib.server.hosts import chameleon_host
40from autotest_lib.server.hosts import plankton_host
41from autotest_lib.server.hosts import servo_host
42from autotest_lib.site_utils.rpm_control_system import rpm_client
43
44
45CONFIG = global_config.global_config
46
47LUCID_SLEEP_BOARDS = ['samus', 'lulu']
48
49# A file to indicate provision failure and require Repair job to powerwash the
50# dut.
51PROVISION_FAILED = '/var/tmp/provision_failed'
52
53class FactoryImageCheckerException(error.AutoservError):
54    """Exception raised when an image is a factory image."""
55    pass
56
57
58class CrosHost(abstract_ssh.AbstractSSHHost):
59    """Chromium OS specific subclass of Host."""
60
61    VERSION_PREFIX = provision.CROS_VERSION_PREFIX
62
63    _parser = autoserv_parser.autoserv_parser
64    _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
65
66    # Timeout values (in seconds) associated with various Chrome OS
67    # state changes.
68    #
69    # In general, a good rule of thumb is that the timeout can be up
70    # to twice the typical measured value on the slowest platform.
71    # The times here have not necessarily been empirically tested to
72    # meet this criterion.
73    #
74    # SLEEP_TIMEOUT:  Time to allow for suspend to memory.
75    # RESUME_TIMEOUT: Time to allow for resume after suspend, plus
76    #   time to restart the netwowrk.
77    # SHUTDOWN_TIMEOUT: Time to allow for shut down.
78    # BOOT_TIMEOUT: Time to allow for boot from power off.  Among
79    #   other things, this must account for the 30 second dev-mode
80    #   screen delay, time to start the network on the DUT, and the
81    #   ssh timeout of 120 seconds.
82    # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device,
83    #   including the 30 second dev-mode delay and time to start the
84    #   network.
85    # INSTALL_TIMEOUT: Time to allow for chromeos-install.
86    # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that
87    #   includes powerwash.
88
89    SLEEP_TIMEOUT = 2
90    RESUME_TIMEOUT = 10
91    SHUTDOWN_TIMEOUT = 10
92    BOOT_TIMEOUT = 150
93    USB_BOOT_TIMEOUT = 300
94    INSTALL_TIMEOUT = 480
95    POWERWASH_BOOT_TIMEOUT = 60
96
97    # Minimum OS version that supports server side packaging. Older builds may
98    # not have server side package built or with Autotest code change to support
99    # server-side packaging.
100    MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
101            'AUTOSERV', 'min_version_support_ssp', type=int)
102
103    # REBOOT_TIMEOUT: How long to wait for a reboot.
104    #
105    # We have a long timeout to ensure we don't flakily fail due to other
106    # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
107    # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
108    # return from reboot' bug is solved.
109    REBOOT_TIMEOUT = 480
110
111    # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
112    # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
113    _USB_POWER_TIMEOUT = 5
114    _POWER_CYCLE_TIMEOUT = 10
115
116    _RPM_RECOVERY_BOARDS = CONFIG.get_config_value('CROS',
117            'rpm_recovery_boards', type=str).split(',')
118
119    _MAX_POWER_CYCLE_ATTEMPTS = 6
120    _LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
121    _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
122                           '-host(\d+)')
123    _LIGHTSENSOR_FILES = [ "in_illuminance0_input",
124                           "in_illuminance_input",
125                           "in_illuminance0_raw",
126                           "in_illuminance_raw",
127                           "illuminance0_input"]
128    _LIGHTSENSOR_SEARCH_DIR = '/sys/bus/iio/devices'
129    _LABEL_FUNCTIONS = []
130    _DETECTABLE_LABELS = []
131    label_decorator = functools.partial(server_utils.add_label_detector,
132                                        _LABEL_FUNCTIONS,
133                                        _DETECTABLE_LABELS)
134
135    # Constants used in ping_wait_up() and ping_wait_down().
136    #
137    # _PING_WAIT_COUNT is the approximate number of polling
138    # cycles to use when waiting for a host state change.
139    #
140    # _PING_STATUS_DOWN and _PING_STATUS_UP are names used
141    # for arguments to the internal _ping_wait_for_status()
142    # method.
143    _PING_WAIT_COUNT = 40
144    _PING_STATUS_DOWN = False
145    _PING_STATUS_UP = True
146
147    # Allowed values for the power_method argument.
148
149    # POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods.
150    # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
151    # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
152    POWER_CONTROL_RPM = 'RPM'
153    POWER_CONTROL_SERVO = 'servoj10'
154    POWER_CONTROL_MANUAL = 'manual'
155
156    POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
157                                POWER_CONTROL_SERVO,
158                                POWER_CONTROL_MANUAL)
159
160    _RPM_OUTLET_CHANGED = 'outlet_changed'
161
162    # URL pattern to download firmware image.
163    _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
164            'CROS', 'firmware_url_pattern', type=str)
165
166    # File that has a list of directories to be collected
167    _LOGS_TO_COLLECT_FILE = os.path.join(
168            common.client_dir, 'common_lib', 'logs_to_collect')
169
170    # Prefix of logging message w.r.t. crash collection
171    _CRASHLOGS_PREFIX = 'collect_crashlogs'
172
173    # Time duration waiting for host up/down check
174    _CHECK_HOST_UP_TIMEOUT_SECS = 15
175
176    # A command that interacts with kernel and hardware (e.g., rm, mkdir, etc)
177    # might not be completely done deep through the hardware when the machine
178    # is powered down right after the command returns.
179    # We should wait for a few seconds to make them done. Finger crossed.
180    _SAFE_WAIT_SECS = 10
181
182
183    @staticmethod
184    def check_host(host, timeout=10):
185        """
186        Check if the given host is a chrome-os host.
187
188        @param host: An ssh host representing a device.
189        @param timeout: The timeout for the run command.
190
191        @return: True if the host device is chromeos.
192
193        """
194        try:
195            result = host.run(
196                    'grep -q CHROMEOS /etc/lsb-release && '
197                    '! test -f /mnt/stateful_partition/.android_tester && '
198                    '! grep -q moblab /etc/lsb-release',
199                    ignore_status=True, timeout=timeout)
200        except (error.AutoservRunError, error.AutoservSSHTimeout):
201            return False
202        return result.exit_status == 0
203
204
205    @staticmethod
206    def _extract_arguments(args_dict, key_subset):
207        """Extract options from `args_dict` and return a subset result.
208
209        Take the provided dictionary of argument options and return
210        a subset that represent standard arguments needed to construct
211        a test-assistant object (chameleon or servo) for a host. The
212        intent is to provide standard argument processing from
213        CrosHost for tests that require a test-assistant board
214        to operate.
215
216        @param args_dict Dictionary from which to extract the arguments.
217        @param key_subset Tuple of keys to extract from the args_dict, e.g.
218          ('servo_host', 'servo_port').
219        """
220        result = {}
221        for arg in key_subset:
222            if arg in args_dict:
223                result[arg] = args_dict[arg]
224        return result
225
226
227    @staticmethod
228    def get_chameleon_arguments(args_dict):
229        """Extract chameleon options from `args_dict` and return the result.
230
231        Recommended usage:
232        ~~~~~~~~
233            args_dict = utils.args_to_dict(args)
234            chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
235            host = hosts.create_host(machine, chameleon_args=chameleon_args)
236        ~~~~~~~~
237
238        @param args_dict Dictionary from which to extract the chameleon
239          arguments.
240        """
241        return CrosHost._extract_arguments(
242                args_dict, ('chameleon_host', 'chameleon_port'))
243
244
245    @staticmethod
246    def get_plankton_arguments(args_dict):
247        """Extract chameleon options from `args_dict` and return the result.
248
249        Recommended usage:
250        ~~~~~~~~
251            args_dict = utils.args_to_dict(args)
252            plankon_args = hosts.CrosHost.get_plankton_arguments(args_dict)
253            host = hosts.create_host(machine, plankton_args=polankton_args)
254        ~~~~~~~~
255
256        @param args_dict Dictionary from which to extract the plankton
257          arguments.
258        """
259        args = CrosHost._extract_arguments(
260                args_dict, ('plankton_host', 'plankton_port'))
261        return args
262
263
264    @staticmethod
265    def get_servo_arguments(args_dict):
266        """Extract servo options from `args_dict` and return the result.
267
268        Recommended usage:
269        ~~~~~~~~
270            args_dict = utils.args_to_dict(args)
271            servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
272            host = hosts.create_host(machine, servo_args=servo_args)
273        ~~~~~~~~
274
275        @param args_dict Dictionary from which to extract the servo
276          arguments.
277        """
278        return CrosHost._extract_arguments(
279                args_dict, ('servo_host', 'servo_port'))
280
281
282    def _initialize(self, hostname, chameleon_args=None, servo_args=None, plankton_args=None,
283                    try_lab_servo=False, ssh_verbosity_flag='', ssh_options='',
284                    *args, **dargs):
285        """Initialize superclasses, |self.chameleon|, and |self.servo|.
286
287        This method will attempt to create the test-assistant object
288        (chameleon/servo) when it is needed by the test. Check
289        the docstring of chameleon_host.create_chameleon_host and
290        servo_host.create_servo_host for how this is determined.
291
292        @param hostname: Hostname of the dut.
293        @param chameleon_args: A dictionary that contains args for creating
294                               a ChameleonHost. See chameleon_host for details.
295        @param servo_args: A dictionary that contains args for creating
296                           a ServoHost object. See servo_host for details.
297        @param try_lab_servo: Boolean, False indicates that ServoHost should
298                              not be created for a device in Cros test lab.
299                              See servo_host for details.
300        @param ssh_verbosity_flag: String, to pass to the ssh command to control
301                                   verbosity.
302        @param ssh_options: String, other ssh options to pass to the ssh
303                            command.
304        """
305        super(CrosHost, self)._initialize(hostname=hostname,
306                                          *args, **dargs)
307        # self.env is a dictionary of environment variable settings
308        # to be exported for commands run on the host.
309        # LIBC_FATAL_STDERR_ can be useful for diagnosing certain
310        # errors that might happen.
311        self.env['LIBC_FATAL_STDERR_'] = '1'
312        self._ssh_verbosity_flag = ssh_verbosity_flag
313        self._ssh_options = ssh_options
314        # TODO(fdeng): We need to simplify the
315        # process of servo and servo_host initialization.
316        # crbug.com/298432
317        self._servo_host =  servo_host.create_servo_host(
318                dut=self.hostname, servo_args=servo_args,
319                try_lab_servo=try_lab_servo)
320        # TODO(waihong): Do the simplication on Chameleon too.
321        self._chameleon_host = chameleon_host.create_chameleon_host(
322                dut=self.hostname, chameleon_args=chameleon_args)
323        # Add plankton host if plankton args were added on command line
324        self._plankton_host = plankton_host.create_plankton_host(plankton_args)
325
326        if self._servo_host is not None:
327            self.servo = self._servo_host.get_servo()
328        else:
329            self.servo = None
330
331        if self._chameleon_host:
332            self.chameleon = self._chameleon_host.create_chameleon_board()
333        else:
334            self.chameleon = None
335
336        if self._plankton_host:
337            self.plankton_servo = self._plankton_host.get_servo()
338            logging.info('plankton_servo: %r', self.plankton_servo)
339            # Create the plankton object used to access the ec uart
340            self.plankton = plankton.Plankton(self.plankton_servo,
341                    self._plankton_host.get_servod_server_proxy())
342        else:
343            self.plankton = None
344
345
346    def get_repair_image_name(self, image_type='cros'):
347        """Generate a image_name from variables in the global config.
348
349        image_type is used to differentiate different images. Default is CrOS,
350        in which case, repair image's name follows the naming convention defined
351        in global setting CROS/stable_build_pattern.
352        If the image_type is not `cros`, the repair image will be looked up
353        using key `board_name/image_type`, e.g., daisy_spring/firmware.
354
355        @param image_type: Type of the image. Default is `cros`.
356
357        @returns a str of $board-version/$BUILD. Returns None if stable version
358                for the board and the default are both not set, e.g., stable
359                firmware version for a new board.
360
361        """
362        board = self._get_board_from_afe()
363        if board is None:
364            raise error.AutoservError('DUT has no board attribute, '
365                                      'cannot be repaired.')
366        if image_type != 'cros':
367            board = '%s/%s' % (board, image_type)
368        stable_version = afe_utils.get_stable_version(board=board)
369        if image_type == 'cros':
370            build_pattern = CONFIG.get_config_value(
371                    'CROS', 'stable_build_pattern')
372            stable_version = build_pattern % (board, stable_version)
373        elif image_type == 'firmware':
374            # If firmware stable version is not specified, `stable_version`
375            # from the RPC is the default stable version for CrOS image.
376            # firmware stable version must be from firmware branch, thus its
377            # value must be like board-firmware/R31-1234.0.0. Check if
378            # firmware exists in the stable version, if not, return None.
379            if 'firmware' not in stable_version:
380                return None
381        return stable_version
382
383
384    def lookup_job_repo_url(self):
385        """Looks up the job_repo_url for the host.
386
387        @returns job_repo_url from AFE or None if not found.
388
389        @raises KeyError if the host does not have a job_repo_url
390        """
391        hosts = self._AFE.get_hosts(hostname=self.hostname)
392        if hosts and ds_constants.JOB_REPO_URL in hosts[0].attributes:
393            return hosts[0].attributes[ds_constants.JOB_REPO_URL]
394        else:
395            return None
396
397
398    def clear_job_repo_url(self):
399        """Clear host attribute job_repo_url."""
400        if not afe_utils.host_in_lab(self):
401            return
402        self.update_job_repo_url(None, None)
403
404
405    def update_job_repo_url(self, devserver_url, image_name):
406        """
407        Updates the job_repo_url host attribute and asserts it's value.
408
409        @param devserver_url: The devserver to use in the job_repo_url.
410        @param image_name: The name of the image to use in the job_repo_url.
411
412        @raises AutoservError: If we failed to update the job_repo_url.
413        """
414        repo_url = None
415        if devserver_url and image_name:
416            repo_url = tools.get_package_url(devserver_url, image_name)
417        self._AFE.set_host_attribute(ds_constants.JOB_REPO_URL, repo_url,
418                                     hostname=self.hostname)
419        if self.lookup_job_repo_url() != repo_url:
420            raise error.AutoservError('Failed to update job_repo_url with %s, '
421                                      'host %s' % (repo_url, self.hostname))
422
423
424    def add_job_repo_url(self, image_name):
425        """Add cros_version labels and host attribute job_repo_url.
426
427        @param image_name: The name of the image e.g.
428                lumpy-release/R27-3837.0.0
429
430        """
431        if not afe_utils.host_in_lab(self):
432            return
433
434        devserver_url = dev_server.ImageServer.resolve(image_name,
435                                                       self.hostname).url()
436        self.update_job_repo_url(devserver_url, image_name)
437
438
439    def verify_job_repo_url(self, tag=''):
440        """
441        Make sure job_repo_url of this host is valid.
442
443        Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\
444        lumpy-release/R29-4279.0.0/autotest/packages" claims to have the
445        autotest package for lumpy-release/R29-4279.0.0. If this isn't the case,
446        download and extract it. If the devserver embedded in the url is
447        unresponsive, update the job_repo_url of the host after staging it on
448        another devserver.
449
450        @param job_repo_url: A url pointing to the devserver where the autotest
451            package for this build should be staged.
452        @param tag: The tag from the server job, in the format
453                    <job_id>-<user>/<hostname>, or <hostless> for a server job.
454
455        @raises DevServerException: If we could not resolve a devserver.
456        @raises AutoservError: If we're unable to save the new job_repo_url as
457            a result of choosing a new devserver because the old one failed to
458            respond to a health check.
459        @raises urllib2.URLError: If the devserver embedded in job_repo_url
460                                  doesn't respond within the timeout.
461        """
462        job_repo_url = self.lookup_job_repo_url()
463        if not job_repo_url:
464            logging.warning('No job repo url set on host %s', self.hostname)
465            return
466
467        logging.info('Verifying job repo url %s', job_repo_url)
468        devserver_url, image_name = tools.get_devserver_build_from_package_url(
469            job_repo_url)
470
471        ds = dev_server.ImageServer(devserver_url)
472
473        logging.info('Staging autotest artifacts for %s on devserver %s',
474            image_name, ds.url())
475
476        start_time = time.time()
477        ds.stage_artifacts(image_name, ['autotest_packages'])
478        stage_time = time.time() - start_time
479
480        # Record how much of the verification time comes from a devserver
481        # restage. If we're doing things right we should not see multiple
482        # devservers for a given board/build/branch path.
483        try:
484            board, build_type, branch = server_utils.ParseBuildName(
485                                                image_name)[:3]
486        except server_utils.ParseBuildNameException:
487            pass
488        else:
489            devserver = devserver_url[
490                devserver_url.find('/') + 2:devserver_url.rfind(':')]
491            stats_key = {
492                'board': board,
493                'build_type': build_type,
494                'branch': branch,
495                'devserver': devserver.replace('.', '_'),
496            }
497            autotest_stats.Gauge('verify_job_repo_url').send(
498                '%(board)s.%(build_type)s.%(branch)s.%(devserver)s' % stats_key,
499                stage_time)
500
501
502    def stage_server_side_package(self, image=None):
503        """Stage autotest server-side package on devserver.
504
505        @param image: Full path of an OS image to install or a build name.
506
507        @return: A url to the autotest server-side package.
508        """
509        if image:
510            image_name = tools.get_build_from_image(image)
511            if not image_name:
512                raise error.AutoservError(
513                        'Failed to parse build name from %s' % image)
514            ds = dev_server.ImageServer.resolve(image_name)
515        else:
516            job_repo_url = self.lookup_job_repo_url()
517            if job_repo_url:
518                devserver_url, image_name = (
519                    tools.get_devserver_build_from_package_url(job_repo_url))
520                ds = dev_server.ImageServer(devserver_url)
521            else:
522                labels = self._AFE.get_labels(
523                        name__startswith=ds_constants.VERSION_PREFIX,
524                        host__hostname=self.hostname)
525                if not labels:
526                    raise error.AutoservError(
527                            'Failed to stage server-side package. The host has '
528                            'no job_report_url attribute or version label.')
529                image_name = labels[0].name[len(ds_constants.VERSION_PREFIX):]
530                ds = dev_server.ImageServer.resolve(image_name)
531
532        # Get the OS version of the build, for any build older than
533        # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported.
534        match = re.match('.*/R\d+-(\d+)\.', image_name)
535        if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP:
536            logging.warn('Build %s is older than %s. Server side packaging is '
537                         'disabled.', image_name, self.MIN_VERSION_SUPPORT_SSP)
538            return None
539
540        ds.stage_artifacts(image_name, ['autotest_server_package'])
541        return '%s/static/%s/%s' % (ds.url(), image_name,
542                                    'autotest_server_package.tar.bz2')
543
544
545    def _try_stateful_update(self, update_url, force_update, updater):
546        """Try to use stateful update to initialize DUT.
547
548        When DUT is already running the same version that machine_install
549        tries to install, stateful update is a much faster way to clean up
550        the DUT for testing, compared to a full reimage. It is implemeted
551        by calling autoupdater.run_update, but skipping updating root, as
552        updating the kernel is time consuming and not necessary.
553
554        @param update_url: url of the image.
555        @param force_update: Set to True to update the image even if the DUT
556            is running the same version.
557        @param updater: ChromiumOSUpdater instance used to update the DUT.
558        @returns: True if the DUT was updated with stateful update.
559
560        """
561        # Stop service ap-update-manager to prevent rebooting during autoupdate.
562        # The service is used in jetstream boards, but not other CrOS devices.
563        self.run('sudo stop ap-update-manager', ignore_status=True)
564
565        # TODO(jrbarnette):  Yes, I hate this re.match() test case.
566        # It's better than the alternative:  see crbug.com/360944.
567        image_name = autoupdater.url_to_image_name(update_url)
568        release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$'
569        if not re.match(release_pattern, image_name):
570            return False
571        if not updater.check_version():
572            return False
573        if not force_update:
574            logging.info('Canceling stateful update because the new and '
575                         'old versions are the same.')
576            return False
577        # Following folders should be rebuilt after stateful update.
578        # A test file is used to confirm each folder gets rebuilt after
579        # the stateful update.
580        folders_to_check = ['/var', '/home', '/mnt/stateful_partition']
581        test_file = '.test_file_to_be_deleted'
582        for folder in folders_to_check:
583            touch_path = os.path.join(folder, test_file)
584            self.run('touch %s' % touch_path)
585
586        updater.run_update(update_root=False)
587
588        # Reboot to complete stateful update.
589        self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
590        check_file_cmd = 'test -f %s; echo $?'
591        for folder in folders_to_check:
592            test_file_path = os.path.join(folder, test_file)
593            result = self.run(check_file_cmd % test_file_path,
594                              ignore_status=True)
595            if result.exit_status == 1:
596                return False
597        return True
598
599
600    def _post_update_processing(self, updater, expected_kernel=None):
601        """After the DUT is updated, confirm machine_install succeeded.
602
603        @param updater: ChromiumOSUpdater instance used to update the DUT.
604        @param expected_kernel: kernel expected to be active after reboot,
605            or `None` to skip rollback checking.
606
607        """
608        # Touch the lab machine file to leave a marker that
609        # distinguishes this image from other test images.
610        # Afterwards, we must re-run the autoreboot script because
611        # it depends on the _LAB_MACHINE_FILE.
612        autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
613                          '( touch "$FILE" ; start autoreboot )')
614        self.run(autoreboot_cmd % self._LAB_MACHINE_FILE)
615        updater.verify_boot_expectations(
616                expected_kernel, rollback_message=
617                'Build %s failed to boot on %s; system rolled back to previous '
618                'build' % (updater.update_version, self.hostname))
619        # Check that we've got the build we meant to install.
620        if not updater.check_version_to_confirm_install():
621            raise autoupdater.ChromiumOSError(
622                'Failed to update %s to build %s; found build '
623                '%s instead' % (self.hostname,
624                                updater.update_version,
625                                self.get_release_version()))
626
627        logging.debug('Cleaning up old autotest directories.')
628        try:
629            installed_autodir = autotest.Autotest.get_installed_autodir(self)
630            self.run('rm -rf ' + installed_autodir)
631        except autotest.AutodirNotFoundError:
632            logging.debug('No autotest installed directory found.')
633
634
635    def _stage_image_for_update(self, image_name=None):
636        """Stage a build on a devserver and return the update_url and devserver.
637
638        @param image_name: a name like lumpy-release/R27-3837.0.0
639        @returns a tuple with an update URL like:
640            http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
641            and the devserver instance.
642        """
643        if not image_name:
644            image_name = self.get_repair_image_name()
645
646        logging.info('Staging build for AU: %s', image_name)
647        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
648        devserver.trigger_download(image_name, synchronous=False)
649        return (tools.image_url_pattern() % (devserver.url(), image_name),
650                devserver)
651
652
653    def stage_image_for_servo(self, image_name=None):
654        """Stage a build on a devserver and return the update_url.
655
656        @param image_name: a name like lumpy-release/R27-3837.0.0
657        @returns an update URL like:
658            http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
659        """
660        if not image_name:
661            image_name = self.get_repair_image_name()
662        logging.info('Staging build for servo install: %s', image_name)
663        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
664        devserver.stage_artifacts(image_name, ['test_image'])
665        return devserver.get_test_image_url(image_name)
666
667
668    def stage_factory_image_for_servo(self, image_name):
669        """Stage a build on a devserver and return the update_url.
670
671        @param image_name: a name like <baord>/4262.204.0
672
673        @return: An update URL, eg:
674            http://<devserver>/static/canary-channel/\
675            <board>/4262.204.0/factory_test/chromiumos_factory_image.bin
676
677        @raises: ValueError if the factory artifact name is missing from
678                 the config.
679
680        """
681        if not image_name:
682            logging.error('Need an image_name to stage a factory image.')
683            return
684
685        factory_artifact = CONFIG.get_config_value(
686                'CROS', 'factory_artifact', type=str, default='')
687        if not factory_artifact:
688            raise ValueError('Cannot retrieve the factory artifact name from '
689                             'autotest config, and hence cannot stage factory '
690                             'artifacts.')
691
692        logging.info('Staging build for servo install: %s', image_name)
693        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
694        devserver.stage_artifacts(
695                image_name,
696                [factory_artifact],
697                archive_url=None)
698
699        return tools.factory_image_url_pattern() % (devserver.url(), image_name)
700
701
702    def machine_install(self, update_url=None, force_update=False,
703                        local_devserver=False, repair=False,
704                        force_full_update=False):
705        """Install the DUT.
706
707        Use stateful update if the DUT is already running the same build.
708        Stateful update does not update kernel and tends to run much faster
709        than a full reimage. If the DUT is running a different build, or it
710        failed to do a stateful update, full update, including kernel update,
711        will be applied to the DUT.
712
713        Once a host enters machine_install its host attribute job_repo_url
714        (used for package install) will be removed and then updated.
715
716        @param update_url: The url to use for the update
717                pattern: http://$devserver:###/update/$build
718                If update_url is None and repair is True we will install the
719                stable image listed in afe_stable_versions table. If the table
720                is not setup, global_config value under CROS.stable_cros_version
721                will be used instead.
722        @param force_update: Force an update even if the version installed
723                is the same. Default:False
724        @param local_devserver: Used by test_that to allow people to
725                use their local devserver. Default: False
726        @param repair: Forces update to repair image. Implies force_update.
727        @param force_full_update: If True, do not attempt to run stateful
728                update, force a full reimage. If False, try stateful update
729                first when the dut is already installed with the same version.
730        @raises autoupdater.ChromiumOSError
731
732        @returns Name of the image installed.
733        """
734        devserver = None
735        if repair:
736            update_url, devserver = self._stage_image_for_update()
737            force_update = True
738
739        if not update_url and not self._parser.options.image:
740            raise error.AutoservError(
741                    'There is no update URL, nor a method to get one.')
742
743        if not update_url and self._parser.options.image:
744            # This is the base case where we have no given update URL i.e.
745            # dynamic suites logic etc. This is the most flexible case where we
746            # can serve an update from any of our fleet of devservers.
747            requested_build = self._parser.options.image
748            if not requested_build.startswith('http://'):
749                logging.debug('Update will be staged for this installation')
750                update_url, devserver = self._stage_image_for_update(
751                        requested_build)
752            else:
753                update_url = requested_build
754
755        logging.debug('Update URL is %s', update_url)
756
757        # Remove cros-version and job_repo_url host attribute from host.
758        self.clear_job_repo_url()
759
760        # Create a file to indicate if provision fails. The file will be removed
761        # by stateful update or full install.
762        self.run('touch %s' % PROVISION_FAILED)
763
764        update_complete = False
765        updater = autoupdater.ChromiumOSUpdater(
766                 update_url, host=self, local_devserver=local_devserver)
767        if not force_full_update:
768            try:
769                # If the DUT is already running the same build, try stateful
770                # update first as it's much quicker than a full re-image.
771                update_complete = self._try_stateful_update(
772                        update_url, force_update, updater)
773            except Exception as e:
774                logging.exception(e)
775
776        inactive_kernel = None
777        if update_complete or (not force_update and updater.check_version()):
778            logging.info('Install complete without full update')
779        else:
780            logging.info('DUT requires full update.')
781            self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
782            # Stop service ap-update-manager to prevent rebooting during
783            # autoupdate. The service is used in jetstream boards, but not other
784            # CrOS devices.
785            self.run('sudo stop ap-update-manager', ignore_status=True)
786
787            num_of_attempts = provision.FLAKY_DEVSERVER_ATTEMPTS
788
789            while num_of_attempts > 0:
790                num_of_attempts -= 1
791                try:
792                    updater.run_update()
793                except Exception:
794                    logging.warn('Autoupdate did not complete.')
795                    # Do additional check for the devserver health. Ideally,
796                    # the autoupdater.py could raise an exception when it
797                    # detected network flake but that would require
798                    # instrumenting the update engine and parsing it log.
799                    if (num_of_attempts <= 0 or
800                            devserver is None or
801                            dev_server.DevServer.devserver_healthy(
802                                    devserver.url())):
803                        raise
804
805                    logging.warn('Devserver looks unhealthy. Trying another')
806                    update_url, devserver = self._stage_image_for_update(
807                            requested_build)
808                    logging.debug('New Update URL is %s', update_url)
809                    updater = autoupdater.ChromiumOSUpdater(
810                            update_url, host=self,
811                            local_devserver=local_devserver)
812                else:
813                    break
814
815            # Give it some time in case of IO issues.
816            time.sleep(10)
817
818            # Figure out active and inactive kernel.
819            active_kernel, inactive_kernel = updater.get_kernel_state()
820
821            # Ensure inactive kernel has higher priority than active.
822            if (updater.get_kernel_priority(inactive_kernel)
823                    < updater.get_kernel_priority(active_kernel)):
824                raise autoupdater.ChromiumOSError(
825                    'Update failed. The priority of the inactive kernel'
826                    ' partition is less than that of the active kernel'
827                    ' partition.')
828
829            # Updater has returned successfully; reboot the host.
830            self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
831
832        self._post_update_processing(updater, inactive_kernel)
833        image_name = autoupdater.url_to_image_name(update_url)
834        self.add_job_repo_url(image_name)
835        return image_name
836
837
838    def _clear_fw_version_labels(self, rw_only):
839        """Clear firmware version labels from the machine.
840
841        @param rw_only: True to only clear fwrw_version; otherewise, clear
842                        both fwro_version and fwrw_version.
843        """
844        labels = self._AFE.get_labels(
845                name__startswith=provision.FW_RW_VERSION_PREFIX,
846                host__hostname=self.hostname)
847        if not rw_only:
848            labels = labels + self._AFE.get_labels(
849                    name__startswith=provision.FW_RO_VERSION_PREFIX,
850                    host__hostname=self.hostname)
851        for label in labels:
852            label.remove_hosts(hosts=[self.hostname])
853
854
855    def _add_fw_version_label(self, build, rw_only):
856        """Add firmware version label to the machine.
857
858        @param build: Build of firmware.
859        @param rw_only: True to only add fwrw_version; otherwise, add both
860                        fwro_version and fwrw_version.
861
862        """
863        fw_label = provision.fwrw_version_to_label(build)
864        self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
865        if not rw_only:
866            fw_label = provision.fwro_version_to_label(build)
867            self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
868
869
870    def firmware_install(self, build=None, rw_only=False):
871        """Install firmware to the DUT.
872
873        Use stateful update if the DUT is already running the same build.
874        Stateful update does not update kernel and tends to run much faster
875        than a full reimage. If the DUT is running a different build, or it
876        failed to do a stateful update, full update, including kernel update,
877        will be applied to the DUT.
878
879        Once a host enters firmware_install its fw[ro|rw]_version label will
880        be removed. After the firmware is updated successfully, a new
881        fw[ro|rw]_version label will be added to the host.
882
883        @param build: The build version to which we want to provision the
884                      firmware of the machine,
885                      e.g. 'link-firmware/R22-2695.1.144'.
886        @param rw_only: True to only install firmware to its RW portions. Keep
887                        the RO portions unchanged.
888
889        TODO(dshi): After bug 381718 is fixed, update here with corresponding
890                    exceptions that could be raised.
891
892        """
893        if not self.servo:
894            raise error.TestError('Host %s does not have servo.' %
895                                  self.hostname)
896
897        # TODO(fdeng): use host.get_board() after
898        # crbug.com/271834 is fixed.
899        board = self._get_board_from_afe()
900
901        # If build is not set, try to install firmware from stable CrOS.
902        if not build:
903            build = self.get_repair_image_name(image_type='firmware')
904            if not build:
905                raise error.TestError(
906                        'Failed to find stable firmware build for %s.',
907                        self.hostname)
908            logging.info('Will install firmware from build %s.', build)
909
910        config = FAFTConfig(board)
911        if config.use_u_boot:
912            ap_image = 'image-%s.bin' % board
913        else: # Depthcharge platform
914            ap_image = 'image.bin'
915        ec_image = 'ec.bin'
916        ds = dev_server.ImageServer.resolve(build, self.hostname)
917        ds.stage_artifacts(build, ['firmware'])
918
919        tmpd = autotemp.tempdir(unique_id='fwimage')
920        try:
921            fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
922            local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl))
923            server_utils.system('wget -O %s %s' % (local_tarball, fwurl),
924                                timeout=60)
925            server_utils.system('tar xf %s -C %s %s %s' %
926                                (local_tarball, tmpd.name, ap_image, ec_image),
927                                timeout=60)
928            server_utils.system('tar xf %s  --wildcards -C %s "dts/*"' %
929                                (local_tarball, tmpd.name),
930                                timeout=60, ignore_status=True)
931
932            self._clear_fw_version_labels(rw_only)
933            logging.info('Will re-program EC %snow', 'RW ' if rw_only else '')
934            self.servo.program_ec(os.path.join(tmpd.name, ec_image), rw_only)
935            logging.info('Will re-program BIOS %snow', 'RW ' if rw_only else '')
936            self.servo.program_bios(os.path.join(tmpd.name, ap_image), rw_only)
937            self.servo.get_power_state_controller().reset()
938            time.sleep(self.servo.BOOT_DELAY)
939            self._add_fw_version_label(build, rw_only)
940        finally:
941            tmpd.clean()
942
943
944    def show_update_engine_log(self):
945        """Output update engine log."""
946        logging.debug('Dumping %s', client_constants.UPDATE_ENGINE_LOG)
947        self.run('cat %s' % client_constants.UPDATE_ENGINE_LOG)
948
949
950    def _get_board_from_afe(self):
951        """Retrieve this host's board from its labels in the AFE.
952
953        Looks for a host label of the form "board:<board>", and
954        returns the "<board>" part of the label.  `None` is returned
955        if there is not a single, unique label matching the pattern.
956
957        @returns board from label, or `None`.
958        """
959        return server_utils.get_board_from_afe(self.hostname, self._AFE)
960
961
962    def get_build(self):
963        """Retrieve the current build for this Host from the AFE.
964
965        Looks through this host's labels in the AFE to determine its build.
966        This method is replaced by afe_utils.get_build. It's kept here to
967        maintain backwards compatibility for test control files in older CrOS
968        builds (R48, R49 etc.) still call host.get_build, e.g.,
969        `provision_AutoUpdate.double`.
970        TODO(sbasi): Once R50 falls into release branch, this method can be
971        removed.
972
973        @returns The current build or None if it could not find it or if there
974                 were multiple build labels assigned to this host.
975        """
976        return afe_utils.get_build(self)
977
978
979    def _install_repair(self):
980        """Attempt to repair this host using the update-engine.
981
982        If the host is up, try installing the DUT with a stable
983        "repair" version of Chrome OS as defined in afe_stable_versions table.
984        If the table is not setup, global_config value under
985        CROS.stable_cros_version will be used instead.
986
987        @raises AutoservRepairMethodNA if the DUT is not reachable.
988        @raises ChromiumOSError if the install failed for some reason.
989
990        """
991        if not self.is_up():
992            raise error.AutoservRepairMethodNA('DUT unreachable for install.')
993        logging.info('Attempting to reimage machine to repair image.')
994        try:
995            afe_utils.machine_install_and_update_labels(self, repair=True)
996        except autoupdater.ChromiumOSError as e:
997            logging.exception(e)
998            logging.info('Repair via install failed.')
999            raise
1000
1001
1002    def _install_repair_with_powerwash(self):
1003        """Attempt to powerwash first then repair this host using update-engine.
1004
1005        update-engine may fail due to a bad image. In such case, powerwash
1006        may help to cleanup the DUT for update-engine to work again.
1007
1008        @raises AutoservRepairMethodNA if the DUT is not reachable.
1009        @raises ChromiumOSError if the install failed for some reason.
1010
1011        """
1012        if not self.is_up():
1013            raise error.AutoservRepairMethodNA('DUT unreachable for install.')
1014
1015        logging.info('Attempting to powerwash the DUT.')
1016        self.run('echo "fast safe" > '
1017                 '/mnt/stateful_partition/factory_install_reset')
1018        self.reboot(timeout=self.POWERWASH_BOOT_TIMEOUT, wait=True)
1019        if not self.is_up():
1020            logging.error('Powerwash failed. DUT did not come back after '
1021                          'reboot.')
1022            raise error.AutoservRepairFailure(
1023                    'DUT failed to boot from powerwash after %d seconds' %
1024                    self.POWERWASH_BOOT_TIMEOUT)
1025
1026        logging.info('Powerwash succeeded.')
1027        self._install_repair()
1028
1029
1030    def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT,
1031                      install_timeout=INSTALL_TIMEOUT):
1032        """
1033        Re-install the OS on the DUT by:
1034        1) installing a test image on a USB storage device attached to the Servo
1035                board,
1036        2) booting that image in recovery mode, and then
1037        3) installing the image with chromeos-install.
1038
1039        @param image_url: If specified use as the url to install on the DUT.
1040                otherwise boot the currently staged image on the USB stick.
1041        @param usb_boot_timeout: The usb_boot_timeout to use during reimage.
1042                Factory images need a longer usb_boot_timeout than regular
1043                cros images.
1044        @param install_timeout: The timeout to use when installing the chromeos
1045                image. Factory images need a longer install_timeout.
1046
1047        @raises AutoservError if the image fails to boot.
1048
1049        """
1050        usb_boot_timer_key = ('servo_install.usb_boot_timeout_%s'
1051                              % usb_boot_timeout)
1052        logging.info('Downloading image to USB, then booting from it. Usb boot '
1053                     'timeout = %s', usb_boot_timeout)
1054        timer = autotest_stats.Timer(usb_boot_timer_key)
1055        timer.start()
1056        self.servo.install_recovery_image(image_url)
1057        if not self.wait_up(timeout=usb_boot_timeout):
1058            raise error.AutoservRepairFailure(
1059                    'DUT failed to boot from USB after %d seconds' %
1060                    usb_boot_timeout)
1061        timer.stop()
1062
1063        # The new chromeos-tpm-recovery has been merged since R44-7073.0.0.
1064        # In old CrOS images, this command fails. Skip the error.
1065        logging.info('Resetting the TPM status')
1066        try:
1067            self.run('chromeos-tpm-recovery')
1068        except error.AutoservRunError:
1069            logging.warn('chromeos-tpm-recovery is too old.')
1070
1071
1072        install_timer_key = ('servo_install.install_timeout_%s'
1073                             % install_timeout)
1074        timer = autotest_stats.Timer(install_timer_key)
1075        timer.start()
1076        logging.info('Installing image through chromeos-install.')
1077        self.run('chromeos-install --yes', timeout=install_timeout)
1078        self.halt()
1079        timer.stop()
1080
1081        logging.info('Power cycling DUT through servo.')
1082        self.servo.get_power_state_controller().power_off()
1083        self.servo.switch_usbkey('off')
1084        # N.B. The Servo API requires that we use power_on() here
1085        # for two reasons:
1086        #  1) After turning on a DUT in recovery mode, you must turn
1087        #     it off and then on with power_on() once more to
1088        #     disable recovery mode (this is a Parrot specific
1089        #     requirement).
1090        #  2) After power_off(), the only way to turn on is with
1091        #     power_on() (this is a Storm specific requirement).
1092        self.servo.get_power_state_controller().power_on()
1093
1094        logging.info('Waiting for DUT to come back up.')
1095        if not self.wait_up(timeout=self.BOOT_TIMEOUT):
1096            raise error.AutoservError('DUT failed to reboot installed '
1097                                      'test image after %d seconds' %
1098                                      self.BOOT_TIMEOUT)
1099
1100
1101    def _setup_servo(self):
1102        """Try to force to create servo object if it's not set up yet.
1103        """
1104        if self.servo:
1105            return
1106
1107        try:
1108            # Setting servo_args to {} will force it to create the servo_host
1109            # object if possible.
1110            self._servo_host = servo_host.create_servo_host(
1111                    dut=self.hostname, servo_args={})
1112            if self._servo_host:
1113                self.servo = self._servo_host.get_servo()
1114            else:
1115                logging.error('Failed to create servo_host object.')
1116        except Exception as e:
1117            logging.error('Failed to create servo object: %s', e)
1118
1119
1120    def _servo_repair_reinstall(self):
1121        """Reinstall the DUT utilizing servo and a test image.
1122
1123        Re-install the OS on the DUT by:
1124        1) installing a test image on a USB storage device attached to the Servo
1125                board,
1126        2) booting that image in recovery mode,
1127        3) resetting the TPM status, and then
1128        4) installing the image with chromeos-install.
1129
1130        @raises AutoservRepairMethodNA if the device does not have servo
1131                support.
1132
1133        """
1134        if not self.servo:
1135            raise error.AutoservRepairMethodNA('Repair Reinstall NA: '
1136                                               'DUT has no servo support.')
1137
1138        logging.info('Attempting to recovery servo enabled device with '
1139                     'servo_repair_reinstall')
1140
1141        image_url = self.stage_image_for_servo()
1142        self.servo_install(image_url)
1143
1144
1145    def _is_firmware_repair_supported(self):
1146        """Check if the firmware repair is supported.
1147
1148        The firmware repair is only applicable to DUTs in pools listed in
1149        global config CROS/pools_support_firmware_repair.
1150
1151        @return: True if it is supported; otherwise False.
1152        """
1153        logging.info('Checking if host %s can be repaired with firmware '
1154                     'repair.', self.hostname)
1155        pools = server_utils.get_labels_from_afe(self.hostname, 'pool:',
1156                                                 self._AFE)
1157        pools_support_firmware_repair = CONFIG.get_config_value('CROS',
1158                'pools_support_firmware_repair', type=str).split(',')
1159
1160        return (pools and pools_support_firmware_repair and
1161                set(pools).intersection(set(pools_support_firmware_repair)))
1162
1163
1164    def _firmware_repair(self):
1165        """Reinstall the firmware image using servo.
1166
1167        This repair function attempts to install the stable firmware specified
1168        by the stable firmware version.
1169        Then reset the DUT and try to verify it. If verify fails, it will try to
1170        install the CrOS image using servo.
1171        """
1172        if not self._is_firmware_repair_supported():
1173            logging.info('Host is not in pools that support firmware repair.')
1174            raise error.AutoservRepairMethodNA(
1175                    'Firmware repair is not applicable to host %s.' %
1176                    self.hostname)
1177
1178        # To repair a DUT connected to a moblab, try to create a servo object if
1179        # it was failed to be created earlier as there may be a servo_host host
1180        # attribute for this host.
1181        if utils.is_moblab():
1182            self._setup_servo()
1183
1184        if not self.servo:
1185            raise error.AutoservRepairMethodNA('Repair Reinstall NA: '
1186                                               'DUT has no servo support.')
1187
1188        logging.info('Attempting to recovery servo enabled device with '
1189                     'firmware_repair.')
1190        self.firmware_install()
1191
1192        logging.info('Firmware repaired. Check if the DUT can boot. If not, '
1193                     'reinstall the CrOS using servo.')
1194        try:
1195            self.verify()
1196        except Exception as e:
1197            logging.warn('Failed to verify DUT, error: %s. Will try to repair '
1198                         'the DUT with servo_repair_reinstall.', e)
1199            self._servo_repair_reinstall()
1200
1201
1202    def _servo_repair_power(self):
1203        """Attempt to repair DUT using an attached Servo.
1204
1205        Attempt to power cycle the DUT via cold_reset.
1206
1207        @raises AutoservRepairMethodNA if the device does not have servo
1208                support.
1209        @raises AutoservRepairFailure if the repair fails for any reason.
1210        """
1211        if not self.servo:
1212            raise error.AutoservRepairMethodNA('Repair Power NA: '
1213                                               'DUT has no servo support.')
1214
1215        logging.info('Attempting to recover servo enabled device by '
1216                     'powering cycling with cold reset.')
1217        self.servo.get_power_state_controller().reset()
1218        if self.wait_up(self.BOOT_TIMEOUT):
1219            return
1220
1221        raise error.AutoservRepairFailure('DUT did not boot after long_press.')
1222
1223
1224    def _powercycle_to_repair(self):
1225        """Utilize the RPM Infrastructure to bring the host back up.
1226
1227        If the host is not up/repaired after the first powercycle we utilize
1228        auto fallback to the last good install by powercycling and rebooting the
1229        host 6 times.
1230
1231        @raises AutoservRepairMethodNA if the device does not support remote
1232                power.
1233        @raises AutoservRepairFailure if the repair fails for any reason.
1234
1235        """
1236        if not self.has_power():
1237            raise error.AutoservRepairMethodNA('Device does not support power.')
1238
1239        logging.info('Attempting repair via RPM powercycle.')
1240        failed_cycles = 0
1241        self.power_cycle()
1242        while not self.wait_up(timeout=self.BOOT_TIMEOUT):
1243            failed_cycles += 1
1244            if failed_cycles >= self._MAX_POWER_CYCLE_ATTEMPTS:
1245                raise error.AutoservRepairFailure(
1246                        'Powercycled host %s %d times; device did not come back'
1247                        ' online.' % (self.hostname, failed_cycles))
1248            self.power_cycle()
1249        if failed_cycles == 0:
1250            logging.info('Powercycling was successful first time.')
1251        else:
1252            logging.info('Powercycling was successful after %d failures.',
1253                         failed_cycles)
1254
1255
1256    def _reboot_repair(self):
1257        """SSH to this host and reboot."""
1258        if not self.is_up(self._CHECK_HOST_UP_TIMEOUT_SECS):
1259            raise error.AutoservRepairMethodNA('DUT unreachable for reboot.')
1260        logging.info('Attempting repair via SSH reboot.')
1261        self.reboot(timeout=self.BOOT_TIMEOUT, wait=True)
1262
1263
1264    def check_device(self):
1265        """Check if a device is ssh-able, and if so, clean and verify it.
1266
1267        @raise AutoservSSHTimeout: If the ssh ping times out.
1268        @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
1269                                                 permissions.
1270        @raise AutoservSshPingHostError: For other AutoservRunErrors during
1271                                         ssh_ping.
1272        @raises AutoservError: As appropriate, during cleanup and verify.
1273        """
1274        self.ssh_ping()
1275        self.cleanup()
1276        self.verify()
1277
1278
1279    def confirm_servo(self):
1280        """Confirm servo is initialized and verified.
1281
1282        @raise AutoservError: If servo is not initialized and verified.
1283        """
1284        if self.servo and self._servo_host.required_by_test:
1285            return
1286
1287        # Force to re-create the servo object to make sure servo is verified.
1288        logging.debug('Rebuilding the servo object.')
1289        self.servo = None
1290        self._servo_host = None
1291        self._setup_servo()
1292        if not self.servo:
1293            raise error.AutoservError('Failed to create servo object.')
1294
1295
1296    def _is_last_provision_failed(self):
1297        """Checks if the last provision job failed.
1298
1299        @return: True if there exists file /var/tmp/provision_failed, which
1300                 indicates the last provision job failed.
1301                 False if the file does not exist or the dut can't be reached.
1302        """
1303        try:
1304            result = self.run('test -f %s' % PROVISION_FAILED,
1305                              ignore_status=True, timeout=5)
1306            return result.exit_status == 0
1307        except (error.AutoservRunError, error.AutoservSSHTimeout):
1308            # Default to False, for repair to try all repair method if the dut
1309            # can't be reached.
1310            return False
1311
1312
1313    def repair(self):
1314        """Attempt to get the DUT to pass `self.verify()`.
1315
1316        This overrides the base class function for repair; it does
1317        not call back to the parent class, but instead offers a
1318        simplified implementation based on the capabilities in the
1319        Chrome OS test lab.
1320
1321        It first verifies and repairs servo if it is a DUT in CrOS
1322        lab and a servo is attached.
1323
1324        This escalates in order through the following procedures and verifies
1325        the status using `self.check_device()` after each of them. This is done
1326        until both the repair and the veryfing step succeed.
1327
1328        Escalation order of repair procedures from less intrusive to
1329        more intrusive repairs:
1330          1. SSH to the DUT and reboot.
1331          2. If there's a servo for the DUT, try to power the DUT off and
1332             on.
1333          3. If the DUT can be power-cycled via RPM, try to repair
1334             by power-cycling.
1335          4. Try to re-install to a known stable image using
1336             auto-update.
1337          5. If there's a servo for the DUT, try to re-install via
1338             the servo.
1339
1340        As with the parent method, the last operation performed on
1341        the DUT must be to call `self.check_device()`; If that call fails the
1342        exception it raises is passed back to the caller.
1343
1344        @raises AutoservRepairTotalFailure if the repair process fails to
1345                fix the DUT.
1346        @raises ServoHostRepairTotalFailure if the repair process fails to
1347                fix the servo host if one is attached to the DUT.
1348        @raises AutoservSshPermissionDeniedError if it is unable
1349                to ssh to the servo host due to permission error.
1350
1351        """
1352        # Caution: Deleting shards relies on repair to always reboot the DUT.
1353
1354        # To repair a DUT connected to a moblab, try to create a servo object if
1355        # it was failed to be created earlier as there may be a servo_host host
1356        # attribute for this host.
1357        if utils.is_moblab():
1358            self._setup_servo()
1359
1360        if self._servo_host and not self.servo:
1361            try:
1362                self._servo_host.repair()
1363            except Exception as e:
1364                logging.error('Could not create a healthy servo: %s', e)
1365            self.servo = self._servo_host.get_servo()
1366
1367        self.try_collect_crashlogs()
1368
1369        # TODO(scottz): This should use something similar to label_decorator,
1370        # but needs to be populated in order so DUTs are repaired with the
1371        # least amount of effort.
1372        force_powerwash = self._is_last_provision_failed()
1373        if force_powerwash:
1374            logging.info('Last provision failed, try powerwash first.')
1375            autotest_stats.Counter(
1376                    'repair_force_powerwash.TOTAL').increment()
1377            repair_funcs = [self._firmware_repair,
1378                            self._install_repair_with_powerwash,
1379                            self._servo_repair_reinstall]
1380        else:
1381            repair_funcs = [self._reboot_repair,
1382                            self._servo_repair_power,
1383                            self._firmware_repair,
1384                            self._powercycle_to_repair,
1385                            self._install_repair,
1386                            self._install_repair_with_powerwash,
1387                            self._servo_repair_reinstall]
1388        errors = []
1389        board = self._get_board_from_afe()
1390        for repair_func in repair_funcs:
1391            try:
1392                repair_func()
1393                self.try_collect_crashlogs()
1394                self.check_device()
1395                autotest_stats.Counter(
1396                        '%s.SUCCEEDED' % repair_func.__name__).increment()
1397                if board:
1398                    autotest_stats.Counter(
1399                            '%s.%s.SUCCEEDED' % (repair_func.__name__,
1400                                             board)).increment()
1401                if force_powerwash:
1402                    autotest_stats.Counter(
1403                            'repair_force_powerwash.SUCCEEDED').increment()
1404                return
1405            except error.AutoservRepairMethodNA as e:
1406                autotest_stats.Counter(
1407                        '%s.RepairNA' % repair_func.__name__).increment()
1408                if board:
1409                    autotest_stats.Counter(
1410                            '%s.%s.RepairNA' % (repair_func.__name__,
1411                                                board)).increment()
1412                logging.warning('Repair function NA: %s', e)
1413                errors.append(str(e))
1414            except Exception as e:
1415                autotest_stats.Counter(
1416                        '%s.FAILED' % repair_func.__name__).increment()
1417                if board:
1418                    autotest_stats.Counter(
1419                            '%s.%s.FAILED' % (repair_func.__name__,
1420                                              board)).increment()
1421                logging.warning('Failed to repair device: %s', e)
1422                errors.append(str(e))
1423
1424        if force_powerwash:
1425            autotest_stats.Counter(
1426                    'repair_force_powerwash.FAILED').increment()
1427        autotest_stats.Counter('Full_Repair_Failed').increment()
1428        if board:
1429            autotest_stats.Counter(
1430                    'Full_Repair_Failed.%s' % board).increment()
1431        raise error.AutoservRepairTotalFailure(
1432                'All attempts at repairing the device failed:\n%s' %
1433                '\n'.join(errors))
1434
1435
1436    def try_collect_crashlogs(self, check_host_up=True):
1437        """
1438        Check if a host is up and logs need to be collected from the host,
1439        if yes, collect them.
1440
1441        @param check_host_up: Flag for checking host is up. Default is True.
1442        """
1443        try:
1444            crash_job = self._need_crash_logs()
1445            if crash_job:
1446                logging.debug('%s: Job %s was crashed', self._CRASHLOGS_PREFIX,
1447                              crash_job)
1448                if not check_host_up or self.is_up(
1449                        self._CHECK_HOST_UP_TIMEOUT_SECS):
1450                    self._collect_crashlogs(crash_job)
1451                    logging.debug('%s: Completed collecting logs for the '
1452                                  'crashed job %s', self._CRASHLOGS_PREFIX,
1453                                  crash_job)
1454        except Exception as e:
1455            # Exception should not result in repair failure.
1456            # Therefore, suppress all exceptions here.
1457            logging.error('%s: Failed while trying to collect crash-logs: %s',
1458                          self._CRASHLOGS_PREFIX, e)
1459
1460
1461    def _need_crash_logs(self):
1462        """Get the value of need_crash_logs attribute of this host.
1463
1464        @return: Value string of need_crash_logs attribute
1465                 None if there is no need_crash_logs attribute
1466        """
1467        attrs = self._AFE.get_host_attribute(constants.CRASHLOGS_HOST_ATTRIBUTE,
1468                                             hostname=self.hostname)
1469        assert len(attrs) < 2
1470        return attrs[0].value if attrs else None
1471
1472
1473    def _collect_crashlogs(self, job_id):
1474        """Grab logs from the host where a job was crashed.
1475
1476        First, check if PRIOR_LOGS_DIR exists in the host.
1477        If yes, collect them.
1478        Otherwise, check if a lab-machine marker (_LAB_MACHINE_FILE) exists
1479        in the host.
1480        If yes, the host was repaired automatically, and we collect normal
1481        system logs.
1482
1483        @param job_id: Id of the job that was crashed.
1484        """
1485        crashlogs_dir = crashcollect.get_crashinfo_dir(self,
1486                constants.CRASHLOGS_DEST_DIR_PREFIX)
1487        flag_prior_logs = False
1488
1489        if self.path_exists(client_constants.PRIOR_LOGS_DIR):
1490            flag_prior_logs = True
1491            self._collect_prior_logs(crashlogs_dir)
1492        elif self.path_exists(self._LAB_MACHINE_FILE):
1493            self._collect_system_logs(crashlogs_dir)
1494        else:
1495            logging.warning('%s: Host was manually re-installed without '
1496                            '--lab_preserve_log option. Skip collecting '
1497                            'crash-logs.', self._CRASHLOGS_PREFIX)
1498
1499        # We make crash collection be one-time effort.
1500        # _collect_prior_logs() and _collect_system_logs() will not throw
1501        # any exception, and following codes will be executed even when
1502        # those methods fail.
1503        # _collect_crashlogs() is called only when the host is up (refer
1504        # to try_collect_crashlogs()). We assume _collect_prior_logs() and
1505        # _collect_system_logs() fail rarely when the host is up.
1506        # In addition, it is not clear how many times we should try crash
1507        # collection again while not triggering next repair unnecessarily.
1508        # Threfore, we try crash collection one time.
1509
1510        # Create a marker file as soon as log collection is done.
1511        # Leave the job id to this marker for gs_offloader to consume.
1512        marker_file = os.path.join(crashlogs_dir, constants.CRASHLOGS_MARKER)
1513        with open(marker_file, 'a') as f:
1514            f.write('%s\n' % job_id)
1515
1516        # Remove need_crash_logs attribute
1517        logging.debug('%s: Remove attribute need_crash_logs from host %s',
1518                      self._CRASHLOGS_PREFIX, self.hostname)
1519        self._AFE.set_host_attribute(constants.CRASHLOGS_HOST_ATTRIBUTE,
1520                                     None, hostname=self.hostname)
1521
1522        if flag_prior_logs:
1523            logging.debug('%s: Remove %s from host %s', self._CRASHLOGS_PREFIX,
1524                          client_constants.PRIOR_LOGS_DIR, self.hostname)
1525            self.run('rm -rf %s; sync' % client_constants.PRIOR_LOGS_DIR)
1526            # Wait for a few seconds to make sure the prior command is
1527            # done deep through storage.
1528            time.sleep(self._SAFE_WAIT_SECS)
1529
1530
1531    def _collect_prior_logs(self, crashlogs_dir):
1532        """Grab prior logs that were stashed before re-installing a host.
1533
1534        @param crashlogs_dir: Directory path where crash-logs are stored.
1535        """
1536        logging.debug('%s: Found %s, collecting them...',
1537                      self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR)
1538        try:
1539            self.collect_logs(client_constants.PRIOR_LOGS_DIR,
1540                              crashlogs_dir, False)
1541            logging.debug('%s: %s is collected',
1542                          self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR)
1543        except Exception as e:
1544            logging.error('%s: Failed to collect %s: %s',
1545                          self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR,
1546                          e)
1547
1548
1549    def _collect_system_logs(self, crashlogs_dir):
1550        """Grab normal system logs from a host.
1551
1552        @param crashlogs_dir: Directory path where crash-logs are stored.
1553        """
1554        logging.debug('%s: Found %s, collecting system logs...',
1555                      self._CRASHLOGS_PREFIX, self._LAB_MACHINE_FILE)
1556        sources = server_utils.parse_simple_config(self._LOGS_TO_COLLECT_FILE)
1557        for src in sources:
1558            try:
1559                if self.path_exists(src):
1560                    logging.debug('%s: Collecting %s...',
1561                                  self._CRASHLOGS_PREFIX, src)
1562                    dest = server_utils.concat_path_except_last(
1563                            crashlogs_dir, src)
1564                    self.collect_logs(src, dest, False)
1565                    logging.debug('%s: %s is collected',
1566                                  self._CRASHLOGS_PREFIX, src)
1567            except Exception as e:
1568                logging.error('%s: Failed to collect %s: %s',
1569                              self._CRASHLOGS_PREFIX, src, e)
1570
1571
1572    def close(self):
1573        super(CrosHost, self).close()
1574
1575
1576    def get_power_supply_info(self):
1577        """Get the output of power_supply_info.
1578
1579        power_supply_info outputs the info of each power supply, e.g.,
1580        Device: Line Power
1581          online:                  no
1582          type:                    Mains
1583          voltage (V):             0
1584          current (A):             0
1585        Device: Battery
1586          state:                   Discharging
1587          percentage:              95.9276
1588          technology:              Li-ion
1589
1590        Above output shows two devices, Line Power and Battery, with details of
1591        each device listed. This function parses the output into a dictionary,
1592        with key being the device name, and value being a dictionary of details
1593        of the device info.
1594
1595        @return: The dictionary of power_supply_info, e.g.,
1596                 {'Line Power': {'online': 'yes', 'type': 'main'},
1597                  'Battery': {'vendor': 'xyz', 'percentage': '100'}}
1598        @raise error.AutoservRunError if power_supply_info tool is not found in
1599               the DUT. Caller should handle this error to avoid false failure
1600               on verification.
1601        """
1602        result = self.run('power_supply_info').stdout.strip()
1603        info = {}
1604        device_name = None
1605        device_info = {}
1606        for line in result.split('\n'):
1607            pair = [v.strip() for v in line.split(':')]
1608            if len(pair) != 2:
1609                continue
1610            if pair[0] == 'Device':
1611                if device_name:
1612                    info[device_name] = device_info
1613                device_name = pair[1]
1614                device_info = {}
1615            else:
1616                device_info[pair[0]] = pair[1]
1617        if device_name and not device_name in info:
1618            info[device_name] = device_info
1619        return info
1620
1621
1622    def get_battery_percentage(self):
1623        """Get the battery percentage.
1624
1625        @return: The percentage of battery level, value range from 0-100. Return
1626                 None if the battery info cannot be retrieved.
1627        """
1628        try:
1629            info = self.get_power_supply_info()
1630            logging.info(info)
1631            return float(info['Battery']['percentage'])
1632        except (KeyError, ValueError, error.AutoservRunError):
1633            return None
1634
1635
1636    def is_ac_connected(self):
1637        """Check if the dut has power adapter connected and charging.
1638
1639        @return: True if power adapter is connected and charging.
1640        """
1641        try:
1642            info = self.get_power_supply_info()
1643            return info['Line Power']['online'] == 'yes'
1644        except (KeyError, error.AutoservRunError):
1645            return None
1646
1647
1648    def _cleanup_poweron(self):
1649        """Special cleanup method to make sure hosts always get power back."""
1650        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
1651        hosts = afe.get_hosts(hostname=self.hostname)
1652        if not hosts or not (self._RPM_OUTLET_CHANGED in
1653                             hosts[0].attributes):
1654            return
1655        logging.debug('This host has recently interacted with the RPM'
1656                      ' Infrastructure. Ensuring power is on.')
1657        try:
1658            self.power_on()
1659            afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
1660                                   hostname=self.hostname)
1661        except rpm_client.RemotePowerException:
1662            logging.error('Failed to turn Power On for this host after '
1663                          'cleanup through the RPM Infrastructure.')
1664            autotest_es.post(
1665                    type_str='RPM_poweron_failure',
1666                    metadata={'hostname': self.hostname})
1667
1668            battery_percentage = self.get_battery_percentage()
1669            if battery_percentage and battery_percentage < 50:
1670                raise
1671            elif self.is_ac_connected():
1672                logging.info('The device has power adapter connected and '
1673                             'charging. No need to try to turn RPM on '
1674                             'again.')
1675                afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
1676                                       hostname=self.hostname)
1677            logging.info('Battery level is now at %s%%. The device may '
1678                         'still have enough power to run test, so no '
1679                         'exception will be raised.', battery_percentage)
1680
1681
1682    def _is_factory_image(self):
1683        """Checks if the image on the DUT is a factory image.
1684
1685        @return: True if the image on the DUT is a factory image.
1686                 False otherwise.
1687        """
1688        result = self.run('[ -f /root/.factory_test ]', ignore_status=True)
1689        return result.exit_status == 0
1690
1691
1692    def _restart_ui(self):
1693        """Restart the Chrome UI.
1694
1695        @raises: FactoryImageCheckerException for factory images, since
1696                 we cannot attempt to restart ui on them.
1697                 error.AutoservRunError for any other type of error that
1698                 occurs while restarting ui.
1699        """
1700        if self._is_factory_image():
1701            raise FactoryImageCheckerException('Cannot restart ui on factory '
1702                                               'images')
1703
1704        # TODO(jrbarnette):  The command to stop/start the ui job
1705        # should live inside cros_ui, too.  However that would seem
1706        # to imply interface changes to the existing start()/restart()
1707        # functions, which is a bridge too far (for now).
1708        prompt = cros_ui.get_chrome_session_ident(self)
1709        self.run('stop ui; start ui')
1710        cros_ui.wait_for_chrome_ready(prompt, self)
1711
1712
1713    def get_release_version(self):
1714        """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
1715
1716        @returns The version string in lsb-release, under attribute
1717                 CHROMEOS_RELEASE_VERSION.
1718        """
1719        lsb_release_content = self.run(
1720                    'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
1721        return lsbrelease_utils.get_chromeos_release_version(
1722                    lsb_release_content=lsb_release_content)
1723
1724
1725    def verify_cros_version_label(self):
1726        """ Make sure host's cros-version label match the actual image in dut.
1727
1728        Remove any cros-version: label that doesn't match that installed in
1729        the dut.
1730
1731        @param raise_error: Set to True to raise exception if any mismatch found
1732
1733        @raise error.AutoservError: If any mismatch between cros-version label
1734                                    and the build installed in dut is found.
1735        """
1736        labels = self._AFE.get_labels(
1737                name__startswith=ds_constants.VERSION_PREFIX,
1738                host__hostname=self.hostname)
1739        mismatch_found = False
1740        if labels:
1741            # Get CHROMEOS_RELEASE_VERSION from lsb-release, e.g., 6908.0.0.
1742            # Note that it's different from cros-version label, which has
1743            # builder and branch info, e.g.,
1744            # cros-version:peppy-release/R43-6908.0.0
1745            release_version = self.get_release_version()
1746            host_list = [self.hostname]
1747            for label in labels:
1748                # Remove any cros-version label that does not match
1749                # release_version.
1750                build_version = label.name[len(ds_constants.VERSION_PREFIX):]
1751                if not utils.version_match(build_version, release_version):
1752                    logging.warn('cros-version label "%s" does not match '
1753                                 'release version %s. Removing the label.',
1754                                 label.name, release_version)
1755                    label.remove_hosts(hosts=host_list)
1756                    mismatch_found = True
1757        if mismatch_found:
1758            autotest_es.post(use_http=True,
1759                             type_str='cros_version_label_mismatch',
1760                             metadata={'hostname': self.hostname})
1761            raise error.AutoservError('The host has wrong cros-version label.')
1762
1763
1764    def verify_tpm_status(self):
1765        """ Verify the host's TPM is in a good state.
1766
1767        @raise error.AutoservError: If state is not good.
1768        """
1769        # This cryptohome command emits status information in JSON format. It
1770        # looks something like this:
1771        # {
1772        #    "installattrs": {
1773        #       "first_install": false,
1774        #       "initialized": true,
1775        #       "invalid": false,
1776        #       "lockbox_index": 536870916,
1777        #       "lockbox_nvram_version": 2,
1778        #       "secure": true,
1779        #       "size": 0,
1780        #       "version": 1
1781        #    },
1782        #    "mounts": [ {
1783        #       "enterprise": false,
1784        #       "keysets": [ {
1785        #          "current": true,
1786        #          "index": 0,
1787        #          "last_activity": 1330111359,
1788        #          "ok": true,
1789        #          "scrypt": true,
1790        #          "tpm": false
1791        #       } ],
1792        #       "mounted": true,
1793        #       "owner": "dbb3dd34edb181245130e136be51fa08478d3909"
1794        #    } ],
1795        #    "tpm": {
1796        #       "being_owned": false,
1797        #       "can_connect": true,
1798        #       "can_decrypt": false,
1799        #       "can_encrypt": false,
1800        #       "can_load_srk": true,
1801        #       "can_load_srk_pubkey": true,
1802        #       "enabled": true,
1803        #       "has_context": true,
1804        #       "has_cryptohome_key": false,
1805        #       "has_key_handle": false,
1806        #       "last_error": 0,
1807        #       "owned": true
1808        #    }
1809        # }
1810        output = self.run('cryptohome --action=status').stdout.strip()
1811        try:
1812            status = json.loads(output)
1813        except ValueError:
1814            logging.error('TPM_VERIFY: Cryptohome did not return valid status.')
1815            return
1816        try:
1817            tpm = status['tpm']
1818            if (not tpm['enabled'] or not tpm['can_connect'] or
1819                (tpm['owned'] and not tpm['can_load_srk']) or
1820                (tpm['can_load_srk'] and not tpm['can_load_srk_pubkey'])):
1821                logging.error('TPM_VERIFY: The host TPM is in a bad state.')
1822                raise error.AutoservError('The host TPM is in a bad state.')
1823            else:
1824                logging.debug('TPM_VERIFY: The host TPM is in a good state.')
1825        except KeyError:
1826            logging.error('TPM_VERIFY: Cryptohome did not return valid status.')
1827
1828
1829    def verify_firmware_status(self):
1830        """Verify the host's firmware is in a good state.
1831
1832        @raise error.AutoservError: If state is not good.
1833        """
1834        if self._is_firmware_repair_supported():
1835            try:
1836                # Read the AP firmware and dump the sections we are interested.
1837                cmd = ('mkdir /tmp/verify_firmware; '
1838                       'cd /tmp/verify_firmware; '
1839                       'for section in VBLOCK_A VBLOCK_B FW_MAIN_A FW_MAIN_B; '
1840                       'do flashrom -r image.bin -i $section:$section; '
1841                       'done')
1842                self.run(cmd)
1843
1844                # Verify the firmware blocks A and B.
1845                cmd = ('vbutil_firmware --verify /tmp/verify_firmware/VBLOCK_%c'
1846                       ' --signpubkey /usr/share/vboot/devkeys/root_key.vbpubk'
1847                       ' --fv /tmp/verify_firmware/FW_MAIN_%c')
1848                for c in ('A', 'B'):
1849                    rv = self.run(cmd % (c, c), ignore_status=True)
1850                    if rv.exit_status:
1851                       raise error.AutoservError(
1852                               'Firmware %c is in a bad state.' % c)
1853            finally:
1854                # Remove the tempoary files.
1855                self.run('rm -rf /tmp/verify_firmware')
1856        else:
1857            logging.info('Do not care about firmware status when the host '
1858                         'is not in pools that support firmware repair.')
1859
1860
1861    def verify_filesystem_write_status(self):
1862        """Verify the DUT's filesystem is read/writable
1863
1864        @raise error.AutoservError: if filesystem is not writable.
1865        """
1866        # try to create & delete a file
1867        filename = "/mnt/stateful_partition/test.txt"
1868        cmd = 'touch %s && rm %s' % (filename, filename)
1869        rv = self.run(command=cmd, ignore_status=True)
1870
1871        if rv.exit_status == 1:
1872            raise error.AutoservError('DUT filesystem is read-only.')
1873
1874
1875    def cleanup(self):
1876        self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
1877        try:
1878            self._restart_ui()
1879        except (error.AutotestRunError, error.AutoservRunError,
1880                FactoryImageCheckerException):
1881            logging.warning('Unable to restart ui, rebooting device.')
1882            # Since restarting the UI fails fall back to normal Autotest
1883            # cleanup routines, i.e. reboot the machine.
1884            super(CrosHost, self).cleanup()
1885        # Check if the rpm outlet was manipulated.
1886        if self.has_power():
1887            self._cleanup_poweron()
1888        self.verify_cros_version_label()
1889
1890
1891    def reboot(self, **dargs):
1892        """
1893        This function reboots the site host. The more generic
1894        RemoteHost.reboot() performs sync and sleeps for 5
1895        seconds. This is not necessary for Chrome OS devices as the
1896        sync should be finished in a short time during the reboot
1897        command.
1898        """
1899        if 'reboot_cmd' not in dargs:
1900            reboot_timeout = dargs.get('reboot_timeout', 10)
1901            dargs['reboot_cmd'] = ('sleep 1; '
1902                                   'reboot & sleep %d; '
1903                                   'reboot -f' % reboot_timeout)
1904        # Enable fastsync to avoid running extra sync commands before reboot.
1905        if 'fastsync' not in dargs:
1906            dargs['fastsync'] = True
1907
1908        # For purposes of logging reboot times:
1909        # Get the board name i.e. 'daisy_spring'
1910        board_fullname = self.get_board()
1911
1912        # Strip the prefix and add it to dargs.
1913        dargs['board'] = board_fullname[board_fullname.find(':')+1:]
1914        super(CrosHost, self).reboot(**dargs)
1915
1916
1917    def suspend(self, **dargs):
1918        """
1919        This function suspends the site host.
1920        """
1921        suspend_time = dargs.get('suspend_time', 60)
1922        dargs['timeout'] = suspend_time
1923        if 'suspend_cmd' not in dargs:
1924            dargs['suspend_cmd'] = ' && '.join([
1925                'echo 0 > /sys/class/rtc/rtc0/wakealarm',
1926                'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
1927                'powerd_dbus_suspend --delay=0'])
1928        super(CrosHost, self).suspend(**dargs)
1929
1930
1931    def upstart_status(self, service_name):
1932        """Check the status of an upstart init script.
1933
1934        @param service_name: Service to look up.
1935
1936        @returns True if the service is running, False otherwise.
1937        """
1938        return self.run('status %s | grep start/running' %
1939                        service_name).stdout.strip() != ''
1940
1941
1942    def verify_software(self):
1943        """Verify working software on a Chrome OS system.
1944
1945        Tests for the following conditions:
1946         1. All conditions tested by the parent version of this
1947            function.
1948         2. Sufficient space in /mnt/stateful_partition.
1949         3. Sufficient space in /mnt/stateful_partition/encrypted.
1950         4. update_engine answers a simple status request over DBus.
1951
1952        """
1953        # Check if a job was crashed on this host.
1954        # If yes, avoid verification until crash-logs are collected.
1955        if self._need_crash_logs():
1956            raise error.AutoservCrashLogCollectRequired(
1957                    'Need to collect crash-logs before verification')
1958
1959        super(CrosHost, self).verify_software()
1960        default_kilo_inodes_required = CONFIG.get_config_value(
1961                'SERVER', 'kilo_inodes_required', type=int, default=100)
1962        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
1963        kilo_inodes_required = CONFIG.get_config_value(
1964                'SERVER', 'kilo_inodes_required_%s' % board,
1965                type=int, default=default_kilo_inodes_required)
1966        self.check_inodes('/mnt/stateful_partition', kilo_inodes_required)
1967        self.check_diskspace(
1968            '/mnt/stateful_partition',
1969            CONFIG.get_config_value(
1970                'SERVER', 'gb_diskspace_required', type=float,
1971                default=20.0))
1972        encrypted_stateful_path = '/mnt/stateful_partition/encrypted'
1973        # Not all targets build with encrypted stateful support.
1974        if self.path_exists(encrypted_stateful_path):
1975            self.check_diskspace(
1976                encrypted_stateful_path,
1977                CONFIG.get_config_value(
1978                    'SERVER', 'gb_encrypted_diskspace_required', type=float,
1979                    default=0.1))
1980
1981        if not self.upstart_status('system-services'):
1982            raise error.AutoservError('Chrome failed to reach login. '
1983                                      'System services not running.')
1984
1985        # Factory images don't run update engine,
1986        # goofy controls dbus on these DUTs.
1987        if not self._is_factory_image():
1988            self.run('update_engine_client --status')
1989        # Makes sure python is present, loads and can use built in functions.
1990        # We have seen cases where importing cPickle fails with undefined
1991        # symbols in cPickle.so.
1992        self.run('python -c "import cPickle"')
1993
1994        self.verify_cros_version_label()
1995
1996        self.verify_tpm_status()
1997
1998        self.verify_firmware_status()
1999
2000        self.verify_filesystem_write_status()
2001
2002
2003    def verify_hardware(self):
2004        """Verify hardware system of a Chrome OS system.
2005
2006        Check following hardware conditions:
2007        1. Battery level.
2008        2. Is power adapter connected.
2009        """
2010        logging.info('Battery percentage: %s', self.get_battery_percentage())
2011        if self.is_ac_connected() is None:
2012            logging.info('Can not determine if the device has power adapter '
2013                         'connected.')
2014        else:
2015            logging.info('Device %s power adapter connected and charging.',
2016                         'has' if self.is_ac_connected() else 'does not have')
2017
2018
2019    def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
2020                         connect_timeout=None, alive_interval=None):
2021        """Override default make_ssh_command to use options tuned for Chrome OS.
2022
2023        Tuning changes:
2024          - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
2025          connection failure.  Consistency with remote_access.sh.
2026
2027          - ServerAliveInterval=900; which causes SSH to ping connection every
2028          900 seconds. In conjunction with ServerAliveCountMax ensures
2029          that if the connection dies, Autotest will bail out.
2030          Originally tried 60 secs, but saw frequent job ABORTS where
2031          the test completed successfully. Later increased from 180 seconds to
2032          900 seconds to account for tests where the DUT is suspended for
2033          longer periods of time.
2034
2035          - ServerAliveCountMax=3; consistency with remote_access.sh.
2036
2037          - ConnectAttempts=4; reduce flakiness in connection errors;
2038          consistency with remote_access.sh.
2039
2040          - UserKnownHostsFile=/dev/null; we don't care about the keys.
2041          Host keys change with every new installation, don't waste
2042          memory/space saving them.
2043
2044          - SSH protocol forced to 2; needed for ServerAliveInterval.
2045
2046        @param user User name to use for the ssh connection.
2047        @param port Port on the target host to use for ssh connection.
2048        @param opts Additional options to the ssh command.
2049        @param hosts_file Ignored.
2050        @param connect_timeout Ignored.
2051        @param alive_interval Ignored.
2052        """
2053        base_command = ('/usr/bin/ssh -a -x %s %s %s'
2054                        ' -o StrictHostKeyChecking=no'
2055                        ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
2056                        ' -o ConnectTimeout=30 -o ServerAliveInterval=900'
2057                        ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
2058                        ' -o Protocol=2 -l %s -p %d')
2059        return base_command % (self._ssh_verbosity_flag, self._ssh_options,
2060                               opts, user, port)
2061    def syslog(self, message, tag='autotest'):
2062        """Logs a message to syslog on host.
2063
2064        @param message String message to log into syslog
2065        @param tag String tag prefix for syslog
2066
2067        """
2068        self.run('logger -t "%s" "%s"' % (tag, message))
2069
2070
2071    def _ping_check_status(self, status):
2072        """Ping the host once, and return whether it has a given status.
2073
2074        @param status Check the ping status against this value.
2075        @return True iff `status` and the result of ping are the same
2076                (i.e. both True or both False).
2077
2078        """
2079        ping_val = utils.ping(self.hostname, tries=1, deadline=1)
2080        return not (status ^ (ping_val == 0))
2081
2082    def _ping_wait_for_status(self, status, timeout):
2083        """Wait for the host to have a given status (UP or DOWN).
2084
2085        Status is checked by polling.  Polling will not last longer
2086        than the number of seconds in `timeout`.  The polling
2087        interval will be long enough that only approximately
2088        _PING_WAIT_COUNT polling cycles will be executed, subject
2089        to a maximum interval of about one minute.
2090
2091        @param status Waiting will stop immediately if `ping` of the
2092                      host returns this status.
2093        @param timeout Poll for at most this many seconds.
2094        @return True iff the host status from `ping` matched the
2095                requested status at the time of return.
2096
2097        """
2098        # _ping_check_status() takes about 1 second, hence the
2099        # "- 1" in the formula below.
2100        poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1
2101        end_time = time.time() + timeout
2102        while time.time() <= end_time:
2103            if self._ping_check_status(status):
2104                return True
2105            if poll_interval > 0:
2106                time.sleep(poll_interval)
2107
2108        # The last thing we did was sleep(poll_interval), so it may
2109        # have been too long since the last `ping`.  Check one more
2110        # time, just to be sure.
2111        return self._ping_check_status(status)
2112
2113    def ping_wait_up(self, timeout):
2114        """Wait for the host to respond to `ping`.
2115
2116        N.B.  This method is not a reliable substitute for
2117        `wait_up()`, because a host that responds to ping will not
2118        necessarily respond to ssh.  This method should only be used
2119        if the target DUT can be considered functional even if it
2120        can't be reached via ssh.
2121
2122        @param timeout Minimum time to allow before declaring the
2123                       host to be non-responsive.
2124        @return True iff the host answered to ping before the timeout.
2125
2126        """
2127        return self._ping_wait_for_status(self._PING_STATUS_UP, timeout)
2128
2129    def ping_wait_down(self, timeout):
2130        """Wait until the host no longer responds to `ping`.
2131
2132        This function can be used as a slightly faster version of
2133        `wait_down()`, by avoiding potentially long ssh timeouts.
2134
2135        @param timeout Minimum time to allow for the host to become
2136                       non-responsive.
2137        @return True iff the host quit answering ping before the
2138                timeout.
2139
2140        """
2141        return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout)
2142
2143    def test_wait_for_sleep(self, sleep_timeout=None):
2144        """Wait for the client to enter low-power sleep mode.
2145
2146        The test for "is asleep" can't distinguish a system that is
2147        powered off; to confirm that the unit was asleep, it is
2148        necessary to force resume, and then call
2149        `test_wait_for_resume()`.
2150
2151        This function is expected to be called from a test as part
2152        of a sequence like the following:
2153
2154        ~~~~~~~~
2155            boot_id = host.get_boot_id()
2156            # trigger sleep on the host
2157            host.test_wait_for_sleep()
2158            # trigger resume on the host
2159            host.test_wait_for_resume(boot_id)
2160        ~~~~~~~~
2161
2162        @param sleep_timeout time limit in seconds to allow the host sleep.
2163
2164        @exception TestFail The host did not go to sleep within
2165                            the allowed time.
2166        """
2167        if sleep_timeout is None:
2168            sleep_timeout = self.SLEEP_TIMEOUT
2169
2170        if not self.ping_wait_down(timeout=sleep_timeout):
2171            raise error.TestFail(
2172                'client failed to sleep after %d seconds' % sleep_timeout)
2173
2174
2175    def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
2176        """Wait for the client to resume from low-power sleep mode.
2177
2178        The `old_boot_id` parameter should be the value from
2179        `get_boot_id()` obtained prior to entering sleep mode.  A
2180        `TestFail` exception is raised if the boot id changes.
2181
2182        See @ref test_wait_for_sleep for more on this function's
2183        usage.
2184
2185        @param old_boot_id A boot id value obtained before the
2186                               target host went to sleep.
2187        @param resume_timeout time limit in seconds to allow the host up.
2188
2189        @exception TestFail The host did not respond within the
2190                            allowed time.
2191        @exception TestFail The host responded, but the boot id test
2192                            indicated a reboot rather than a sleep
2193                            cycle.
2194        """
2195        if resume_timeout is None:
2196            resume_timeout = self.RESUME_TIMEOUT
2197
2198        if not self.wait_up(timeout=resume_timeout):
2199            raise error.TestFail(
2200                'client failed to resume from sleep after %d seconds' %
2201                    resume_timeout)
2202        else:
2203            new_boot_id = self.get_boot_id()
2204            if new_boot_id != old_boot_id:
2205                logging.error('client rebooted (old boot %s, new boot %s)',
2206                              old_boot_id, new_boot_id)
2207                raise error.TestFail(
2208                    'client rebooted, but sleep was expected')
2209
2210
2211    def test_wait_for_shutdown(self, shutdown_timeout=None):
2212        """Wait for the client to shut down.
2213
2214        The test for "has shut down" can't distinguish a system that
2215        is merely asleep; to confirm that the unit was down, it is
2216        necessary to force boot, and then call test_wait_for_boot().
2217
2218        This function is expected to be called from a test as part
2219        of a sequence like the following:
2220
2221        ~~~~~~~~
2222            boot_id = host.get_boot_id()
2223            # trigger shutdown on the host
2224            host.test_wait_for_shutdown()
2225            # trigger boot on the host
2226            host.test_wait_for_boot(boot_id)
2227        ~~~~~~~~
2228
2229        @param shutdown_timeout time limit in seconds to allow the host down.
2230        @exception TestFail The host did not shut down within the
2231                            allowed time.
2232        """
2233        if shutdown_timeout is None:
2234            shutdown_timeout = self.SHUTDOWN_TIMEOUT
2235
2236        if not self.ping_wait_down(timeout=shutdown_timeout):
2237            raise error.TestFail(
2238                'client failed to shut down after %d seconds' %
2239                    shutdown_timeout)
2240
2241
2242    def test_wait_for_boot(self, old_boot_id=None):
2243        """Wait for the client to boot from cold power.
2244
2245        The `old_boot_id` parameter should be the value from
2246        `get_boot_id()` obtained prior to shutting down.  A
2247        `TestFail` exception is raised if the boot id does not
2248        change.  The boot id test is omitted if `old_boot_id` is not
2249        specified.
2250
2251        See @ref test_wait_for_shutdown for more on this function's
2252        usage.
2253
2254        @param old_boot_id A boot id value obtained before the
2255                               shut down.
2256
2257        @exception TestFail The host did not respond within the
2258                            allowed time.
2259        @exception TestFail The host responded, but the boot id test
2260                            indicated that there was no reboot.
2261        """
2262        if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
2263            raise error.TestFail(
2264                'client failed to reboot after %d seconds' %
2265                    self.REBOOT_TIMEOUT)
2266        elif old_boot_id:
2267            if self.get_boot_id() == old_boot_id:
2268                logging.error('client not rebooted (boot %s)',
2269                              old_boot_id)
2270                raise error.TestFail(
2271                    'client is back up, but did not reboot')
2272
2273
2274    @staticmethod
2275    def check_for_rpm_support(hostname):
2276        """For a given hostname, return whether or not it is powered by an RPM.
2277
2278        @param hostname: hostname to check for rpm support.
2279
2280        @return None if this host does not follows the defined naming format
2281                for RPM powered DUT's in the lab. If it does follow the format,
2282                it returns a regular expression MatchObject instead.
2283        """
2284        return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
2285
2286
2287    def has_power(self):
2288        """For this host, return whether or not it is powered by an RPM.
2289
2290        @return True if this host is in the CROS lab and follows the defined
2291                naming format.
2292        """
2293        return CrosHost.check_for_rpm_support(self.hostname)
2294
2295
2296    def _set_power(self, state, power_method):
2297        """Sets the power to the host via RPM, Servo or manual.
2298
2299        @param state Specifies which power state to set to DUT
2300        @param power_method Specifies which method of power control to
2301                            use. By default "RPM" will be used. Valid values
2302                            are the strings "RPM", "manual", "servoj10".
2303
2304        """
2305        ACCEPTABLE_STATES = ['ON', 'OFF']
2306
2307        if state.upper() not in ACCEPTABLE_STATES:
2308            raise error.TestError('State must be one of: %s.'
2309                                   % (ACCEPTABLE_STATES,))
2310
2311        if power_method == self.POWER_CONTROL_SERVO:
2312            logging.info('Setting servo port J10 to %s', state)
2313            self.servo.set('prtctl3_pwren', state.lower())
2314            time.sleep(self._USB_POWER_TIMEOUT)
2315        elif power_method == self.POWER_CONTROL_MANUAL:
2316            logging.info('You have %d seconds to set the AC power to %s.',
2317                         self._POWER_CYCLE_TIMEOUT, state)
2318            time.sleep(self._POWER_CYCLE_TIMEOUT)
2319        else:
2320            if not self.has_power():
2321                raise error.TestFail('DUT does not have RPM connected.')
2322            afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
2323            afe.set_host_attribute(self._RPM_OUTLET_CHANGED, True,
2324                                   hostname=self.hostname)
2325            rpm_client.set_power(self.hostname, state.upper(), timeout_mins=5)
2326
2327
2328    def power_off(self, power_method=POWER_CONTROL_RPM):
2329        """Turn off power to this host via RPM, Servo or manual.
2330
2331        @param power_method Specifies which method of power control to
2332                            use. By default "RPM" will be used. Valid values
2333                            are the strings "RPM", "manual", "servoj10".
2334
2335        """
2336        self._set_power('OFF', power_method)
2337
2338
2339    def power_on(self, power_method=POWER_CONTROL_RPM):
2340        """Turn on power to this host via RPM, Servo or manual.
2341
2342        @param power_method Specifies which method of power control to
2343                            use. By default "RPM" will be used. Valid values
2344                            are the strings "RPM", "manual", "servoj10".
2345
2346        """
2347        self._set_power('ON', power_method)
2348
2349
2350    def power_cycle(self, power_method=POWER_CONTROL_RPM):
2351        """Cycle power to this host by turning it OFF, then ON.
2352
2353        @param power_method Specifies which method of power control to
2354                            use. By default "RPM" will be used. Valid values
2355                            are the strings "RPM", "manual", "servoj10".
2356
2357        """
2358        if power_method in (self.POWER_CONTROL_SERVO,
2359                            self.POWER_CONTROL_MANUAL):
2360            self.power_off(power_method=power_method)
2361            time.sleep(self._POWER_CYCLE_TIMEOUT)
2362            self.power_on(power_method=power_method)
2363        else:
2364            rpm_client.set_power(self.hostname, 'CYCLE')
2365
2366
2367    def get_platform(self):
2368        """Determine the correct platform label for this host.
2369
2370        @returns a string representing this host's platform.
2371        """
2372        crossystem = utils.Crossystem(self)
2373        crossystem.init()
2374        # Extract fwid value and use the leading part as the platform id.
2375        # fwid generally follow the format of {platform}.{firmware version}
2376        # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z
2377        platform = crossystem.fwid().split('.')[0].lower()
2378        # Newer platforms start with 'Google_' while the older ones do not.
2379        return platform.replace('google_', '')
2380
2381
2382    def get_architecture(self):
2383        """Determine the correct architecture label for this host.
2384
2385        @returns a string representing this host's architecture.
2386        """
2387        crossystem = utils.Crossystem(self)
2388        crossystem.init()
2389        return crossystem.arch()
2390
2391
2392    def get_chrome_version(self):
2393        """Gets the Chrome version number and milestone as strings.
2394
2395        Invokes "chrome --version" to get the version number and milestone.
2396
2397        @return A tuple (chrome_ver, milestone) where "chrome_ver" is the
2398            current Chrome version number as a string (in the form "W.X.Y.Z")
2399            and "milestone" is the first component of the version number
2400            (the "W" from "W.X.Y.Z").  If the version number cannot be parsed
2401            in the "W.X.Y.Z" format, the "chrome_ver" will be the full output
2402            of "chrome --version" and the milestone will be the empty string.
2403
2404        """
2405        version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout
2406        return utils.parse_chrome_version(version_string)
2407
2408
2409    @label_decorator()
2410    def get_board(self):
2411        """Determine the correct board label for this host.
2412
2413        @returns a string representing this host's board.
2414        """
2415        release_info = utils.parse_cmd_output('cat /etc/lsb-release',
2416                                              run_method=self.run)
2417        return (ds_constants.BOARD_PREFIX +
2418                release_info['CHROMEOS_RELEASE_BOARD'])
2419
2420
2421    @label_decorator('lightsensor')
2422    def has_lightsensor(self):
2423        """Determine the correct board label for this host.
2424
2425        @returns the string 'lightsensor' if this host has a lightsensor or
2426                 None if it does not.
2427        """
2428        search_cmd = "find -L %s -maxdepth 4 | egrep '%s'" % (
2429            self._LIGHTSENSOR_SEARCH_DIR, '|'.join(self._LIGHTSENSOR_FILES))
2430        try:
2431            # Run the search cmd following the symlinks. Stderr_tee is set to
2432            # None as there can be a symlink loop, but the command will still
2433            # execute correctly with a few messages printed to stderr.
2434            self.run(search_cmd, stdout_tee=None, stderr_tee=None)
2435            return 'lightsensor'
2436        except error.AutoservRunError:
2437            # egrep exited with a return code of 1 meaning none of the possible
2438            # lightsensor files existed.
2439            return None
2440
2441
2442    @label_decorator('bluetooth')
2443    def has_bluetooth(self):
2444        """Determine the correct board label for this host.
2445
2446        @returns the string 'bluetooth' if this host has bluetooth or
2447                 None if it does not.
2448        """
2449        try:
2450            self.run('test -d /sys/class/bluetooth/hci0')
2451            # test exited with a return code of 0.
2452            return 'bluetooth'
2453        except error.AutoservRunError:
2454            # test exited with a return code 1 meaning the directory did not
2455            # exist.
2456            return None
2457
2458
2459    @label_decorator('ec')
2460    def get_ec(self):
2461        """
2462        Determine the type of EC on this host.
2463
2464        @returns a string representing this host's embedded controller type.
2465        At present, it only returns "ec:cros", for Chrome OS ECs. Other types
2466        of EC (or none) don't return any strings, since no tests depend on
2467        those.
2468        """
2469        cmd = 'mosys ec info'
2470        # The output should look like these, so that the last field should
2471        # match our EC version scheme:
2472        #
2473        #   stm | stm32f100 | snow_v1.3.139-375eb9f
2474        #   ti | Unknown-10de | peppy_v1.5.114-5d52788
2475        #
2476        # Non-Chrome OS ECs will look like these:
2477        #
2478        #   ENE | KB932 | 00BE107A00
2479        #   ite | it8518 | 3.08
2480        #
2481        # And some systems don't have ECs at all (Lumpy, for example).
2482        regexp = r'^.*\|\s*(\S+_v\d+\.\d+\.\d+-[0-9a-f]+)\s*$'
2483
2484        ecinfo = self.run(command=cmd, ignore_status=True)
2485        if ecinfo.exit_status == 0:
2486            res = re.search(regexp, ecinfo.stdout)
2487            if res:
2488                logging.info("EC version is %s", res.groups()[0])
2489                return 'ec:cros'
2490            logging.info("%s got: %s", cmd, ecinfo.stdout)
2491            # Has an EC, but it's not a Chrome OS EC
2492            return None
2493        logging.info("%s exited with status %d", cmd, ecinfo.exit_status)
2494        # No EC present
2495        return None
2496
2497
2498    @label_decorator('accels')
2499    def get_accels(self):
2500        """
2501        Determine the type of accelerometers on this host.
2502
2503        @returns a string representing this host's accelerometer type.
2504        At present, it only returns "accel:cros-ec", for accelerometers
2505        attached to a Chrome OS EC, or none, if no accelerometers.
2506        """
2507        # Check to make sure we have ectool
2508        rv = self.run('which ectool', ignore_status=True)
2509        if rv.exit_status:
2510            logging.info("No ectool cmd found, assuming no EC accelerometers")
2511            return None
2512
2513        # Check that the EC supports the motionsense command
2514        rv = self.run('ectool motionsense', ignore_status=True)
2515        if rv.exit_status:
2516            logging.info("EC does not support motionsense command "
2517                         "assuming no EC accelerometers")
2518            return None
2519
2520        # Check that EC motion sensors are active
2521        active = self.run('ectool motionsense active').stdout.split('\n')
2522        if active[0] == "0":
2523            logging.info("Motion sense inactive, assuming no EC accelerometers")
2524            return None
2525
2526        logging.info("EC accelerometers found")
2527        return 'accel:cros-ec'
2528
2529
2530    @label_decorator('chameleon')
2531    def has_chameleon(self):
2532        """Determine if a Chameleon connected to this host.
2533
2534        @returns a list containing two strings ('chameleon' and
2535                 'chameleon:' + label, e.g. 'chameleon:hdmi') if this host
2536                 has a Chameleon or None if it has not.
2537        """
2538        if self._chameleon_host:
2539            return ['chameleon', 'chameleon:' + self.chameleon.get_label()]
2540        else:
2541            return None
2542
2543
2544    @label_decorator('audio_loopback_dongle')
2545    def has_loopback_dongle(self):
2546        """Determine if an audio loopback dongle is plugged to this host.
2547
2548        @returns 'audio_loopback_dongle' when there is an audio loopback dongle
2549                                         plugged to this host.
2550                 None                    when there is no audio loopback dongle
2551                                         plugged to this host.
2552        """
2553        nodes_info = self.run(command=cras_utils.get_cras_nodes_cmd(),
2554                              ignore_status=True).stdout
2555        if (cras_utils.node_type_is_plugged('HEADPHONE', nodes_info) and
2556            cras_utils.node_type_is_plugged('MIC', nodes_info)):
2557                return 'audio_loopback_dongle'
2558        else:
2559                return None
2560
2561
2562    @label_decorator('power_supply')
2563    def get_power_supply(self):
2564        """
2565        Determine what type of power supply the host has
2566
2567        @returns a string representing this host's power supply.
2568                 'power:battery' when the device has a battery intended for
2569                        extended use
2570                 'power:AC_primary' when the device has a battery not intended
2571                        for extended use (for moving the machine, etc)
2572                 'power:AC_only' when the device has no battery at all.
2573        """
2574        psu = self.run(command='mosys psu type', ignore_status=True)
2575        if psu.exit_status:
2576            # The psu command for mosys is not included for all platforms. The
2577            # assumption is that the device will have a battery if the command
2578            # is not found.
2579            return 'power:battery'
2580
2581        psu_str = psu.stdout.strip()
2582        if psu_str == 'unknown':
2583            return None
2584
2585        return 'power:%s' % psu_str
2586
2587
2588    @label_decorator('storage')
2589    def get_storage(self):
2590        """
2591        Determine the type of boot device for this host.
2592
2593        Determine if the internal device is SCSI or dw_mmc device.
2594        Then check that it is SSD or HDD or eMMC or something else.
2595
2596        @returns a string representing this host's internal device type.
2597                 'storage:ssd' when internal device is solid state drive
2598                 'storage:hdd' when internal device is hard disk drive
2599                 'storage:mmc' when internal device is mmc drive
2600                 None          When internal device is something else or
2601                               when we are unable to determine the type
2602        """
2603        # The output should be /dev/mmcblk* for SD/eMMC or /dev/sd* for scsi
2604        rootdev_cmd = ' '.join(['. /usr/sbin/write_gpt.sh;',
2605                                '. /usr/share/misc/chromeos-common.sh;',
2606                                'load_base_vars;',
2607                                'get_fixed_dst_drive'])
2608        rootdev = self.run(command=rootdev_cmd, ignore_status=True)
2609        if rootdev.exit_status:
2610            logging.info("Fail to run %s", rootdev_cmd)
2611            return None
2612        rootdev_str = rootdev.stdout.strip()
2613
2614        if not rootdev_str:
2615            return None
2616
2617        rootdev_base = os.path.basename(rootdev_str)
2618
2619        mmc_pattern = '/dev/mmcblk[0-9]'
2620        if re.match(mmc_pattern, rootdev_str):
2621            # Use type to determine if the internal device is eMMC or somthing
2622            # else. We can assume that MMC is always an internal device.
2623            type_cmd = 'cat /sys/block/%s/device/type' % rootdev_base
2624            type = self.run(command=type_cmd, ignore_status=True)
2625            if type.exit_status:
2626                logging.info("Fail to run %s", type_cmd)
2627                return None
2628            type_str = type.stdout.strip()
2629
2630            if type_str == 'MMC':
2631                return 'storage:mmc'
2632
2633        scsi_pattern = '/dev/sd[a-z]+'
2634        if re.match(scsi_pattern, rootdev.stdout):
2635            # Read symlink for /sys/block/sd* to determine if the internal
2636            # device is connected via ata or usb.
2637            link_cmd = 'readlink /sys/block/%s' % rootdev_base
2638            link = self.run(command=link_cmd, ignore_status=True)
2639            if link.exit_status:
2640                logging.info("Fail to run %s", link_cmd)
2641                return None
2642            link_str = link.stdout.strip()
2643            if 'usb' in link_str:
2644                return None
2645
2646            # Read rotation to determine if the internal device is ssd or hdd.
2647            rotate_cmd = str('cat /sys/block/%s/queue/rotational'
2648                              % rootdev_base)
2649            rotate = self.run(command=rotate_cmd, ignore_status=True)
2650            if rotate.exit_status:
2651                logging.info("Fail to run %s", rotate_cmd)
2652                return None
2653            rotate_str = rotate.stdout.strip()
2654
2655            rotate_dict = {'0':'storage:ssd', '1':'storage:hdd'}
2656            return rotate_dict.get(rotate_str)
2657
2658        # All other internal device / error case will always fall here
2659        return None
2660
2661
2662    @label_decorator('servo')
2663    def get_servo(self):
2664        """Determine if the host has a servo attached.
2665
2666        If the host has a working servo attached, it should have a servo label.
2667
2668        @return: string 'servo' if the host has servo attached. Otherwise,
2669                 returns None.
2670        """
2671        return 'servo' if self._servo_host else None
2672
2673
2674    @label_decorator('video_labels')
2675    def get_video_labels(self):
2676        """Run /usr/local/bin/avtest_label_detect to get a list of video labels.
2677
2678        Sample output of avtest_label_detect:
2679        Detected label: hw_video_acc_vp8
2680        Detected label: webcam
2681
2682        @return: A list of labels detected by tool avtest_label_detect.
2683        """
2684        try:
2685            result = self.run('/usr/local/bin/avtest_label_detect').stdout
2686            return re.findall('^Detected label: (\w+)$', result, re.M)
2687        except error.AutoservRunError:
2688            # The tool is not installed.
2689            return []
2690
2691
2692    @label_decorator('video_glitch_detection')
2693    def is_video_glitch_detection_supported(self):
2694        """ Determine if a board under test is supported for video glitch
2695        detection tests.
2696
2697        @return: 'video_glitch_detection' if board is supported, None otherwise.
2698        """
2699        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
2700
2701        if board in video_test_constants.SUPPORTED_BOARDS:
2702            return 'video_glitch_detection'
2703
2704        return None
2705
2706
2707    @label_decorator('touch_labels')
2708    def get_touch(self):
2709        """
2710        Determine whether board under test has a touchpad or touchscreen.
2711
2712        @return: A list of some combination of 'touchscreen' and 'touchpad',
2713            depending on what is present on the device.
2714
2715        """
2716        labels = []
2717        looking_for = ['touchpad', 'touchscreen']
2718        player = input_playback.InputPlayback()
2719        input_events = self.run('ls /dev/input/event*').stdout.strip().split()
2720        filename = '/tmp/touch_labels'
2721        for event in input_events:
2722            self.run('evtest %s > %s' % (event, filename), timeout=1,
2723                     ignore_timeout=True)
2724            properties = self.run('cat %s' % filename).stdout
2725            input_type = player._determine_input_type(properties)
2726            if input_type in looking_for:
2727                labels.append(input_type)
2728                looking_for.remove(input_type)
2729            if len(looking_for) == 0:
2730                break
2731        self.run('rm %s' % filename)
2732
2733        return labels
2734
2735
2736    @label_decorator('internal_display')
2737    def has_internal_display(self):
2738        """Determine if the device under test is equipped with an internal
2739        display.
2740
2741        @return: 'internal_display' if one is present; None otherwise.
2742        """
2743        from autotest_lib.client.cros.graphics import graphics_utils
2744        from autotest_lib.client.common_lib import utils as common_utils
2745
2746        def __system_output(cmd):
2747            return self.run(cmd).stdout
2748
2749        def __read_file(remote_path):
2750            return self.run('cat %s' % remote_path).stdout
2751
2752        # Hijack the necessary client functions so that we can take advantage
2753        # of the client lib here.
2754        # FIXME: find a less hacky way than this
2755        original_system_output = utils.system_output
2756        original_read_file = common_utils.read_file
2757        utils.system_output = __system_output
2758        common_utils.read_file = __read_file
2759        try:
2760            return ('internal_display' if graphics_utils.has_internal_display()
2761                                   else None)
2762        finally:
2763            utils.system_output = original_system_output
2764            common_utils.read_file = original_read_file
2765
2766
2767    @label_decorator('lucidsleep')
2768    def has_lucid_sleep_support(self):
2769        """Determine if the device under test has support for lucid sleep.
2770
2771        @return 'lucidsleep' if this board supports lucid sleep; None otherwise
2772        """
2773        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
2774        return 'lucidsleep' if board in LUCID_SLEEP_BOARDS else None
2775
2776
2777    def is_boot_from_usb(self):
2778        """Check if DUT is boot from USB.
2779
2780        @return: True if DUT is boot from usb.
2781        """
2782        device = self.run('rootdev -s -d').stdout.strip()
2783        removable = int(self.run('cat /sys/block/%s/removable' %
2784                                 os.path.basename(device)).stdout.strip())
2785        return removable == 1
2786
2787
2788    def read_from_meminfo(self, key):
2789        """Return the memory info from /proc/meminfo
2790
2791        @param key: meminfo requested
2792
2793        @return the memory value as a string
2794
2795        """
2796        meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip()
2797        logging.debug('%s', meminfo)
2798        return int(re.search(r'\d+', meminfo).group(0))
2799
2800
2801    def get_board_type(self):
2802        """
2803        Get the DUT's device type from /etc/lsb-release.
2804        DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more.
2805
2806        @return value of DEVICETYPE param from lsb-release.
2807        """
2808        device_type = self.run('grep DEVICETYPE /etc/lsb-release',
2809                               ignore_status=True).stdout
2810        if device_type:
2811            return device_type.split('=')[-1].strip()
2812        return ''
2813
2814
2815    def get_os_type(self):
2816        return 'cros'
2817
2818
2819    def enable_adb_testing(self):
2820        """Mark this host as an adb tester."""
2821        self.run('touch %s' % constants.ANDROID_TESTER_FILEFLAG)
2822