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 logging
6import os
7import re
8import sys
9import time
10
11import common
12from autotest_lib.client.bin import utils
13from autotest_lib.client.common_lib import autotemp
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib import global_config
16from autotest_lib.client.common_lib import hosts
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.cros import constants as client_constants
22from autotest_lib.client.cros import cros_ui
23from autotest_lib.client.cros.audio import cras_utils
24from autotest_lib.client.cros.input_playback import input_playback
25from autotest_lib.client.cros.video import constants as video_test_constants
26from autotest_lib.server import afe_utils
27from autotest_lib.server import autoserv_parser
28from autotest_lib.server import autotest
29from autotest_lib.server import constants
30from autotest_lib.server import utils as server_utils
31from autotest_lib.server.cros import provision
32from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
33from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers
34from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig
35from autotest_lib.server.cros.servo import plankton
36from autotest_lib.server.hosts import abstract_ssh
37from autotest_lib.server.hosts import base_label
38from autotest_lib.server.hosts import cros_label
39from autotest_lib.server.hosts import chameleon_host
40from autotest_lib.server.hosts import cros_repair
41from autotest_lib.server.hosts import plankton_host
42from autotest_lib.server.hosts import servo_host
43from autotest_lib.site_utils.rpm_control_system import rpm_client
44
45# In case cros_host is being ran via SSP on an older Moblab version with an
46# older chromite version.
47try:
48    from chromite.lib import metrics
49except ImportError:
50    metrics =  utils.metrics_mock
51
52
53CONFIG = global_config.global_config
54ENABLE_DEVSERVER_TRIGGER_AUTO_UPDATE = CONFIG.get_config_value(
55        'CROS', 'enable_devserver_trigger_auto_update', type=bool,
56        default=False)
57
58LUCID_SLEEP_BOARDS = ['samus', 'lulu']
59
60
61class FactoryImageCheckerException(error.AutoservError):
62    """Exception raised when an image is a factory image."""
63    pass
64
65
66class CrosHost(abstract_ssh.AbstractSSHHost):
67    """Chromium OS specific subclass of Host."""
68
69    VERSION_PREFIX = provision.CROS_VERSION_PREFIX
70
71    _parser = autoserv_parser.autoserv_parser
72    _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
73    support_devserver_provision = ENABLE_DEVSERVER_TRIGGER_AUTO_UPDATE
74
75    # Timeout values (in seconds) associated with various Chrome OS
76    # state changes.
77    #
78    # In general, a good rule of thumb is that the timeout can be up
79    # to twice the typical measured value on the slowest platform.
80    # The times here have not necessarily been empirically tested to
81    # meet this criterion.
82    #
83    # SLEEP_TIMEOUT:  Time to allow for suspend to memory.
84    # RESUME_TIMEOUT: Time to allow for resume after suspend, plus
85    #   time to restart the netwowrk.
86    # SHUTDOWN_TIMEOUT: Time to allow for shut down.
87    # BOOT_TIMEOUT: Time to allow for boot from power off.  Among
88    #   other things, this must account for the 30 second dev-mode
89    #   screen delay, time to start the network on the DUT, and the
90    #   ssh timeout of 120 seconds.
91    # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device,
92    #   including the 30 second dev-mode delay and time to start the
93    #   network.
94    # INSTALL_TIMEOUT: Time to allow for chromeos-install.
95    # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that
96    #   includes powerwash.
97
98    SLEEP_TIMEOUT = 2
99    RESUME_TIMEOUT = 10
100    SHUTDOWN_TIMEOUT = 10
101    BOOT_TIMEOUT = 150
102    USB_BOOT_TIMEOUT = 300
103    INSTALL_TIMEOUT = 480
104    POWERWASH_BOOT_TIMEOUT = 60
105
106    # Minimum OS version that supports server side packaging. Older builds may
107    # not have server side package built or with Autotest code change to support
108    # server-side packaging.
109    MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
110            'AUTOSERV', 'min_version_support_ssp', type=int)
111
112    # REBOOT_TIMEOUT: How long to wait for a reboot.
113    #
114    # We have a long timeout to ensure we don't flakily fail due to other
115    # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
116    # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
117    # return from reboot' bug is solved.
118    REBOOT_TIMEOUT = 480
119
120    # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
121    # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
122    _USB_POWER_TIMEOUT = 5
123    _POWER_CYCLE_TIMEOUT = 10
124
125    _RPM_RECOVERY_BOARDS = CONFIG.get_config_value('CROS',
126            'rpm_recovery_boards', type=str).split(',')
127
128    _LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
129    _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
130                           '-host(\d+)')
131    _LIGHTSENSOR_FILES = [ "in_illuminance0_input",
132                           "in_illuminance_input",
133                           "in_illuminance0_raw",
134                           "in_illuminance_raw",
135                           "illuminance0_input"]
136    _LIGHTSENSOR_SEARCH_DIR = '/sys/bus/iio/devices'
137
138    # Constants used in ping_wait_up() and ping_wait_down().
139    #
140    # _PING_WAIT_COUNT is the approximate number of polling
141    # cycles to use when waiting for a host state change.
142    #
143    # _PING_STATUS_DOWN and _PING_STATUS_UP are names used
144    # for arguments to the internal _ping_wait_for_status()
145    # method.
146    _PING_WAIT_COUNT = 40
147    _PING_STATUS_DOWN = False
148    _PING_STATUS_UP = True
149
150    # Allowed values for the power_method argument.
151
152    # POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods.
153    # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
154    # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
155    POWER_CONTROL_RPM = 'RPM'
156    POWER_CONTROL_SERVO = 'servoj10'
157    POWER_CONTROL_MANUAL = 'manual'
158
159    POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
160                                POWER_CONTROL_SERVO,
161                                POWER_CONTROL_MANUAL)
162
163    _RPM_OUTLET_CHANGED = 'outlet_changed'
164
165    # URL pattern to download firmware image.
166    _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
167            'CROS', 'firmware_url_pattern', type=str)
168
169
170    # A flag file to indicate provision failures.  The file is created
171    # at the start of any AU procedure (see `machine_install()`).  The
172    # file's location in stateful means that on successul update it will
173    # be removed.  Thus, if this file exists, it indicates that we've
174    # tried and failed in a previous attempt to update.
175    PROVISION_FAILED = '/var/tmp/provision_failed'
176
177
178    @staticmethod
179    def check_host(host, timeout=10):
180        """
181        Check if the given host is a chrome-os host.
182
183        @param host: An ssh host representing a device.
184        @param timeout: The timeout for the run command.
185
186        @return: True if the host device is chromeos.
187
188        """
189        try:
190            result = host.run(
191                    'grep -q CHROMEOS /etc/lsb-release && '
192                    '! test -f /mnt/stateful_partition/.android_tester && '
193                    '! grep -q moblab /etc/lsb-release',
194                    ignore_status=True, timeout=timeout)
195        except (error.AutoservRunError, error.AutoservSSHTimeout):
196            return False
197        return result.exit_status == 0
198
199
200    @staticmethod
201    def get_chameleon_arguments(args_dict):
202        """Extract chameleon options from `args_dict` and return the result.
203
204        Recommended usage:
205        ~~~~~~~~
206            args_dict = utils.args_to_dict(args)
207            chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
208            host = hosts.create_host(machine, chameleon_args=chameleon_args)
209        ~~~~~~~~
210
211        @param args_dict Dictionary from which to extract the chameleon
212          arguments.
213        """
214        return {key: args_dict[key]
215                for key in ('chameleon_host', 'chameleon_port')
216                if key in args_dict}
217
218
219    @staticmethod
220    def get_plankton_arguments(args_dict):
221        """Extract chameleon options from `args_dict` and return the result.
222
223        Recommended usage:
224        ~~~~~~~~
225            args_dict = utils.args_to_dict(args)
226            plankton_args = hosts.CrosHost.get_plankton_arguments(args_dict)
227            host = hosts.create_host(machine, plankton_args=plankton_args)
228        ~~~~~~~~
229
230        @param args_dict Dictionary from which to extract the plankton
231          arguments.
232        """
233        return {key: args_dict[key]
234                for key in ('plankton_host', 'plankton_port')
235                if key in args_dict}
236
237
238    @staticmethod
239    def get_servo_arguments(args_dict):
240        """Extract servo options from `args_dict` and return the result.
241
242        Recommended usage:
243        ~~~~~~~~
244            args_dict = utils.args_to_dict(args)
245            servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
246            host = hosts.create_host(machine, servo_args=servo_args)
247        ~~~~~~~~
248
249        @param args_dict Dictionary from which to extract the servo
250          arguments.
251        """
252        servo_attrs = (servo_host.SERVO_HOST_ATTR,
253                       servo_host.SERVO_PORT_ATTR,
254                       servo_host.SERVO_BOARD_ATTR)
255        return {key: args_dict[key]
256                for key in servo_attrs
257                if key in args_dict}
258
259
260    def _initialize(self, hostname, chameleon_args=None, servo_args=None,
261                    plankton_args=None, try_lab_servo=False,
262                    try_servo_repair=False,
263                    ssh_verbosity_flag='', ssh_options='',
264                    *args, **dargs):
265        """Initialize superclasses, |self.chameleon|, and |self.servo|.
266
267        This method will attempt to create the test-assistant object
268        (chameleon/servo) when it is needed by the test. Check
269        the docstring of chameleon_host.create_chameleon_host and
270        servo_host.create_servo_host for how this is determined.
271
272        @param hostname: Hostname of the dut.
273        @param chameleon_args: A dictionary that contains args for creating
274                               a ChameleonHost. See chameleon_host for details.
275        @param servo_args: A dictionary that contains args for creating
276                           a ServoHost object. See servo_host for details.
277        @param try_lab_servo: When true, indicates that an attempt should
278                              be made to create a ServoHost for a DUT in
279                              the test lab, even if not required by
280                              `servo_args`. See servo_host for details.
281        @param try_servo_repair: If a servo host is created, check it
282                              with `repair()` rather than `verify()`.
283                              See servo_host for details.
284        @param ssh_verbosity_flag: String, to pass to the ssh command to control
285                                   verbosity.
286        @param ssh_options: String, other ssh options to pass to the ssh
287                            command.
288        """
289        super(CrosHost, self)._initialize(hostname=hostname,
290                                          *args, **dargs)
291        self._repair_strategy = cros_repair.create_cros_repair_strategy()
292        self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS)
293        # self.env is a dictionary of environment variable settings
294        # to be exported for commands run on the host.
295        # LIBC_FATAL_STDERR_ can be useful for diagnosing certain
296        # errors that might happen.
297        self.env['LIBC_FATAL_STDERR_'] = '1'
298        self._ssh_verbosity_flag = ssh_verbosity_flag
299        self._ssh_options = ssh_options
300        # TODO(fdeng): We need to simplify the
301        # process of servo and servo_host initialization.
302        # crbug.com/298432
303        self._servo_host = servo_host.create_servo_host(
304                dut=self, servo_args=servo_args,
305                try_lab_servo=try_lab_servo,
306                try_servo_repair=try_servo_repair)
307        if self._servo_host is not None:
308            self.servo = self._servo_host.get_servo()
309        else:
310            self.servo = None
311
312        # TODO(waihong): Do the simplication on Chameleon too.
313        self._chameleon_host = chameleon_host.create_chameleon_host(
314                dut=self.hostname, chameleon_args=chameleon_args)
315        # Add plankton host if plankton args were added on command line
316        self._plankton_host = plankton_host.create_plankton_host(plankton_args)
317
318        if self._chameleon_host:
319            self.chameleon = self._chameleon_host.create_chameleon_board()
320        else:
321            self.chameleon = None
322
323        if self._plankton_host:
324            self.plankton_servo = self._plankton_host.get_servo()
325            logging.info('plankton_servo: %r', self.plankton_servo)
326            # Create the plankton object used to access the ec uart
327            self.plankton = plankton.Plankton(self.plankton_servo,
328                    self._plankton_host.get_servod_server_proxy())
329        else:
330            self.plankton = None
331
332
333    def _get_cros_repair_image_name(self):
334        info = self.host_info_store.get()
335        if info.board is None:
336            raise error.AutoservError('Cannot obtain repair image name. '
337                                      'No board label value found')
338
339        return afe_utils.get_stable_cros_image_name(info.board)
340
341
342    def verify_job_repo_url(self, tag=''):
343        """
344        Make sure job_repo_url of this host is valid.
345
346        Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\
347        lumpy-release/R29-4279.0.0/autotest/packages" claims to have the
348        autotest package for lumpy-release/R29-4279.0.0. If this isn't the case,
349        download and extract it. If the devserver embedded in the url is
350        unresponsive, update the job_repo_url of the host after staging it on
351        another devserver.
352
353        @param job_repo_url: A url pointing to the devserver where the autotest
354            package for this build should be staged.
355        @param tag: The tag from the server job, in the format
356                    <job_id>-<user>/<hostname>, or <hostless> for a server job.
357
358        @raises DevServerException: If we could not resolve a devserver.
359        @raises AutoservError: If we're unable to save the new job_repo_url as
360            a result of choosing a new devserver because the old one failed to
361            respond to a health check.
362        @raises urllib2.URLError: If the devserver embedded in job_repo_url
363                                  doesn't respond within the timeout.
364        """
365        job_repo_url = afe_utils.get_host_attribute(self,
366                                                    ds_constants.JOB_REPO_URL)
367        if not job_repo_url:
368            logging.warning('No job repo url set on host %s', self.hostname)
369            return
370
371        logging.info('Verifying job repo url %s', job_repo_url)
372        devserver_url, image_name = tools.get_devserver_build_from_package_url(
373            job_repo_url)
374
375        ds = dev_server.ImageServer(devserver_url)
376
377        logging.info('Staging autotest artifacts for %s on devserver %s',
378            image_name, ds.url())
379
380        start_time = time.time()
381        ds.stage_artifacts(image_name, ['autotest_packages'])
382        stage_time = time.time() - start_time
383
384        # Record how much of the verification time comes from a devserver
385        # restage. If we're doing things right we should not see multiple
386        # devservers for a given board/build/branch path.
387        try:
388            board, build_type, branch = server_utils.ParseBuildName(
389                                                image_name)[:3]
390        except server_utils.ParseBuildNameException:
391            pass
392        else:
393            devserver = devserver_url[
394                devserver_url.find('/') + 2:devserver_url.rfind(':')]
395            stats_key = {
396                'board': board,
397                'build_type': build_type,
398                'branch': branch,
399                'devserver': devserver.replace('.', '_'),
400            }
401
402            monarch_fields = {
403                'board': board,
404                'build_type': build_type,
405                # TODO(akeshet): To be consistent with most other metrics,
406                # consider changing the following field to be named
407                # 'milestone'.
408                'branch': branch,
409                'dev_server': devserver,
410            }
411            metrics.Counter(
412                    'chromeos/autotest/provision/verify_url'
413                    ).increment(fields=monarch_fields)
414            metrics.SecondsDistribution(
415                    'chromeos/autotest/provision/verify_url_duration'
416                    ).add(stage_time, fields=monarch_fields)
417
418
419    def stage_server_side_package(self, image=None):
420        """Stage autotest server-side package on devserver.
421
422        @param image: Full path of an OS image to install or a build name.
423
424        @return: A url to the autotest server-side package.
425
426        @raise: error.AutoservError if fail to locate the build to test with, or
427                fail to stage server-side package.
428        """
429        # If enable_drone_in_restricted_subnet is False, do not set hostname
430        # in devserver.resolve call, so a devserver in non-restricted subnet
431        # is picked to stage autotest server package for drone to download.
432        hostname = self.hostname
433        if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
434            hostname = None
435        if image:
436            image_name = tools.get_build_from_image(image)
437            if not image_name:
438                raise error.AutoservError(
439                        'Failed to parse build name from %s' % image)
440            ds = dev_server.ImageServer.resolve(image_name, hostname)
441        else:
442            job_repo_url = afe_utils.get_host_attribute(
443                    self, ds_constants.JOB_REPO_URL)
444            if job_repo_url:
445                devserver_url, image_name = (
446                    tools.get_devserver_build_from_package_url(job_repo_url))
447                # If enable_drone_in_restricted_subnet is True, use the
448                # existing devserver. Otherwise, resolve a new one in
449                # non-restricted subnet.
450                if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
451                    ds = dev_server.ImageServer(devserver_url)
452                else:
453                    ds = dev_server.ImageServer.resolve(image_name)
454            elif info.build is not None:
455                ds = dev_server.ImageServer.resolve(info.build, hostname)
456            else:
457                raise error.AutoservError(
458                        'Failed to stage server-side package. The host has '
459                        'no job_report_url attribute or version label.')
460
461        # Get the OS version of the build, for any build older than
462        # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported.
463        match = re.match('.*/R\d+-(\d+)\.', image_name)
464        if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP:
465            raise error.AutoservError(
466                    'Build %s is older than %s. Server side packaging is '
467                    'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP))
468
469        ds.stage_artifacts(image_name, ['autotest_server_package'])
470        return '%s/static/%s/%s' % (ds.url(), image_name,
471                                    'autotest_server_package.tar.bz2')
472
473
474    def _try_stateful_update(self, update_url, force_update, updater):
475        """Try to use stateful update to initialize DUT.
476
477        When DUT is already running the same version that machine_install
478        tries to install, stateful update is a much faster way to clean up
479        the DUT for testing, compared to a full reimage. It is implemeted
480        by calling autoupdater.run_update, but skipping updating root, as
481        updating the kernel is time consuming and not necessary.
482
483        @param update_url: url of the image.
484        @param force_update: Set to True to update the image even if the DUT
485            is running the same version.
486        @param updater: ChromiumOSUpdater instance used to update the DUT.
487        @returns: True if the DUT was updated with stateful update.
488
489        """
490        # Stop service ap-update-manager to prevent rebooting during autoupdate.
491        # The service is used in jetstream boards, but not other CrOS devices.
492        self.run('sudo stop ap-update-manager', ignore_status=True)
493
494        # TODO(jrbarnette):  Yes, I hate this re.match() test case.
495        # It's better than the alternative:  see crbug.com/360944.
496        image_name = autoupdater.url_to_image_name(update_url)
497        release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$'
498        if not re.match(release_pattern, image_name):
499            return False
500        if not updater.check_version():
501            return False
502        if not force_update:
503            logging.info('Canceling stateful update because the new and '
504                         'old versions are the same.')
505            return False
506        # Following folders should be rebuilt after stateful update.
507        # A test file is used to confirm each folder gets rebuilt after
508        # the stateful update.
509        folders_to_check = ['/var', '/home', '/mnt/stateful_partition']
510        test_file = '.test_file_to_be_deleted'
511        for folder in folders_to_check:
512            touch_path = os.path.join(folder, test_file)
513            self.run('touch %s' % touch_path)
514
515        updater.run_update(update_root=False)
516
517        # Reboot to complete stateful update.
518        self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
519
520        # After stateful update and a reboot, all of the test_files shouldn't
521        # exist any more. Otherwise the stateful update is failed.
522        return not any(
523            self.path_exists(os.path.join(folder, test_file))
524            for folder in folders_to_check)
525
526
527    def _post_update_processing(self, updater, expected_kernel=None):
528        """After the DUT is updated, confirm machine_install succeeded.
529
530        @param updater: ChromiumOSUpdater instance used to update the DUT.
531        @param expected_kernel: kernel expected to be active after reboot,
532            or `None` to skip rollback checking.
533
534        """
535        # Touch the lab machine file to leave a marker that
536        # distinguishes this image from other test images.
537        # Afterwards, we must re-run the autoreboot script because
538        # it depends on the _LAB_MACHINE_FILE.
539        autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
540                          '( touch "$FILE" ; start autoreboot )')
541        self.run(autoreboot_cmd % self._LAB_MACHINE_FILE)
542        updater.verify_boot_expectations(
543                expected_kernel, rollback_message=
544                'Build %s failed to boot on %s; system rolled back to previous '
545                'build' % (updater.update_version, self.hostname))
546        # Check that we've got the build we meant to install.
547        if not updater.check_version_to_confirm_install():
548            raise autoupdater.ChromiumOSError(
549                'Failed to update %s to build %s; found build '
550                '%s instead' % (self.hostname,
551                                updater.update_version,
552                                self.get_release_version()))
553
554        logging.debug('Cleaning up old autotest directories.')
555        try:
556            installed_autodir = autotest.Autotest.get_installed_autodir(self)
557            self.run('rm -rf ' + installed_autodir)
558        except autotest.AutodirNotFoundError:
559            logging.debug('No autotest installed directory found.')
560
561
562    def _stage_image_for_update(self, image_name=None):
563        """Stage a build on a devserver and return the update_url and devserver.
564
565        @param image_name: a name like lumpy-release/R27-3837.0.0
566        @returns a tuple with an update URL like:
567            http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
568            and the devserver instance.
569        """
570        if not image_name:
571            image_name = self._get_cros_repair_image_name()
572        logging.info('Staging build for AU: %s', image_name)
573        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
574        devserver.trigger_download(image_name, synchronous=False)
575        return (tools.image_url_pattern() % (devserver.url(), image_name),
576                devserver)
577
578
579    def stage_image_for_servo(self, image_name=None):
580        """Stage a build on a devserver and return the update_url.
581
582        @param image_name: a name like lumpy-release/R27-3837.0.0
583        @returns an update URL like:
584            http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
585        """
586        if not image_name:
587            image_name = self._get_cros_repair_image_name()
588        logging.info('Staging build for servo install: %s', image_name)
589        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
590        devserver.stage_artifacts(image_name, ['test_image'])
591        return devserver.get_test_image_url(image_name)
592
593
594    def stage_factory_image_for_servo(self, image_name):
595        """Stage a build on a devserver and return the update_url.
596
597        @param image_name: a name like <baord>/4262.204.0
598
599        @return: An update URL, eg:
600            http://<devserver>/static/canary-channel/\
601            <board>/4262.204.0/factory_test/chromiumos_factory_image.bin
602
603        @raises: ValueError if the factory artifact name is missing from
604                 the config.
605
606        """
607        if not image_name:
608            logging.error('Need an image_name to stage a factory image.')
609            return
610
611        factory_artifact = CONFIG.get_config_value(
612                'CROS', 'factory_artifact', type=str, default='')
613        if not factory_artifact:
614            raise ValueError('Cannot retrieve the factory artifact name from '
615                             'autotest config, and hence cannot stage factory '
616                             'artifacts.')
617
618        logging.info('Staging build for servo install: %s', image_name)
619        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
620        devserver.stage_artifacts(
621                image_name,
622                [factory_artifact],
623                archive_url=None)
624
625        return tools.factory_image_url_pattern() % (devserver.url(), image_name)
626
627
628    def _get_au_monarch_fields(self, devserver, build):
629        """Form monarch fields by given devserve & build for auto-update.
630
631        @param devserver: the devserver (ImageServer instance) for auto-update.
632        @param build: the build to be updated.
633
634        @return A dictionary of monach fields.
635        """
636        try:
637            board, build_type, milestone, _ = server_utils.ParseBuildName(build)
638        except server_utils.ParseBuildNameException:
639            logging.warning('Unable to parse build name %s. Something is '
640                            'likely broken, but will continue anyway.',
641                            build)
642            board, build_type, milestone = ('', '', '')
643
644        monarch_fields = {
645                'dev_server': devserver.hostname,
646                'board': board,
647                'build_type': build_type,
648                'milestone': milestone
649        }
650        return monarch_fields
651
652
653    def machine_install_by_devserver(self, update_url=None, force_update=False,
654                    local_devserver=False, repair=False,
655                    force_full_update=False):
656        """Ultiize devserver to install the DUT.
657
658        (TODO) crbugs.com/627269: The logic in this function has some overlap
659        with those in function machine_install. The merge will be done later,
660        not in the same CL.
661
662        @param update_url: The update_url or build for the host to update.
663        @param force_update: Force an update even if the version installed
664                is the same. Default:False
665        @param local_devserver: Used by test_that to allow people to
666                use their local devserver. Default: False
667        @param repair: Forces update to repair image. Implies force_update.
668        @param force_full_update: If True, do not attempt to run stateful
669                update, force a full reimage. If False, try stateful update
670                first when the dut is already installed with the same version.
671        @raises autoupdater.ChromiumOSError
672
673        @returns A tuple of (image_name, host_attributes).
674                image_name is the name of image installed, e.g.,
675                veyron_jerry-release/R50-7871.0.0
676                host_attributes is a dictionary of (attribute, value), which
677                can be saved to afe_host_attributes table in database. This
678                method returns a dictionary with a single entry of
679                `job_repo_url`: repo_url, where repo_url is a devserver url to
680                autotest packages.
681        """
682        if repair:
683            update_url = self._get_cros_repair_image_name()
684            force_update = True
685
686        if not update_url and not self._parser.options.image:
687            raise error.AutoservError(
688                    'There is no update URL, nor a method to get one.')
689
690        if not update_url and self._parser.options.image:
691            update_url = self._parser.options.image
692
693
694        # Get build from parameter or AFE.
695        # If the build is not a URL, let devserver to stage it first.
696        # Otherwise, choose a devserver to trigger auto-update.
697        build = None
698        devserver = None
699        logging.debug('Resolving a devserver for auto-update')
700        previously_resolved = False
701        if update_url.startswith('http://'):
702            build = autoupdater.url_to_image_name(update_url)
703            previously_resolved = True
704        else:
705            build = update_url
706        devserver = dev_server.resolve(build, self.hostname)
707        server_name = devserver.hostname
708
709        monarch_fields = self._get_au_monarch_fields(devserver, build)
710
711        if previously_resolved:
712            # Make sure devserver for Auto-Update has staged the build.
713            if server_name not in update_url:
714              logging.debug('Resolved to devserver that does not match '
715                            'update_url. The previously resolved devserver '
716                            'must be unhealthy. Switching to use devserver %s,'
717                            ' and re-staging.',
718                            server_name)
719              logging.info('Staging build for AU: %s', update_url)
720              devserver.trigger_download(build, synchronous=False)
721              c = metrics.Counter(
722                  'chromeos/autotest/provision/failover_download')
723              c.increment(fields=monarch_fields)
724        else:
725            logging.info('Staging build for AU: %s', update_url)
726            devserver.trigger_download(build, synchronous=False)
727            c = metrics.Counter('chromeos/autotest/provision/trigger_download')
728            c.increment(fields=monarch_fields)
729
730        # Report provision stats.
731        install_with_dev_counter = metrics.Counter(
732                'chromeos/autotest/provision/install_with_devserver')
733        install_with_dev_counter.increment(fields=monarch_fields)
734        logging.debug('Resolved devserver for auto-update: %s', devserver.url())
735
736        # and other metrics from this function.
737        metrics.Counter('chromeos/autotest/provision/resolve'
738                        ).increment(fields=monarch_fields)
739
740        success, retryable = devserver.auto_update(
741                self.hostname, build, log_dir=self.job.resultdir,
742                force_update=force_update, full_update=force_full_update)
743        if not success and retryable:
744          # It indicates that last provision failed due to devserver load
745          # issue, so another devserver is resolved to kick off provision
746          # job once again and only once.
747          logging.debug('Provision failed due to devserver issue,'
748                        'retry it with another devserver.')
749
750          # Check first whether this DUT is completely offline. If so, skip
751          # the following provision tries.
752          logging.debug('Checking whether host %s is online.', self.hostname)
753          if utils.ping(self.hostname, tries=1, deadline=1) == 0:
754              devserver = dev_server.resolve(
755                      build, self.hostname, ban_list=[devserver.url()])
756              devserver.trigger_download(build, synchronous=False)
757              monarch_fields = self._get_au_monarch_fields(devserver, build)
758              logging.debug('Retry auto_update: resolved devserver for '
759                            'auto-update: %s', devserver.url())
760
761              # Add metrics
762              install_with_dev_counter.increment(fields=monarch_fields)
763              c = metrics.Counter(
764                      'chromeos/autotest/provision/retry_by_devserver')
765              monarch_fields['last_devserver'] = server_name
766              monarch_fields['host'] = self.hostname
767              c.increment(fields=monarch_fields)
768
769              devserver.auto_update(
770                    self.hostname, build, log_dir=self.job.resultdir,
771                    force_update=force_update, full_update=force_full_update)
772
773
774        # The reason to resolve a new devserver in function machine_install
775        # is mostly because that the update_url there may has a strange format,
776        # and it's hard to parse the devserver url from it.
777        # Since we already resolve a devserver to trigger auto-update, the same
778        # devserver is used to form JOB_REPO_URL here. Verified in local test.
779        repo_url = tools.get_package_url(devserver.url(), build)
780        return build, {ds_constants.JOB_REPO_URL: repo_url}
781
782
783    def machine_install(self, update_url=None, force_update=False,
784                        local_devserver=False, repair=False,
785                        force_full_update=False):
786        """Install the DUT.
787
788        Use stateful update if the DUT is already running the same build.
789        Stateful update does not update kernel and tends to run much faster
790        than a full reimage. If the DUT is running a different build, or it
791        failed to do a stateful update, full update, including kernel update,
792        will be applied to the DUT.
793
794        Once a host enters machine_install its host attribute job_repo_url
795        (used for package install) will be removed and then updated.
796
797        @param update_url: The url to use for the update
798                pattern: http://$devserver:###/update/$build
799                If update_url is None and repair is True we will install the
800                stable image listed in afe_stable_versions table. If the table
801                is not setup, global_config value under CROS.stable_cros_version
802                will be used instead.
803        @param force_update: Force an update even if the version installed
804                is the same. Default:False
805        @param local_devserver: Used by test_that to allow people to
806                use their local devserver. Default: False
807        @param repair: Forces update to repair image. Implies force_update.
808        @param force_full_update: If True, do not attempt to run stateful
809                update, force a full reimage. If False, try stateful update
810                first when the dut is already installed with the same version.
811        @raises autoupdater.ChromiumOSError
812
813        @returns A tuple of (image_name, host_attributes).
814                image_name is the name of image installed, e.g.,
815                veyron_jerry-release/R50-7871.0.0
816                host_attributes is a dictionary of (attribute, value), which
817                can be saved to afe_host_attributes table in database. This
818                method returns a dictionary with a single entry of
819                `job_repo_url`: repo_url, where repo_url is a devserver url to
820                autotest packages.
821        """
822        devserver = None
823        if repair:
824            update_url, devserver = self._stage_image_for_update()
825            force_update = True
826
827        if not update_url and not self._parser.options.image:
828            raise error.AutoservError(
829                    'There is no update URL, nor a method to get one.')
830
831        if not update_url and self._parser.options.image:
832            # This is the base case where we have no given update URL i.e.
833            # dynamic suites logic etc. This is the most flexible case where we
834            # can serve an update from any of our fleet of devservers.
835            requested_build = self._parser.options.image
836            if not requested_build.startswith('http://'):
837                logging.debug('Update will be staged for this installation')
838                update_url, devserver = self._stage_image_for_update(
839                        requested_build)
840            else:
841                update_url = requested_build
842
843        logging.debug('Update URL is %s', update_url)
844
845        # Report provision stats.
846        server_name = dev_server.get_hostname(update_url)
847        (metrics.Counter('chromeos/autotest/provision/install')
848         .increment(fields={'devserver': server_name}))
849
850        # Create a file to indicate if provision fails. The file will be removed
851        # by stateful update or full install.
852        self.run('touch %s' % self.PROVISION_FAILED)
853
854        update_complete = False
855        updater = autoupdater.ChromiumOSUpdater(
856                 update_url, host=self, local_devserver=local_devserver)
857        if not force_full_update:
858            try:
859                # If the DUT is already running the same build, try stateful
860                # update first as it's much quicker than a full re-image.
861                update_complete = self._try_stateful_update(
862                        update_url, force_update, updater)
863            except Exception as e:
864                logging.exception(e)
865
866        inactive_kernel = None
867        if update_complete or (not force_update and updater.check_version()):
868            logging.info('Install complete without full update')
869        else:
870            logging.info('DUT requires full update.')
871            self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
872            # Stop service ap-update-manager to prevent rebooting during
873            # autoupdate. The service is used in jetstream boards, but not other
874            # CrOS devices.
875            self.run('sudo stop ap-update-manager', ignore_status=True)
876
877            num_of_attempts = provision.FLAKY_DEVSERVER_ATTEMPTS
878
879            while num_of_attempts > 0:
880                num_of_attempts -= 1
881                try:
882                    updater.run_update()
883                except Exception:
884                    logging.warn('Autoupdate did not complete.')
885                    # Do additional check for the devserver health. Ideally,
886                    # the autoupdater.py could raise an exception when it
887                    # detected network flake but that would require
888                    # instrumenting the update engine and parsing it log.
889                    if (num_of_attempts <= 0 or
890                            devserver is None or
891                            dev_server.ImageServer.devserver_healthy(
892                                    devserver.url())):
893                        raise
894
895                    logging.warn('Devserver looks unhealthy. Trying another')
896                    update_url, devserver = self._stage_image_for_update(
897                            requested_build)
898                    logging.debug('New Update URL is %s', update_url)
899                    updater = autoupdater.ChromiumOSUpdater(
900                            update_url, host=self,
901                            local_devserver=local_devserver)
902                else:
903                    break
904
905            # Give it some time in case of IO issues.
906            time.sleep(10)
907
908            # Figure out active and inactive kernel.
909            active_kernel, inactive_kernel = updater.get_kernel_state()
910
911            # Ensure inactive kernel has higher priority than active.
912            if (updater.get_kernel_priority(inactive_kernel)
913                    < updater.get_kernel_priority(active_kernel)):
914                raise autoupdater.ChromiumOSError(
915                    'Update failed. The priority of the inactive kernel'
916                    ' partition is less than that of the active kernel'
917                    ' partition.')
918
919            # Updater has returned successfully; reboot the host.
920            #
921            # Regarding the 'crossystem' command: In some cases, the
922            # TPM gets into a state such that it fails verification.
923            # We don't know why.  However, this call papers over the
924            # problem by clearing the TPM during the reboot.
925            #
926            # We ignore failures from 'crossystem'.  Although failure
927            # here is unexpected, and could signal a bug, the point
928            # of the exercise is to paper over problems; allowing
929            # this to fail would defeat the purpose.
930            self.run('crossystem clear_tpm_owner_request=1',
931                     ignore_status=True)
932            self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
933
934        self._post_update_processing(updater, inactive_kernel)
935        image_name = autoupdater.url_to_image_name(update_url)
936        # update_url is different from devserver url needed to stage autotest
937        # packages, therefore, resolve a new devserver url here.
938        devserver_url = dev_server.ImageServer.resolve(image_name,
939                                                       self.hostname).url()
940        repo_url = tools.get_package_url(devserver_url, image_name)
941        return image_name, {ds_constants.JOB_REPO_URL: repo_url}
942
943
944    def _clear_fw_version_labels(self, rw_only):
945        """Clear firmware version labels from the machine.
946
947        @param rw_only: True to only clear fwrw_version; otherewise, clear
948                        both fwro_version and fwrw_version.
949        """
950        labels = self._AFE.get_labels(
951                name__startswith=provision.FW_RW_VERSION_PREFIX,
952                host__hostname=self.hostname)
953        if not rw_only:
954            labels = labels + self._AFE.get_labels(
955                    name__startswith=provision.FW_RO_VERSION_PREFIX,
956                    host__hostname=self.hostname)
957        for label in labels:
958            label.remove_hosts(hosts=[self.hostname])
959
960
961    def _add_fw_version_label(self, build, rw_only):
962        """Add firmware version label to the machine.
963
964        @param build: Build of firmware.
965        @param rw_only: True to only add fwrw_version; otherwise, add both
966                        fwro_version and fwrw_version.
967
968        """
969        fw_label = provision.fwrw_version_to_label(build)
970        self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
971        if not rw_only:
972            fw_label = provision.fwro_version_to_label(build)
973            self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
974
975
976    def firmware_install(self, build=None, rw_only=False):
977        """Install firmware to the DUT.
978
979        Use stateful update if the DUT is already running the same build.
980        Stateful update does not update kernel and tends to run much faster
981        than a full reimage. If the DUT is running a different build, or it
982        failed to do a stateful update, full update, including kernel update,
983        will be applied to the DUT.
984
985        Once a host enters firmware_install its fw[ro|rw]_version label will
986        be removed. After the firmware is updated successfully, a new
987        fw[ro|rw]_version label will be added to the host.
988
989        @param build: The build version to which we want to provision the
990                      firmware of the machine,
991                      e.g. 'link-firmware/R22-2695.1.144'.
992        @param rw_only: True to only install firmware to its RW portions. Keep
993                        the RO portions unchanged.
994
995        TODO(dshi): After bug 381718 is fixed, update here with corresponding
996                    exceptions that could be raised.
997
998        """
999        if not self.servo:
1000            raise error.TestError('Host %s does not have servo.' %
1001                                  self.hostname)
1002
1003        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
1004
1005        # If build is not set, try to install firmware from stable CrOS.
1006        if not build:
1007            build = afe_utils.get_stable_faft_version(board)
1008            if not build:
1009                raise error.TestError(
1010                        'Failed to find stable firmware build for %s.',
1011                        self.hostname)
1012            logging.info('Will install firmware from build %s.', build)
1013
1014        config = FAFTConfig(board)
1015        if config.use_u_boot:
1016            ap_image = 'image-%s.bin' % board
1017        else: # Depthcharge platform
1018            ap_image = 'image.bin'
1019        ec_image = 'ec.bin'
1020        ds = dev_server.ImageServer.resolve(build, self.hostname)
1021        ds.stage_artifacts(build, ['firmware'])
1022
1023        tmpd = autotemp.tempdir(unique_id='fwimage')
1024        try:
1025            fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
1026            local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl))
1027            ds.download_file(fwurl, local_tarball)
1028
1029            self._clear_fw_version_labels(rw_only)
1030            if config.chrome_ec:
1031                logging.info('Will re-program EC %snow', 'RW ' if rw_only else '')
1032                server_utils.system('tar xf %s -C %s %s' %
1033                                    (local_tarball, tmpd.name, ec_image),
1034                                    timeout=60)
1035                self.servo.program_ec(os.path.join(tmpd.name, ec_image), rw_only)
1036            else:
1037                logging.info('Not a Chrome EC, ignore re-programing it')
1038            logging.info('Will re-program BIOS %snow', 'RW ' if rw_only else '')
1039            server_utils.system('tar xf %s -C %s %s' %
1040                                (local_tarball, tmpd.name, ap_image),
1041                                timeout=60)
1042            self.servo.program_bios(os.path.join(tmpd.name, ap_image), rw_only)
1043            self.servo.get_power_state_controller().reset()
1044            time.sleep(self.servo.BOOT_DELAY)
1045            if utils.host_is_in_lab_zone(self.hostname):
1046                self._add_fw_version_label(build, rw_only)
1047        finally:
1048            tmpd.clean()
1049
1050
1051    def show_update_engine_log(self):
1052        """Output update engine log."""
1053        logging.debug('Dumping %s', client_constants.UPDATE_ENGINE_LOG)
1054        self.run('cat %s' % client_constants.UPDATE_ENGINE_LOG)
1055
1056
1057    def _get_board_from_afe(self):
1058        """Retrieve this host's board from its labels stored locally.
1059
1060        Looks for a host label of the form "board:<board>", and
1061        returns the "<board>" part of the label.  `None` is returned
1062        if there is not a single, unique label matching the pattern.
1063
1064        @returns board from label, or `None`.
1065        """
1066        board = afe_utils.get_board(self)
1067        if board is None:
1068            raise error.AutoservError('DUT cannot be repaired, '
1069                                      'there is no board attribute.')
1070        return board
1071
1072
1073    def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT,
1074                      install_timeout=INSTALL_TIMEOUT):
1075        """
1076        Re-install the OS on the DUT by:
1077        1) installing a test image on a USB storage device attached to the Servo
1078                board,
1079        2) booting that image in recovery mode, and then
1080        3) installing the image with chromeos-install.
1081
1082        @param image_url: If specified use as the url to install on the DUT.
1083                otherwise boot the currently staged image on the USB stick.
1084        @param usb_boot_timeout: The usb_boot_timeout to use during reimage.
1085                Factory images need a longer usb_boot_timeout than regular
1086                cros images.
1087        @param install_timeout: The timeout to use when installing the chromeos
1088                image. Factory images need a longer install_timeout.
1089
1090        @raises AutoservError if the image fails to boot.
1091
1092        """
1093        logging.info('Downloading image to USB, then booting from it. Usb boot '
1094                     'timeout = %s', usb_boot_timeout)
1095        with metrics.SecondsTimer(
1096                'chromeos/autotest/provision/servo_install/boot_duration'):
1097            self.servo.install_recovery_image(image_url)
1098            if not self.wait_up(timeout=usb_boot_timeout):
1099                raise hosts.AutoservRepairError(
1100                        'DUT failed to boot from USB after %d seconds' %
1101                        usb_boot_timeout)
1102
1103        # The new chromeos-tpm-recovery has been merged since R44-7073.0.0.
1104        # In old CrOS images, this command fails. Skip the error.
1105        logging.info('Resetting the TPM status')
1106        try:
1107            self.run('chromeos-tpm-recovery')
1108        except error.AutoservRunError:
1109            logging.warn('chromeos-tpm-recovery is too old.')
1110
1111
1112        with metrics.SecondsTimer(
1113                'chromeos/autotest/provision/servo_install/install_duration'):
1114            logging.info('Installing image through chromeos-install.')
1115            self.run('chromeos-install --yes', timeout=install_timeout)
1116            self.halt()
1117
1118        logging.info('Power cycling DUT through servo.')
1119        self.servo.get_power_state_controller().power_off()
1120        self.servo.switch_usbkey('off')
1121        # N.B. The Servo API requires that we use power_on() here
1122        # for two reasons:
1123        #  1) After turning on a DUT in recovery mode, you must turn
1124        #     it off and then on with power_on() once more to
1125        #     disable recovery mode (this is a Parrot specific
1126        #     requirement).
1127        #  2) After power_off(), the only way to turn on is with
1128        #     power_on() (this is a Storm specific requirement).
1129        self.servo.get_power_state_controller().power_on()
1130
1131        logging.info('Waiting for DUT to come back up.')
1132        if not self.wait_up(timeout=self.BOOT_TIMEOUT):
1133            raise error.AutoservError('DUT failed to reboot installed '
1134                                      'test image after %d seconds' %
1135                                      self.BOOT_TIMEOUT)
1136
1137
1138    def repair_servo(self):
1139        """
1140        Confirm that servo is initialized and verified.
1141
1142        If the servo object is missing, attempt to repair the servo
1143        host.  Repair failures are passed back to the caller.
1144
1145        @raise AutoservError: If there is no servo host for this CrOS
1146                              host.
1147        """
1148        if self.servo:
1149            return
1150        if not self._servo_host:
1151            raise error.AutoservError('No servo host for %s.' %
1152                                      self.hostname)
1153        self._servo_host.repair()
1154        self.servo = self._servo_host.get_servo()
1155
1156
1157    def repair(self):
1158        """Attempt to get the DUT to pass `self.verify()`.
1159
1160        This overrides the base class function for repair; it does
1161        not call back to the parent class, but instead relies on
1162        `self._repair_strategy` to coordinate the verification and
1163        repair steps needed to get the DUT working.
1164        """
1165        self._repair_strategy.repair(self)
1166
1167
1168    def close(self):
1169        super(CrosHost, self).close()
1170        if self._chameleon_host:
1171            self._chameleon_host.close()
1172
1173        if self._servo_host:
1174            self._servo_host.close()
1175
1176
1177    def get_power_supply_info(self):
1178        """Get the output of power_supply_info.
1179
1180        power_supply_info outputs the info of each power supply, e.g.,
1181        Device: Line Power
1182          online:                  no
1183          type:                    Mains
1184          voltage (V):             0
1185          current (A):             0
1186        Device: Battery
1187          state:                   Discharging
1188          percentage:              95.9276
1189          technology:              Li-ion
1190
1191        Above output shows two devices, Line Power and Battery, with details of
1192        each device listed. This function parses the output into a dictionary,
1193        with key being the device name, and value being a dictionary of details
1194        of the device info.
1195
1196        @return: The dictionary of power_supply_info, e.g.,
1197                 {'Line Power': {'online': 'yes', 'type': 'main'},
1198                  'Battery': {'vendor': 'xyz', 'percentage': '100'}}
1199        @raise error.AutoservRunError if power_supply_info tool is not found in
1200               the DUT. Caller should handle this error to avoid false failure
1201               on verification.
1202        """
1203        result = self.run('power_supply_info').stdout.strip()
1204        info = {}
1205        device_name = None
1206        device_info = {}
1207        for line in result.split('\n'):
1208            pair = [v.strip() for v in line.split(':')]
1209            if len(pair) != 2:
1210                continue
1211            if pair[0] == 'Device':
1212                if device_name:
1213                    info[device_name] = device_info
1214                device_name = pair[1]
1215                device_info = {}
1216            else:
1217                device_info[pair[0]] = pair[1]
1218        if device_name and not device_name in info:
1219            info[device_name] = device_info
1220        return info
1221
1222
1223    def get_battery_percentage(self):
1224        """Get the battery percentage.
1225
1226        @return: The percentage of battery level, value range from 0-100. Return
1227                 None if the battery info cannot be retrieved.
1228        """
1229        try:
1230            info = self.get_power_supply_info()
1231            logging.info(info)
1232            return float(info['Battery']['percentage'])
1233        except (KeyError, ValueError, error.AutoservRunError):
1234            return None
1235
1236
1237    def is_ac_connected(self):
1238        """Check if the dut has power adapter connected and charging.
1239
1240        @return: True if power adapter is connected and charging.
1241        """
1242        try:
1243            info = self.get_power_supply_info()
1244            return info['Line Power']['online'] == 'yes'
1245        except (KeyError, error.AutoservRunError):
1246            return None
1247
1248
1249    def _cleanup_poweron(self):
1250        """Special cleanup method to make sure hosts always get power back."""
1251        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
1252        hosts = afe.get_hosts(hostname=self.hostname)
1253        if not hosts or not (self._RPM_OUTLET_CHANGED in
1254                             hosts[0].attributes):
1255            return
1256        logging.debug('This host has recently interacted with the RPM'
1257                      ' Infrastructure. Ensuring power is on.')
1258        try:
1259            self.power_on()
1260            afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
1261                                   hostname=self.hostname)
1262        except rpm_client.RemotePowerException:
1263            logging.error('Failed to turn Power On for this host after '
1264                          'cleanup through the RPM Infrastructure.')
1265            autotest_es.post(
1266                    type_str='RPM_poweron_failure',
1267                    metadata={'hostname': self.hostname})
1268
1269            battery_percentage = self.get_battery_percentage()
1270            if battery_percentage and battery_percentage < 50:
1271                raise
1272            elif self.is_ac_connected():
1273                logging.info('The device has power adapter connected and '
1274                             'charging. No need to try to turn RPM on '
1275                             'again.')
1276                afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
1277                                       hostname=self.hostname)
1278            logging.info('Battery level is now at %s%%. The device may '
1279                         'still have enough power to run test, so no '
1280                         'exception will be raised.', battery_percentage)
1281
1282
1283    def _is_factory_image(self):
1284        """Checks if the image on the DUT is a factory image.
1285
1286        @return: True if the image on the DUT is a factory image.
1287                 False otherwise.
1288        """
1289        result = self.run('[ -f /root/.factory_test ]', ignore_status=True)
1290        return result.exit_status == 0
1291
1292
1293    def _restart_ui(self):
1294        """Restart the Chrome UI.
1295
1296        @raises: FactoryImageCheckerException for factory images, since
1297                 we cannot attempt to restart ui on them.
1298                 error.AutoservRunError for any other type of error that
1299                 occurs while restarting ui.
1300        """
1301        if self._is_factory_image():
1302            raise FactoryImageCheckerException('Cannot restart ui on factory '
1303                                               'images')
1304
1305        # TODO(jrbarnette):  The command to stop/start the ui job
1306        # should live inside cros_ui, too.  However that would seem
1307        # to imply interface changes to the existing start()/restart()
1308        # functions, which is a bridge too far (for now).
1309        prompt = cros_ui.get_chrome_session_ident(self)
1310        self.run('stop ui; start ui')
1311        cros_ui.wait_for_chrome_ready(prompt, self)
1312
1313
1314    def get_release_version(self):
1315        """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
1316
1317        @returns The version string in lsb-release, under attribute
1318                 CHROMEOS_RELEASE_VERSION.
1319        """
1320        lsb_release_content = self.run(
1321                    'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
1322        return lsbrelease_utils.get_chromeos_release_version(
1323                    lsb_release_content=lsb_release_content)
1324
1325
1326    def verify_cros_version_label(self):
1327        """ Make sure host's cros-version label match the actual image in dut.
1328
1329        Remove any cros-version: label that doesn't match that installed in
1330        the dut.
1331
1332        @param raise_error: Set to True to raise exception if any mismatch found
1333
1334        @raise error.AutoservError: If any mismatch between cros-version label
1335                                    and the build installed in dut is found.
1336        """
1337        labels = self._AFE.get_labels(
1338                name__startswith=ds_constants.VERSION_PREFIX,
1339                host__hostname=self.hostname)
1340        mismatch_found = False
1341        if labels:
1342            # Get CHROMEOS_RELEASE_VERSION from lsb-release, e.g., 6908.0.0.
1343            # Note that it's different from cros-version label, which has
1344            # builder and branch info, e.g.,
1345            # cros-version:peppy-release/R43-6908.0.0
1346            release_version = self.get_release_version()
1347            host_list = [self.hostname]
1348            for label in labels:
1349                # Remove any cros-version label that does not match
1350                # release_version.
1351                build_version = label.name[len(ds_constants.VERSION_PREFIX):]
1352                if not utils.version_match(build_version, release_version):
1353                    logging.warn('cros-version label "%s" does not match '
1354                                 'release version %s. Removing the label.',
1355                                 label.name, release_version)
1356                    label.remove_hosts(hosts=host_list)
1357                    mismatch_found = True
1358        if mismatch_found:
1359            autotest_es.post(use_http=True,
1360                             type_str='cros_version_label_mismatch',
1361                             metadata={'hostname': self.hostname})
1362            raise error.AutoservError('The host has wrong cros-version label.')
1363
1364
1365    def cleanup(self):
1366        self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
1367        try:
1368            self._restart_ui()
1369        except (error.AutotestRunError, error.AutoservRunError,
1370                FactoryImageCheckerException):
1371            logging.warning('Unable to restart ui, rebooting device.')
1372            # Since restarting the UI fails fall back to normal Autotest
1373            # cleanup routines, i.e. reboot the machine.
1374            super(CrosHost, self).cleanup()
1375        # Check if the rpm outlet was manipulated.
1376        if self.has_power():
1377            self._cleanup_poweron()
1378        self.verify_cros_version_label()
1379
1380
1381    def reboot(self, **dargs):
1382        """
1383        This function reboots the site host. The more generic
1384        RemoteHost.reboot() performs sync and sleeps for 5
1385        seconds. This is not necessary for Chrome OS devices as the
1386        sync should be finished in a short time during the reboot
1387        command.
1388        """
1389        if 'reboot_cmd' not in dargs:
1390            reboot_timeout = dargs.get('reboot_timeout', 10)
1391            dargs['reboot_cmd'] = ('sleep 1; '
1392                                   'reboot & sleep %d; '
1393                                   'reboot -f' % reboot_timeout)
1394        # Enable fastsync to avoid running extra sync commands before reboot.
1395        if 'fastsync' not in dargs:
1396            dargs['fastsync'] = True
1397
1398        # For purposes of logging reboot times:
1399        # Get the board name i.e. 'daisy_spring'
1400        board_fullname = self.get_board()
1401
1402        # Strip the prefix and add it to dargs.
1403        dargs['board'] = board_fullname[board_fullname.find(':')+1:]
1404        # Record who called us
1405        orig = sys._getframe(1).f_code
1406        metric_fields = {'board' : dargs['board'],
1407                         'dut_host_name' : self.hostname,
1408                         'success' : True}
1409        metric_debug_fields = {'board' : dargs['board'],
1410                               'caller' : "%s:%s" % (orig.co_filename, orig.co_name),
1411                               'success' : True,
1412                               'error' : ''}
1413
1414        t0 = time.time()
1415        try:
1416            super(CrosHost, self).reboot(**dargs)
1417        except Exception as e:
1418            metric_fields['success'] = False
1419            metric_debug_fields['success'] = False
1420            metric_debug_fields['error'] = type(e).__name__
1421            raise
1422        finally:
1423            duration = int(time.time() - t0)
1424            metrics.Counter(
1425                    'chromeos/autotest/autoserv/reboot_count').increment(
1426                    fields=metric_fields)
1427            metrics.Counter(
1428                    'chromeos/autotest/autoserv/reboot_debug').increment(
1429                    fields=metric_debug_fields)
1430            metrics.SecondsDistribution(
1431                    'chromeos/autotest/autoserv/reboot_duration').add(
1432                    duration, fields=metric_fields)
1433
1434
1435    def suspend(self, **dargs):
1436        """
1437        This function suspends the site host.
1438        """
1439        suspend_time = dargs.get('suspend_time', 60)
1440        dargs['timeout'] = suspend_time
1441        if 'suspend_cmd' not in dargs:
1442            dargs['suspend_cmd'] = ' && '.join([
1443                'echo 0 > /sys/class/rtc/rtc0/wakealarm',
1444                'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
1445                'powerd_dbus_suspend --delay=0'])
1446        super(CrosHost, self).suspend(**dargs)
1447
1448
1449    def upstart_status(self, service_name):
1450        """Check the status of an upstart init script.
1451
1452        @param service_name: Service to look up.
1453
1454        @returns True if the service is running, False otherwise.
1455        """
1456        return self.run('status %s | grep start/running' %
1457                        service_name).stdout.strip() != ''
1458
1459
1460    def verify_software(self):
1461        """Verify working software on a Chrome OS system.
1462
1463        Tests for the following conditions:
1464         1. All conditions tested by the parent version of this
1465            function.
1466         2. Sufficient space in /mnt/stateful_partition.
1467         3. Sufficient space in /mnt/stateful_partition/encrypted.
1468         4. update_engine answers a simple status request over DBus.
1469
1470        """
1471        super(CrosHost, self).verify_software()
1472        default_kilo_inodes_required = CONFIG.get_config_value(
1473                'SERVER', 'kilo_inodes_required', type=int, default=100)
1474        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
1475        kilo_inodes_required = CONFIG.get_config_value(
1476                'SERVER', 'kilo_inodes_required_%s' % board,
1477                type=int, default=default_kilo_inodes_required)
1478        self.check_inodes('/mnt/stateful_partition', kilo_inodes_required)
1479        self.check_diskspace(
1480            '/mnt/stateful_partition',
1481            CONFIG.get_config_value(
1482                'SERVER', 'gb_diskspace_required', type=float,
1483                default=20.0))
1484        encrypted_stateful_path = '/mnt/stateful_partition/encrypted'
1485        # Not all targets build with encrypted stateful support.
1486        if self.path_exists(encrypted_stateful_path):
1487            self.check_diskspace(
1488                encrypted_stateful_path,
1489                CONFIG.get_config_value(
1490                    'SERVER', 'gb_encrypted_diskspace_required', type=float,
1491                    default=0.1))
1492
1493        if not self.upstart_status('system-services'):
1494            raise error.AutoservError('Chrome failed to reach login. '
1495                                      'System services not running.')
1496
1497        # Factory images don't run update engine,
1498        # goofy controls dbus on these DUTs.
1499        if not self._is_factory_image():
1500            self.run('update_engine_client --status')
1501
1502        self.verify_cros_version_label()
1503
1504
1505    def verify(self):
1506        self._repair_strategy.verify(self)
1507
1508
1509    def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
1510                         connect_timeout=None, alive_interval=None):
1511        """Override default make_ssh_command to use options tuned for Chrome OS.
1512
1513        Tuning changes:
1514          - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
1515          connection failure.  Consistency with remote_access.sh.
1516
1517          - ServerAliveInterval=900; which causes SSH to ping connection every
1518          900 seconds. In conjunction with ServerAliveCountMax ensures
1519          that if the connection dies, Autotest will bail out.
1520          Originally tried 60 secs, but saw frequent job ABORTS where
1521          the test completed successfully. Later increased from 180 seconds to
1522          900 seconds to account for tests where the DUT is suspended for
1523          longer periods of time.
1524
1525          - ServerAliveCountMax=3; consistency with remote_access.sh.
1526
1527          - ConnectAttempts=4; reduce flakiness in connection errors;
1528          consistency with remote_access.sh.
1529
1530          - UserKnownHostsFile=/dev/null; we don't care about the keys.
1531          Host keys change with every new installation, don't waste
1532          memory/space saving them.
1533
1534          - SSH protocol forced to 2; needed for ServerAliveInterval.
1535
1536        @param user User name to use for the ssh connection.
1537        @param port Port on the target host to use for ssh connection.
1538        @param opts Additional options to the ssh command.
1539        @param hosts_file Ignored.
1540        @param connect_timeout Ignored.
1541        @param alive_interval Ignored.
1542        """
1543        base_command = ('/usr/bin/ssh -a -x %s %s %s'
1544                        ' -o StrictHostKeyChecking=no'
1545                        ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
1546                        ' -o ConnectTimeout=30 -o ServerAliveInterval=900'
1547                        ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
1548                        ' -o Protocol=2 -l %s -p %d')
1549        return base_command % (self._ssh_verbosity_flag, self._ssh_options,
1550                               opts, user, port)
1551    def syslog(self, message, tag='autotest'):
1552        """Logs a message to syslog on host.
1553
1554        @param message String message to log into syslog
1555        @param tag String tag prefix for syslog
1556
1557        """
1558        self.run('logger -t "%s" "%s"' % (tag, message))
1559
1560
1561    def _ping_check_status(self, status):
1562        """Ping the host once, and return whether it has a given status.
1563
1564        @param status Check the ping status against this value.
1565        @return True iff `status` and the result of ping are the same
1566                (i.e. both True or both False).
1567
1568        """
1569        ping_val = utils.ping(self.hostname, tries=1, deadline=1)
1570        return not (status ^ (ping_val == 0))
1571
1572    def _ping_wait_for_status(self, status, timeout):
1573        """Wait for the host to have a given status (UP or DOWN).
1574
1575        Status is checked by polling.  Polling will not last longer
1576        than the number of seconds in `timeout`.  The polling
1577        interval will be long enough that only approximately
1578        _PING_WAIT_COUNT polling cycles will be executed, subject
1579        to a maximum interval of about one minute.
1580
1581        @param status Waiting will stop immediately if `ping` of the
1582                      host returns this status.
1583        @param timeout Poll for at most this many seconds.
1584        @return True iff the host status from `ping` matched the
1585                requested status at the time of return.
1586
1587        """
1588        # _ping_check_status() takes about 1 second, hence the
1589        # "- 1" in the formula below.
1590        # FIXME: if the ping command errors then _ping_check_status()
1591        # returns instantly. If timeout is also smaller than twice
1592        # _PING_WAIT_COUNT then the while loop below forks many
1593        # thousands of ping commands (see /tmp/test_that_results_XXXXX/
1594        # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one
1595        # CPU core for 60 seconds.
1596        poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1
1597        end_time = time.time() + timeout
1598        while time.time() <= end_time:
1599            if self._ping_check_status(status):
1600                return True
1601            if poll_interval > 0:
1602                time.sleep(poll_interval)
1603
1604        # The last thing we did was sleep(poll_interval), so it may
1605        # have been too long since the last `ping`.  Check one more
1606        # time, just to be sure.
1607        return self._ping_check_status(status)
1608
1609    def ping_wait_up(self, timeout):
1610        """Wait for the host to respond to `ping`.
1611
1612        N.B.  This method is not a reliable substitute for
1613        `wait_up()`, because a host that responds to ping will not
1614        necessarily respond to ssh.  This method should only be used
1615        if the target DUT can be considered functional even if it
1616        can't be reached via ssh.
1617
1618        @param timeout Minimum time to allow before declaring the
1619                       host to be non-responsive.
1620        @return True iff the host answered to ping before the timeout.
1621
1622        """
1623        return self._ping_wait_for_status(self._PING_STATUS_UP, timeout)
1624
1625    def ping_wait_down(self, timeout):
1626        """Wait until the host no longer responds to `ping`.
1627
1628        This function can be used as a slightly faster version of
1629        `wait_down()`, by avoiding potentially long ssh timeouts.
1630
1631        @param timeout Minimum time to allow for the host to become
1632                       non-responsive.
1633        @return True iff the host quit answering ping before the
1634                timeout.
1635
1636        """
1637        return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout)
1638
1639    def test_wait_for_sleep(self, sleep_timeout=None):
1640        """Wait for the client to enter low-power sleep mode.
1641
1642        The test for "is asleep" can't distinguish a system that is
1643        powered off; to confirm that the unit was asleep, it is
1644        necessary to force resume, and then call
1645        `test_wait_for_resume()`.
1646
1647        This function is expected to be called from a test as part
1648        of a sequence like the following:
1649
1650        ~~~~~~~~
1651            boot_id = host.get_boot_id()
1652            # trigger sleep on the host
1653            host.test_wait_for_sleep()
1654            # trigger resume on the host
1655            host.test_wait_for_resume(boot_id)
1656        ~~~~~~~~
1657
1658        @param sleep_timeout time limit in seconds to allow the host sleep.
1659
1660        @exception TestFail The host did not go to sleep within
1661                            the allowed time.
1662        """
1663        if sleep_timeout is None:
1664            sleep_timeout = self.SLEEP_TIMEOUT
1665
1666        if not self.ping_wait_down(timeout=sleep_timeout):
1667            raise error.TestFail(
1668                'client failed to sleep after %d seconds' % sleep_timeout)
1669
1670
1671    def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
1672        """Wait for the client to resume from low-power sleep mode.
1673
1674        The `old_boot_id` parameter should be the value from
1675        `get_boot_id()` obtained prior to entering sleep mode.  A
1676        `TestFail` exception is raised if the boot id changes.
1677
1678        See @ref test_wait_for_sleep for more on this function's
1679        usage.
1680
1681        @param old_boot_id A boot id value obtained before the
1682                               target host went to sleep.
1683        @param resume_timeout time limit in seconds to allow the host up.
1684
1685        @exception TestFail The host did not respond within the
1686                            allowed time.
1687        @exception TestFail The host responded, but the boot id test
1688                            indicated a reboot rather than a sleep
1689                            cycle.
1690        """
1691        if resume_timeout is None:
1692            resume_timeout = self.RESUME_TIMEOUT
1693
1694        if not self.wait_up(timeout=resume_timeout):
1695            raise error.TestFail(
1696                'client failed to resume from sleep after %d seconds' %
1697                    resume_timeout)
1698        else:
1699            new_boot_id = self.get_boot_id()
1700            if new_boot_id != old_boot_id:
1701                logging.error('client rebooted (old boot %s, new boot %s)',
1702                              old_boot_id, new_boot_id)
1703                raise error.TestFail(
1704                    'client rebooted, but sleep was expected')
1705
1706
1707    def test_wait_for_shutdown(self, shutdown_timeout=None):
1708        """Wait for the client to shut down.
1709
1710        The test for "has shut down" can't distinguish a system that
1711        is merely asleep; to confirm that the unit was down, it is
1712        necessary to force boot, and then call test_wait_for_boot().
1713
1714        This function is expected to be called from a test as part
1715        of a sequence like the following:
1716
1717        ~~~~~~~~
1718            boot_id = host.get_boot_id()
1719            # trigger shutdown on the host
1720            host.test_wait_for_shutdown()
1721            # trigger boot on the host
1722            host.test_wait_for_boot(boot_id)
1723        ~~~~~~~~
1724
1725        @param shutdown_timeout time limit in seconds to allow the host down.
1726        @exception TestFail The host did not shut down within the
1727                            allowed time.
1728        """
1729        if shutdown_timeout is None:
1730            shutdown_timeout = self.SHUTDOWN_TIMEOUT
1731
1732        if not self.ping_wait_down(timeout=shutdown_timeout):
1733            raise error.TestFail(
1734                'client failed to shut down after %d seconds' %
1735                    shutdown_timeout)
1736
1737
1738    def test_wait_for_boot(self, old_boot_id=None):
1739        """Wait for the client to boot from cold power.
1740
1741        The `old_boot_id` parameter should be the value from
1742        `get_boot_id()` obtained prior to shutting down.  A
1743        `TestFail` exception is raised if the boot id does not
1744        change.  The boot id test is omitted if `old_boot_id` is not
1745        specified.
1746
1747        See @ref test_wait_for_shutdown for more on this function's
1748        usage.
1749
1750        @param old_boot_id A boot id value obtained before the
1751                               shut down.
1752
1753        @exception TestFail The host did not respond within the
1754                            allowed time.
1755        @exception TestFail The host responded, but the boot id test
1756                            indicated that there was no reboot.
1757        """
1758        if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
1759            raise error.TestFail(
1760                'client failed to reboot after %d seconds' %
1761                    self.REBOOT_TIMEOUT)
1762        elif old_boot_id:
1763            if self.get_boot_id() == old_boot_id:
1764                logging.error('client not rebooted (boot %s)',
1765                              old_boot_id)
1766                raise error.TestFail(
1767                    'client is back up, but did not reboot')
1768
1769
1770    @staticmethod
1771    def check_for_rpm_support(hostname):
1772        """For a given hostname, return whether or not it is powered by an RPM.
1773
1774        @param hostname: hostname to check for rpm support.
1775
1776        @return None if this host does not follows the defined naming format
1777                for RPM powered DUT's in the lab. If it does follow the format,
1778                it returns a regular expression MatchObject instead.
1779        """
1780        return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
1781
1782
1783    def has_power(self):
1784        """For this host, return whether or not it is powered by an RPM.
1785
1786        @return True if this host is in the CROS lab and follows the defined
1787                naming format.
1788        """
1789        return CrosHost.check_for_rpm_support(self.hostname)
1790
1791
1792    def _set_power(self, state, power_method):
1793        """Sets the power to the host via RPM, Servo or manual.
1794
1795        @param state Specifies which power state to set to DUT
1796        @param power_method Specifies which method of power control to
1797                            use. By default "RPM" will be used. Valid values
1798                            are the strings "RPM", "manual", "servoj10".
1799
1800        """
1801        ACCEPTABLE_STATES = ['ON', 'OFF']
1802
1803        if state.upper() not in ACCEPTABLE_STATES:
1804            raise error.TestError('State must be one of: %s.'
1805                                   % (ACCEPTABLE_STATES,))
1806
1807        if power_method == self.POWER_CONTROL_SERVO:
1808            logging.info('Setting servo port J10 to %s', state)
1809            self.servo.set('prtctl3_pwren', state.lower())
1810            time.sleep(self._USB_POWER_TIMEOUT)
1811        elif power_method == self.POWER_CONTROL_MANUAL:
1812            logging.info('You have %d seconds to set the AC power to %s.',
1813                         self._POWER_CYCLE_TIMEOUT, state)
1814            time.sleep(self._POWER_CYCLE_TIMEOUT)
1815        else:
1816            if not self.has_power():
1817                raise error.TestFail('DUT does not have RPM connected.')
1818            afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
1819            afe.set_host_attribute(self._RPM_OUTLET_CHANGED, True,
1820                                   hostname=self.hostname)
1821            rpm_client.set_power(self.hostname, state.upper(), timeout_mins=5)
1822
1823
1824    def power_off(self, power_method=POWER_CONTROL_RPM):
1825        """Turn off power to this host via RPM, Servo or manual.
1826
1827        @param power_method Specifies which method of power control to
1828                            use. By default "RPM" will be used. Valid values
1829                            are the strings "RPM", "manual", "servoj10".
1830
1831        """
1832        self._set_power('OFF', power_method)
1833
1834
1835    def power_on(self, power_method=POWER_CONTROL_RPM):
1836        """Turn on power to this host via RPM, Servo or manual.
1837
1838        @param power_method Specifies which method of power control to
1839                            use. By default "RPM" will be used. Valid values
1840                            are the strings "RPM", "manual", "servoj10".
1841
1842        """
1843        self._set_power('ON', power_method)
1844
1845
1846    def power_cycle(self, power_method=POWER_CONTROL_RPM):
1847        """Cycle power to this host by turning it OFF, then ON.
1848
1849        @param power_method Specifies which method of power control to
1850                            use. By default "RPM" will be used. Valid values
1851                            are the strings "RPM", "manual", "servoj10".
1852
1853        """
1854        if power_method in (self.POWER_CONTROL_SERVO,
1855                            self.POWER_CONTROL_MANUAL):
1856            self.power_off(power_method=power_method)
1857            time.sleep(self._POWER_CYCLE_TIMEOUT)
1858            self.power_on(power_method=power_method)
1859        else:
1860            rpm_client.set_power(self.hostname, 'CYCLE')
1861
1862
1863    def get_platform(self):
1864        """Determine the correct platform label for this host.
1865
1866        @returns a string representing this host's platform.
1867        """
1868        crossystem = utils.Crossystem(self)
1869        crossystem.init()
1870        # Extract fwid value and use the leading part as the platform id.
1871        # fwid generally follow the format of {platform}.{firmware version}
1872        # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z
1873        platform = crossystem.fwid().split('.')[0].lower()
1874        # Newer platforms start with 'Google_' while the older ones do not.
1875        return platform.replace('google_', '')
1876
1877
1878    def get_architecture(self):
1879        """Determine the correct architecture label for this host.
1880
1881        @returns a string representing this host's architecture.
1882        """
1883        crossystem = utils.Crossystem(self)
1884        crossystem.init()
1885        return crossystem.arch()
1886
1887
1888    def get_chrome_version(self):
1889        """Gets the Chrome version number and milestone as strings.
1890
1891        Invokes "chrome --version" to get the version number and milestone.
1892
1893        @return A tuple (chrome_ver, milestone) where "chrome_ver" is the
1894            current Chrome version number as a string (in the form "W.X.Y.Z")
1895            and "milestone" is the first component of the version number
1896            (the "W" from "W.X.Y.Z").  If the version number cannot be parsed
1897            in the "W.X.Y.Z" format, the "chrome_ver" will be the full output
1898            of "chrome --version" and the milestone will be the empty string.
1899
1900        """
1901        version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout
1902        return utils.parse_chrome_version(version_string)
1903
1904
1905    # TODO(kevcheng): change this to just return the board without the
1906    # 'board:' prefix and fix up all the callers.  Also look into removing the
1907    # need for this method.
1908    def get_board(self):
1909        """Determine the correct board label for this host.
1910
1911        @returns a string representing this host's board.
1912        """
1913        release_info = utils.parse_cmd_output('cat /etc/lsb-release',
1914                                              run_method=self.run)
1915        return (ds_constants.BOARD_PREFIX +
1916                release_info['CHROMEOS_RELEASE_BOARD'])
1917
1918
1919
1920    def has_lightsensor(self):
1921        """Determine the correct board label for this host.
1922
1923        @returns the string 'lightsensor' if this host has a lightsensor or
1924                 None if it does not.
1925        """
1926        search_cmd = "find -L %s -maxdepth 4 | egrep '%s'" % (
1927            self._LIGHTSENSOR_SEARCH_DIR, '|'.join(self._LIGHTSENSOR_FILES))
1928        try:
1929            # Run the search cmd following the symlinks. Stderr_tee is set to
1930            # None as there can be a symlink loop, but the command will still
1931            # execute correctly with a few messages printed to stderr.
1932            self.run(search_cmd, stdout_tee=None, stderr_tee=None)
1933            return 'lightsensor'
1934        except error.AutoservRunError:
1935            # egrep exited with a return code of 1 meaning none of the possible
1936            # lightsensor files existed.
1937            return None
1938
1939
1940    def has_bluetooth(self):
1941        """Determine the correct board label for this host.
1942
1943        @returns the string 'bluetooth' if this host has bluetooth or
1944                 None if it does not.
1945        """
1946        try:
1947            self.run('test -d /sys/class/bluetooth/hci0')
1948            # test exited with a return code of 0.
1949            return 'bluetooth'
1950        except error.AutoservRunError:
1951            # test exited with a return code 1 meaning the directory did not
1952            # exist.
1953            return None
1954
1955
1956    def get_accels(self):
1957        """
1958        Determine the type of accelerometers on this host.
1959
1960        @returns a string representing this host's accelerometer type.
1961        At present, it only returns "accel:cros-ec", for accelerometers
1962        attached to a Chrome OS EC, or none, if no accelerometers.
1963        """
1964        # Check to make sure we have ectool
1965        rv = self.run('which ectool', ignore_status=True)
1966        if rv.exit_status:
1967            logging.info("No ectool cmd found, assuming no EC accelerometers")
1968            return None
1969
1970        # Check that the EC supports the motionsense command
1971        rv = self.run('ectool motionsense', ignore_status=True)
1972        if rv.exit_status:
1973            logging.info("EC does not support motionsense command "
1974                         "assuming no EC accelerometers")
1975            return None
1976
1977        # Check that EC motion sensors are active
1978        active = self.run('ectool motionsense active').stdout.split('\n')
1979        if active[0] == "0":
1980            logging.info("Motion sense inactive, assuming no EC accelerometers")
1981            return None
1982
1983        logging.info("EC accelerometers found")
1984        return 'accel:cros-ec'
1985
1986
1987    def has_chameleon(self):
1988        """Determine if a Chameleon connected to this host.
1989
1990        @returns a list containing two strings ('chameleon' and
1991                 'chameleon:' + label, e.g. 'chameleon:hdmi') if this host
1992                 has a Chameleon or None if it has not.
1993        """
1994        if self._chameleon_host:
1995            return ['chameleon', 'chameleon:' + self.chameleon.get_label()]
1996        else:
1997            return None
1998
1999
2000    def has_loopback_dongle(self):
2001        """Determine if an audio loopback dongle is plugged to this host.
2002
2003        @returns 'audio_loopback_dongle' when there is an audio loopback dongle
2004                                         plugged to this host.
2005                 None                    when there is no audio loopback dongle
2006                                         plugged to this host.
2007        """
2008        nodes_info = self.run(command=cras_utils.get_cras_nodes_cmd(),
2009                              ignore_status=True).stdout
2010        if (cras_utils.node_type_is_plugged('HEADPHONE', nodes_info) and
2011            cras_utils.node_type_is_plugged('MIC', nodes_info)):
2012                return 'audio_loopback_dongle'
2013        else:
2014                return None
2015
2016
2017    def get_power_supply(self):
2018        """
2019        Determine what type of power supply the host has
2020
2021        @returns a string representing this host's power supply.
2022                 'power:battery' when the device has a battery intended for
2023                        extended use
2024                 'power:AC_primary' when the device has a battery not intended
2025                        for extended use (for moving the machine, etc)
2026                 'power:AC_only' when the device has no battery at all.
2027        """
2028        psu = self.run(command='mosys psu type', ignore_status=True)
2029        if psu.exit_status:
2030            # The psu command for mosys is not included for all platforms. The
2031            # assumption is that the device will have a battery if the command
2032            # is not found.
2033            return 'power:battery'
2034
2035        psu_str = psu.stdout.strip()
2036        if psu_str == 'unknown':
2037            return None
2038
2039        return 'power:%s' % psu_str
2040
2041
2042    def get_storage(self):
2043        """
2044        Determine the type of boot device for this host.
2045
2046        Determine if the internal device is SCSI or dw_mmc device.
2047        Then check that it is SSD or HDD or eMMC or something else.
2048
2049        @returns a string representing this host's internal device type.
2050                 'storage:ssd' when internal device is solid state drive
2051                 'storage:hdd' when internal device is hard disk drive
2052                 'storage:mmc' when internal device is mmc drive
2053                 None          When internal device is something else or
2054                               when we are unable to determine the type
2055        """
2056        # The output should be /dev/mmcblk* for SD/eMMC or /dev/sd* for scsi
2057        rootdev_cmd = ' '.join(['. /usr/sbin/write_gpt.sh;',
2058                                '. /usr/share/misc/chromeos-common.sh;',
2059                                'load_base_vars;',
2060                                'get_fixed_dst_drive'])
2061        rootdev = self.run(command=rootdev_cmd, ignore_status=True)
2062        if rootdev.exit_status:
2063            logging.info("Fail to run %s", rootdev_cmd)
2064            return None
2065        rootdev_str = rootdev.stdout.strip()
2066
2067        if not rootdev_str:
2068            return None
2069
2070        rootdev_base = os.path.basename(rootdev_str)
2071
2072        mmc_pattern = '/dev/mmcblk[0-9]'
2073        if re.match(mmc_pattern, rootdev_str):
2074            # Use type to determine if the internal device is eMMC or somthing
2075            # else. We can assume that MMC is always an internal device.
2076            type_cmd = 'cat /sys/block/%s/device/type' % rootdev_base
2077            type = self.run(command=type_cmd, ignore_status=True)
2078            if type.exit_status:
2079                logging.info("Fail to run %s", type_cmd)
2080                return None
2081            type_str = type.stdout.strip()
2082
2083            if type_str == 'MMC':
2084                return 'storage:mmc'
2085
2086        scsi_pattern = '/dev/sd[a-z]+'
2087        if re.match(scsi_pattern, rootdev.stdout):
2088            # Read symlink for /sys/block/sd* to determine if the internal
2089            # device is connected via ata or usb.
2090            link_cmd = 'readlink /sys/block/%s' % rootdev_base
2091            link = self.run(command=link_cmd, ignore_status=True)
2092            if link.exit_status:
2093                logging.info("Fail to run %s", link_cmd)
2094                return None
2095            link_str = link.stdout.strip()
2096            if 'usb' in link_str:
2097                return None
2098
2099            # Read rotation to determine if the internal device is ssd or hdd.
2100            rotate_cmd = str('cat /sys/block/%s/queue/rotational'
2101                              % rootdev_base)
2102            rotate = self.run(command=rotate_cmd, ignore_status=True)
2103            if rotate.exit_status:
2104                logging.info("Fail to run %s", rotate_cmd)
2105                return None
2106            rotate_str = rotate.stdout.strip()
2107
2108            rotate_dict = {'0':'storage:ssd', '1':'storage:hdd'}
2109            return rotate_dict.get(rotate_str)
2110
2111        # All other internal device / error case will always fall here
2112        return None
2113
2114
2115    def get_servo(self):
2116        """Determine if the host has a servo attached.
2117
2118        If the host has a working servo attached, it should have a servo label.
2119
2120        @return: string 'servo' if the host has servo attached. Otherwise,
2121                 returns None.
2122        """
2123        return 'servo' if self._servo_host else None
2124
2125
2126    def get_video_labels(self):
2127        """Run /usr/local/bin/avtest_label_detect to get a list of video labels.
2128
2129        Sample output of avtest_label_detect:
2130        Detected label: hw_video_acc_vp8
2131        Detected label: webcam
2132
2133        @return: A list of labels detected by tool avtest_label_detect.
2134        """
2135        try:
2136            result = self.run('/usr/local/bin/avtest_label_detect').stdout
2137            return re.findall('^Detected label: (\w+)$', result, re.M)
2138        except error.AutoservRunError:
2139            # The tool is not installed.
2140            return []
2141
2142
2143    def is_video_glitch_detection_supported(self):
2144        """ Determine if a board under test is supported for video glitch
2145        detection tests.
2146
2147        @return: 'video_glitch_detection' if board is supported, None otherwise.
2148        """
2149        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
2150
2151        if board in video_test_constants.SUPPORTED_BOARDS:
2152            return 'video_glitch_detection'
2153
2154        return None
2155
2156
2157    def get_touch(self):
2158        """
2159        Determine whether board under test has a touchpad or touchscreen.
2160
2161        @return: A list of some combination of 'touchscreen' and 'touchpad',
2162            depending on what is present on the device.
2163
2164        """
2165        labels = []
2166        looking_for = ['touchpad', 'touchscreen']
2167        player = input_playback.InputPlayback()
2168        input_events = self.run('ls /dev/input/event*').stdout.strip().split()
2169        filename = '/tmp/touch_labels'
2170        for event in input_events:
2171            self.run('evtest %s > %s' % (event, filename), timeout=1,
2172                     ignore_timeout=True)
2173            properties = self.run('cat %s' % filename).stdout
2174            input_type = player._determine_input_type(properties)
2175            if input_type in looking_for:
2176                labels.append(input_type)
2177                looking_for.remove(input_type)
2178            if len(looking_for) == 0:
2179                break
2180        self.run('rm %s' % filename)
2181
2182        return labels
2183
2184
2185    def has_internal_display(self):
2186        """Determine if the device under test is equipped with an internal
2187        display.
2188
2189        @return: 'internal_display' if one is present; None otherwise.
2190        """
2191        from autotest_lib.client.cros.graphics import graphics_utils
2192        from autotest_lib.client.common_lib import utils as common_utils
2193
2194        def __system_output(cmd):
2195            return self.run(cmd).stdout
2196
2197        def __read_file(remote_path):
2198            return self.run('cat %s' % remote_path).stdout
2199
2200        # Hijack the necessary client functions so that we can take advantage
2201        # of the client lib here.
2202        # FIXME: find a less hacky way than this
2203        original_system_output = utils.system_output
2204        original_read_file = common_utils.read_file
2205        utils.system_output = __system_output
2206        common_utils.read_file = __read_file
2207        try:
2208            return ('internal_display' if graphics_utils.has_internal_display()
2209                                   else None)
2210        finally:
2211            utils.system_output = original_system_output
2212            common_utils.read_file = original_read_file
2213
2214
2215    def has_lucid_sleep_support(self):
2216        """Determine if the device under test has support for lucid sleep.
2217
2218        @return 'lucidsleep' if this board supports lucid sleep; None otherwise
2219        """
2220        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
2221        return 'lucidsleep' if board in LUCID_SLEEP_BOARDS else None
2222
2223
2224    def is_boot_from_usb(self):
2225        """Check if DUT is boot from USB.
2226
2227        @return: True if DUT is boot from usb.
2228        """
2229        device = self.run('rootdev -s -d').stdout.strip()
2230        removable = int(self.run('cat /sys/block/%s/removable' %
2231                                 os.path.basename(device)).stdout.strip())
2232        return removable == 1
2233
2234
2235    def read_from_meminfo(self, key):
2236        """Return the memory info from /proc/meminfo
2237
2238        @param key: meminfo requested
2239
2240        @return the memory value as a string
2241
2242        """
2243        meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip()
2244        logging.debug('%s', meminfo)
2245        return int(re.search(r'\d+', meminfo).group(0))
2246
2247
2248    def get_cpu_arch(self):
2249        """Returns CPU arch of the device.
2250
2251        @return CPU architecture of the DUT.
2252        """
2253        # Add CPUs by following logic in client/bin/base_utils.py.
2254        if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo",
2255                ignore_status=True).stdout:
2256            return 'x86_64'
2257        if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo",
2258                ignore_status=True).stdout:
2259            return 'arm'
2260        return 'i386'
2261
2262
2263    def get_board_type(self):
2264        """
2265        Get the DUT's device type from /etc/lsb-release.
2266        DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more.
2267
2268        @return value of DEVICETYPE param from lsb-release.
2269        """
2270        device_type = self.run('grep DEVICETYPE /etc/lsb-release',
2271                               ignore_status=True).stdout
2272        if device_type:
2273            return device_type.split('=')[-1].strip()
2274        return ''
2275
2276
2277    def get_os_type(self):
2278        return 'cros'
2279
2280
2281    def enable_adb_testing(self):
2282        """Mark this host as an adb tester."""
2283        self.run('touch %s' % constants.ANDROID_TESTER_FILEFLAG)
2284
2285
2286    def get_labels(self):
2287        """Return the detected labels on the host."""
2288        return self.labels.get_labels(self)
2289
2290
2291    def update_labels(self):
2292        """Update the labels on the host."""
2293        self.labels.update_labels(self)
2294