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