• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9from io import StringIO
10import json
11
12import logging
13import os
14import re
15import sys
16import six
17import time
18
19import common
20from autotest_lib.client.bin import utils
21from autotest_lib.client.common_lib import autotemp
22from autotest_lib.client.common_lib import error
23from autotest_lib.client.common_lib import global_config
24from autotest_lib.client.common_lib import hosts
25from autotest_lib.client.common_lib import lsbrelease_utils
26from autotest_lib.client.common_lib import utils as common_utils
27from autotest_lib.client.common_lib.cros import cros_config
28from autotest_lib.client.common_lib.cros import dev_server
29from autotest_lib.client.common_lib.cros import retry
30from autotest_lib.client.cros import constants as client_constants
31from autotest_lib.client.cros import cros_ui
32from autotest_lib.server import afe_utils
33from autotest_lib.server import utils as server_utils
34from autotest_lib.server.cros import provision
35from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
36from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers
37from autotest_lib.server.cros.device_health_profile import device_health_profile
38from autotest_lib.server.cros.device_health_profile import profile_constants
39from autotest_lib.server.cros.servo import pdtester
40from autotest_lib.server.hosts import abstract_ssh
41from autotest_lib.server.hosts import base_label
42from autotest_lib.server.hosts import chameleon_host
43from autotest_lib.server.hosts import cros_constants
44from autotest_lib.server.hosts import cros_label
45from autotest_lib.server.hosts import cros_repair
46from autotest_lib.server.hosts import pdtester_host
47from autotest_lib.server.hosts import servo_host
48from autotest_lib.server.hosts import servo_constants
49from autotest_lib.site_utils.rpm_control_system import rpm_client
50from autotest_lib.site_utils.admin_audit import constants as audit_const
51from autotest_lib.site_utils.admin_audit import verifiers as audit_verify
52from six.moves import zip
53
54
55# In case cros_host is being ran via SSP on an older Moblab version with an
56# older chromite version.
57try:
58    from autotest_lib.utils.frozen_chromite.lib import metrics
59except ImportError:
60    metrics = utils.metrics_mock
61
62
63CONFIG = global_config.global_config
64
65class FactoryImageCheckerException(error.AutoservError):
66    """Exception raised when an image is a factory image."""
67    pass
68
69
70class CrosHost(abstract_ssh.AbstractSSHHost):
71    """Chromium OS specific subclass of Host."""
72
73    VERSION_PREFIX = provision.CROS_VERSION_PREFIX
74
75    _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
76
77    # Timeout values (in seconds) associated with various ChromeOS
78    # state changes.
79    #
80    # In general, a good rule of thumb is that the timeout can be up
81    # to twice the typical measured value on the slowest platform.
82    # The times here have not necessarily been empirically tested to
83    # meet this criterion.
84    #
85    # SLEEP_TIMEOUT:  Time to allow for suspend to memory.
86    # RESUME_TIMEOUT: Time to allow for resume after suspend, plus
87    #   time to restart the netwowrk.
88    # SHUTDOWN_TIMEOUT: Time to allow for shut down.
89    # BOOT_TIMEOUT: Time to allow for boot from power off.  Among
90    #   other things, this must account for the 30 second dev-mode
91    #   screen delay, time to start the network on the DUT, and the
92    #   ssh timeout of 120 seconds.
93    # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device,
94    #   including the 30 second dev-mode delay and time to start the
95    #   network.
96    # INSTALL_TIMEOUT: Time to allow for chromeos-install.
97    # ADMIN_INSTALL_TIMEOUT: Time to allow for chromeos-install
98    #   used by admin tasks.
99    # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that
100    #   includes powerwash.
101
102    SLEEP_TIMEOUT = 2
103    RESUME_TIMEOUT = 10
104    SHUTDOWN_TIMEOUT = 10
105    BOOT_TIMEOUT = 150
106    USB_BOOT_TIMEOUT = 300
107    INSTALL_TIMEOUT = 480
108    ADMIN_INSTALL_TIMEOUT = 600
109    POWERWASH_BOOT_TIMEOUT = 60
110    DEVSERVER_DOWNLOAD_TIMEOUT = 600
111
112    # Minimum OS version that supports server side packaging. Older builds may
113    # not have server side package built or with Autotest code change to support
114    # server-side packaging.
115    MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
116            'AUTOSERV', 'min_version_support_ssp', type=int)
117
118    USE_FSFREEZE = CONFIG.get_config_value(
119            'CROS', 'enable_fs_freeze', type=bool, default=False)
120
121    # REBOOT_TIMEOUT: How long to wait for a reboot.
122    #
123    # We have a long timeout to ensure we don't flakily fail due to other
124    # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
125    # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
126    # return from reboot' bug is solved.
127    REBOOT_TIMEOUT = 480
128
129    # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
130    # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
131    # _CHANGE_SERVO_ROLE_TIMEOUT: Time to allow DUT regain network connection
132    #                             since changing servo role will reset USB state
133    #                             and causes temporary ethernet drop.
134    _USB_POWER_TIMEOUT = 5
135    _POWER_CYCLE_TIMEOUT = 10
136    _CHANGE_SERVO_ROLE_TIMEOUT = 180
137
138    _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
139                           '-host(\d+)')
140
141    # Constants used in ping_wait_up() and ping_wait_down().
142    #
143    # _PING_WAIT_COUNT is the approximate number of polling
144    # cycles to use when waiting for a host state change.
145    #
146    # _PING_STATUS_DOWN and _PING_STATUS_UP are names used
147    # for arguments to the internal _ping_wait_for_status()
148    # method.
149    _PING_WAIT_COUNT = 40
150    _PING_STATUS_DOWN = False
151    _PING_STATUS_UP = True
152
153    # Allowed values for the power_method argument.
154
155    # POWER_CONTROL_RPM: Used in power_off/on/cycle() methods, default for all
156    #                    DUTs except those with servo_v4 CCD.
157    # POWER_CONTROL_CCD: Used in power_off/on/cycle() methods, default for all
158    #                    DUTs with servo_v4 CCD.
159    # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
160    # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
161    POWER_CONTROL_RPM = 'RPM'
162    POWER_CONTROL_CCD = 'CCD'
163    POWER_CONTROL_SERVO = 'servoj10'
164    POWER_CONTROL_MANUAL = 'manual'
165
166    POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
167                                POWER_CONTROL_CCD,
168                                POWER_CONTROL_SERVO,
169                                POWER_CONTROL_MANUAL)
170
171    _RPM_OUTLET_CHANGED = 'outlet_changed'
172
173    # URL pattern to download firmware image.
174    _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
175            'CROS', 'firmware_url_pattern', type=str)
176
177    # Regular expression for extracting EC version string
178    _EC_REGEX = '(%s_\w*[-\.]\w*[-\.]\w*[-\.]\w*)'
179
180    # Regular expression for extracting BIOS version string
181    _BIOS_REGEX = '(%s\.\w*\.\w*\.\w*)'
182
183    # Command to update firmware located on DUT
184    _FW_UPDATE_CMD = 'chromeos-firmwareupdate --mode=recovery %s'
185
186    @staticmethod
187    def check_host(host, timeout=10):
188        """
189        Check if the given host is a chrome-os host.
190
191        @param host: An ssh host representing a device.
192        @param timeout: The timeout for the run command.
193
194        @return: True if the host device is chromeos.
195
196        """
197        try:
198            result = host.run(
199                    'grep -q CHROMEOS /etc/lsb-release && '
200                    '! grep -q moblab /etc/lsb-release && '
201                    '! grep -q labstation /etc/lsb-release &&'
202                    ' grep CHROMEOS_RELEASE_BOARD /etc/lsb-release',
203                    ignore_status=True,
204                    timeout=timeout).stdout
205            if result:
206                return not (
207                    lsbrelease_utils.is_jetstream(
208                        lsb_release_content=result) or
209                    lsbrelease_utils.is_gce_board(
210                        lsb_release_content=result))
211
212        except (error.AutoservRunError, error.AutoservSSHTimeout):
213            return False
214
215        return False
216
217
218    @staticmethod
219    def get_chameleon_arguments(args_dict):
220        """Extract chameleon options from `args_dict` and return the result.
221
222        Recommended usage:
223        ~~~~~~~~
224            args_dict = utils.args_to_dict(args)
225            chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
226            host = hosts.create_host(machine, chameleon_args=chameleon_args)
227        ~~~~~~~~
228
229        @param args_dict Dictionary from which to extract the chameleon
230          arguments.
231        """
232        chameleon_args = {key: args_dict[key]
233                          for key in ('chameleon_host', 'chameleon_port')
234                          if key in args_dict}
235        if 'chameleon_ssh_port' in args_dict:
236            chameleon_args['port'] = int(args_dict['chameleon_ssh_port'])
237        return chameleon_args
238
239    @staticmethod
240    def get_btattenuator_arguments(args_dict):
241        """Extract btattenuator options from `args_dict` and return the result.
242
243        @param args_dict Dictionary from which to extract the btattenuator
244          arguments.
245        """
246        logging.debug("args dict in croshost is  %s", args_dict)
247        btattenuator_args = {
248                key: args_dict[key]
249                for key in ('btatten_addr', ) if key in args_dict
250        }
251
252        return btattenuator_args
253
254    @staticmethod
255    def get_btpeer_arguments(args_dict):
256        """Extract btpeer options from `args_dict` and return the result.
257
258        This is used to parse details of Bluetooth peer.
259        Recommended usage:
260        ~~~~~~~~
261            args_dict = utils.args_to_dict(args)
262            btpeer_args = hosts.CrosHost.get_btpeer_arguments(args_dict)
263            host = hosts.create_host(machine, btpeer_args=btpeer_args)
264        ~~~~~~~~
265
266        If btpeer_host_list is given, it should be a comma delimited list of
267            host:ssh_port/chameleon_port
268            127.0.0.1:22/9992
269
270        When using ipv6, wrap the host portion in square brackets:
271            [::1]:22/9992
272
273        Note: Only the host name is required. Both ports are optional.
274              If providing the chameleon port, note that you should provide an
275              unforwarded port (i.e. the port exposed on the actual dut).
276
277        @param args_dict: Dictionary from which to extract the btpeer
278          arguments.
279        """
280        if 'btpeer_host_list' in args_dict:
281            result = []
282            for btpeer in args_dict['btpeer_host_list'].split(','):
283                # IPv6 addresses including a port number should be enclosed in
284                # square brackets.
285                delimiter = ']:' if re.search(r':.*:', btpeer) else ':'
286
287                # Split into ip + ports
288                split = btpeer.strip('[]').split(delimiter)
289
290                # If ports are given, split into ssh + chameleon ports
291                if len(split) > 1:
292                    ports = split[1].split('/')
293                    split = [split[0]] + ports
294
295                result.append({
296                        key: value
297                        for key, value in zip(('btpeer_host',
298                                               'btpeer_ssh_port',
299                                               'btpeer_port'), split)
300                })
301            return result
302        else:
303            return {key: args_dict[key]
304                for key in ('btpeer_host', 'btpeer_port', 'btpeer_ssh_port')
305                if key in args_dict}
306
307
308    @staticmethod
309    def get_local_host_ip(args_dict):
310        """Ip address of DUT in the local LAN.
311
312        When using port forwarding during testing, the host ip is 127.0.0.1 and
313        can't be used by any peer devices (for example to scp). A local IP
314        should be given in this case so peripherals can access the DUT in the
315        local LAN.
316
317        The argument should be given with the key |local_host_ip|.
318
319        @params args_dict: Dictionary from which to extract the local host ip.
320        """
321        return {
322                key: args_dict[key]
323                for key in ('local_host_ip', ) if key in args_dict
324        }
325
326    @staticmethod
327    def get_pdtester_arguments(args_dict):
328        """Extract chameleon options from `args_dict` and return the result.
329
330        Recommended usage:
331        ~~~~~~~~
332            args_dict = utils.args_to_dict(args)
333            pdtester_args = hosts.CrosHost.get_pdtester_arguments(args_dict)
334            host = hosts.create_host(machine, pdtester_args=pdtester_args)
335        ~~~~~~~~
336
337        @param args_dict Dictionary from which to extract the pdtester
338          arguments.
339        """
340        return {key: args_dict[key]
341                for key in ('pdtester_host', 'pdtester_port')
342                if key in args_dict}
343
344
345    @staticmethod
346    def get_servo_arguments(args_dict):
347        """Extract servo options from `args_dict` and return the result.
348
349        Recommended usage:
350        ~~~~~~~~
351            args_dict = utils.args_to_dict(args)
352            servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
353            host = hosts.create_host(machine, servo_args=servo_args)
354        ~~~~~~~~
355
356        @param args_dict Dictionary from which to extract the servo
357          arguments.
358        """
359        servo_attrs = (servo_constants.SERVO_HOST_ATTR,
360                       servo_constants.SERVO_HOST_SSH_PORT_ATTR,
361                       servo_constants.SERVO_PORT_ATTR,
362                       servo_constants.SERVOD_DOCKER_ATTR,
363                       servo_constants.SERVO_SERIAL_ATTR,
364                       servo_constants.SERVO_BOARD_ATTR,
365                       servo_constants.SERVO_MODEL_ATTR)
366        servo_args = {key: args_dict[key]
367                      for key in servo_attrs
368                      if key in args_dict}
369        return (
370            None
371            if servo_constants.SERVO_HOST_ATTR in servo_args
372                and not servo_args[servo_constants.SERVO_HOST_ATTR]
373            else servo_args)
374
375
376    def _initialize(self,
377                    hostname,
378                    chameleon_args=None,
379                    servo_args=None,
380                    pdtester_args=None,
381                    try_lab_servo=False,
382                    try_servo_repair=False,
383                    ssh_verbosity_flag='',
384                    ssh_options='',
385                    try_servo_recovery=False,
386                    *args,
387                    **dargs):
388        """Initialize superclasses, |self.chameleon|, and |self.servo|.
389
390        This method will attempt to create the test-assistant object
391        (chameleon/servo) when it is needed by the test. Check
392        the docstring of chameleon_host.create_chameleon_host and
393        servo_host.create_servo_host for how this is determined.
394
395        @param hostname: Hostname of the dut.
396        @param chameleon_args: A dictionary that contains args for creating
397                               a ChameleonHost. See chameleon_host for details.
398        @param servo_args: A dictionary that contains args for creating
399                           a ServoHost object. See servo_host for details.
400        @param try_lab_servo: When true, indicates that an attempt should
401                              be made to create a ServoHost for a DUT in
402                              the test lab, even if not required by
403                              `servo_args`. See servo_host for details.
404        @param try_servo_repair: If a servo host is created, check it
405                              with `repair()` rather than `verify()`.
406                              See servo_host for details.
407        @param ssh_verbosity_flag: String, to pass to the ssh command to control
408                                   verbosity.
409        @param ssh_options: String, other ssh options to pass to the ssh
410                            command.
411        @param try_servo_recovery:  When True, start servod in recovery mode.
412                                    See servo_host for details.
413        """
414        super(CrosHost, self)._initialize(hostname=hostname, *args, **dargs)
415        self._repair_strategy = cros_repair.create_cros_repair_strategy()
416        # hold special dut_state for repair process
417        self._device_repair_state = None
418        self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS)
419        # self.env is a dictionary of environment variable settings
420        # to be exported for commands run on the host.
421        # LIBC_FATAL_STDERR_ can be useful for diagnosing certain
422        # errors that might happen.
423        self.env['LIBC_FATAL_STDERR_'] = '1'
424        self._ssh_verbosity_flag = ssh_verbosity_flag
425        self._ssh_options = ssh_options
426        self.health_profile = None
427        self._default_power_method = None
428        dut_health_profile = device_health_profile.DeviceHealthProfile(
429                hostname=self.hostname,
430                host_info=self.host_info_store.get(),
431                result_dir=self.get_result_dir())
432
433        # TODO(otabek@): remove when b/171414073 closed
434        if self.use_icmp:
435            pingable_before_servo = self.is_up_fast(count=1)
436            if pingable_before_servo:
437                logging.info('DUT is pingable before init Servo.')
438        else:
439            logging.info('Skipping ping to DUT before init Servo.')
440        _servo_host, servo_state = servo_host.create_servo_host(
441                dut=self,
442                servo_args=servo_args,
443                try_lab_servo=try_lab_servo,
444                try_servo_repair=try_servo_repair,
445                try_servo_recovery=try_servo_recovery,
446                dut_host_info=self.host_info_store.get(),
447                dut_health_profile=dut_health_profile)
448        if dut_health_profile.is_loaded():
449            logging.info('Device health profile loaded.')
450            # The device profile is located in the servo_host which make it
451            # dependency. If profile is not loaded yet then we do not have it
452            # TODO(otabek@) persist device provide out of servo-host.
453            self.health_profile = dut_health_profile
454        self.set_servo_host(_servo_host, servo_state)
455
456        # TODO(waihong): Do the simplication on Chameleon too.
457        self._chameleon_host = chameleon_host.create_chameleon_host(
458            dut=self.hostname,
459            chameleon_args=chameleon_args)
460        if self._chameleon_host:
461            self.chameleon = self._chameleon_host.create_chameleon_board()
462        else:
463            self.chameleon = None
464
465        # Bluetooth peers will be populated by the test if needed
466        self._btpeer_host_list = []
467        self.btpeer_list = []
468        self.btpeer = None
469
470        # Add pdtester host if pdtester args were added on command line
471        self._pdtester_host = pdtester_host.create_pdtester_host(
472                pdtester_args, self._servo_host)
473
474        if self._pdtester_host:
475            self.pdtester_servo = self._pdtester_host.get_servo()
476            logging.info('pdtester_servo: %r', self.pdtester_servo)
477            # Create the pdtester object used to access the ec uart
478            self.pdtester = pdtester.PDTester(self.pdtester_servo,
479                    self._pdtester_host.get_servod_server_proxy())
480        else:
481            self.pdtester = None
482
483    def initialize_btpeer(self, btpeer_args=[]):
484        """ Initialize the Bluetooth peers
485
486        Initialize Bluetooth peer devices given in the arguments. Bluetooth peer
487        is chameleon host on Raspberry Pi.
488        @param btpeer_args: A dictionary that contains args for creating
489                            a ChameleonHost. See chameleon_host for details.
490
491        """
492        logging.debug('Attempting to initialize bluetooth peers if available')
493        try:
494            if type(btpeer_args) is list:
495                btpeer_args_list = btpeer_args
496            else:
497                btpeer_args_list = [btpeer_args]
498
499            self._btpeer_host_list = chameleon_host.create_btpeer_host(
500                dut=self.hostname, btpeer_args_list=btpeer_args_list)
501            logging.debug('Bluetooth peer hosts are  %s',
502                          self._btpeer_host_list)
503            self.btpeer_list = [_host.create_chameleon_board() for _host in
504                                self._btpeer_host_list if _host is not None]
505
506            if len(self.btpeer_list) > 0:
507                self.btpeer = self.btpeer_list[0]
508
509            logging.debug('After initialize_btpeer btpeer_list %s '
510                          'btpeer_host_list is %s and btpeer is %s',
511                          self.btpeer_list, self._btpeer_host_list,
512                          self.btpeer)
513        except Exception as e:
514            logging.error('Exception %s in initialize_btpeer', str(e))
515
516
517    def get_cros_repair_image_name(self):
518        """Get latest stable cros image name from AFE.
519
520        Use the board name from the info store. Should that fail, try to
521        retrieve the board name from the host's installed image itself.
522
523        @returns: current stable cros image name for this host.
524        """
525        info = self.host_info_store.get()
526        if not info.board:
527            logging.warning('No board label value found. Trying to infer '
528                         'from the host itself.')
529            try:
530                info.labels.append(self.get_board())
531            except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
532                logging.error('Also failed to get the board name from the DUT '
533                              'itself. %s.', str(e))
534                raise error.AutoservError('Cannot determine board of the DUT'
535                                          ' while getting repair image name.')
536        return afe_utils.get_stable_cros_image_name_v2(info)
537
538
539    def host_version_prefix(self, image):
540        """Return version label prefix.
541
542        In case the CrOS provisioning version is something other than the
543        standard CrOS version e.g. CrOS TH version, this function will
544        find the prefix from provision.py.
545
546        @param image: The image name to find its version prefix.
547        @returns: A prefix string for the image type.
548        """
549        return provision.get_version_label_prefix(image)
550
551    def stage_build_to_usb(self, build):
552        """Stage the current ChromeOS image on the USB stick connected to the
553        servo.
554
555        @param build: The build to download and send to USB.
556        """
557        if not self.servo:
558            raise error.TestError('Host %s does not have servo.' %
559                                  self.hostname)
560
561        _, update_url = self.stage_image_for_servo(build)
562
563        try:
564            self.servo.image_to_servo_usb(update_url)
565        finally:
566            # servo.image_to_servo_usb turned the DUT off, so turn it back on
567            logging.debug('Turn DUT power back on.')
568            self.servo.get_power_state_controller().power_on()
569
570        logging.debug('ChromeOS image %s is staged on the USB stick.',
571                      build)
572
573    def verify_job_repo_url(self, tag=''):
574        """
575        Make sure job_repo_url of this host is valid.
576
577        Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\
578        lumpy-release/R29-4279.0.0/autotest/packages" claims to have the
579        autotest package for lumpy-release/R29-4279.0.0. If this isn't the case,
580        download and extract it. If the devserver embedded in the url is
581        unresponsive, update the job_repo_url of the host after staging it on
582        another devserver.
583
584        @param job_repo_url: A url pointing to the devserver where the autotest
585            package for this build should be staged.
586        @param tag: The tag from the server job, in the format
587                    <job_id>-<user>/<hostname>, or <hostless> for a server job.
588
589        @raises DevServerException: If we could not resolve a devserver.
590        @raises AutoservError: If we're unable to save the new job_repo_url as
591            a result of choosing a new devserver because the old one failed to
592            respond to a health check.
593        @raises urllib2.URLError: If the devserver embedded in job_repo_url
594                                  doesn't respond within the timeout.
595        """
596        info = self.host_info_store.get()
597        job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '')
598        if not job_repo_url:
599            logging.warning('No job repo url set on host %s', self.hostname)
600            return
601
602        logging.info('Verifying job repo url %s', job_repo_url)
603        devserver_url, image_name = tools.get_devserver_build_from_package_url(
604            job_repo_url)
605
606        ds = dev_server.ImageServer(devserver_url)
607
608        logging.info('Staging autotest artifacts for %s on devserver %s',
609            image_name, ds.url())
610
611        start_time = time.time()
612        ds.stage_artifacts(image_name, ['autotest_packages'])
613        stage_time = time.time() - start_time
614
615        # Record how much of the verification time comes from a devserver
616        # restage. If we're doing things right we should not see multiple
617        # devservers for a given board/build/branch path.
618        try:
619            board, build_type, branch = server_utils.ParseBuildName(
620                                                image_name)[:3]
621        except server_utils.ParseBuildNameException:
622            pass
623        else:
624            devserver = devserver_url[
625                devserver_url.find('/') + 2:devserver_url.rfind(':')]
626            stats_key = {
627                'board': board,
628                'build_type': build_type,
629                'branch': branch,
630                'devserver': devserver.replace('.', '_'),
631            }
632
633            monarch_fields = {
634                'board': board,
635                'build_type': build_type,
636                'branch': branch,
637                'dev_server': devserver,
638            }
639            metrics.Counter(
640                    'chromeos/autotest/provision/verify_url'
641                    ).increment(fields=monarch_fields)
642            metrics.SecondsDistribution(
643                    'chromeos/autotest/provision/verify_url_duration'
644                    ).add(stage_time, fields=monarch_fields)
645
646
647    def stage_server_side_package(self, image=None):
648        """Stage autotest server-side package on devserver.
649
650        @param image: Full path of an OS image to install or a build name.
651
652        @return: A url to the autotest server-side package.
653
654        @raise: error.AutoservError if fail to locate the build to test with, or
655                fail to stage server-side package.
656        """
657        # If enable_drone_in_restricted_subnet is False, do not set hostname
658        # in devserver.resolve call, so a devserver in non-restricted subnet
659        # is picked to stage autotest server package for drone to download.
660        hostname = self.hostname
661        if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
662            hostname = None
663        if image:
664            image_name = tools.get_build_from_image(image)
665            if not image_name:
666                raise error.AutoservError(
667                        'Failed to parse build name from %s' % image)
668            ds = dev_server.ImageServer.resolve(image_name, hostname)
669        else:
670            info = self.host_info_store.get()
671            job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '')
672            if job_repo_url:
673                devserver_url, image_name = (
674                    tools.get_devserver_build_from_package_url(job_repo_url))
675                # If enable_drone_in_restricted_subnet is True, use the
676                # existing devserver. Otherwise, resolve a new one in
677                # non-restricted subnet.
678                if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
679                    ds = dev_server.ImageServer(devserver_url)
680                else:
681                    ds = dev_server.ImageServer.resolve(image_name)
682            elif info.build is not None:
683                ds = dev_server.ImageServer.resolve(info.build, hostname)
684                image_name = info.build
685            else:
686                raise error.AutoservError(
687                        'Failed to stage server-side package. The host has '
688                        'no job_repo_url attribute or cros-version label.')
689
690        # Get the OS version of the build, for any build older than
691        # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported.
692        match = re.match('.*/R\d+-(\d+)\.', image_name)
693        if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP:
694            raise error.AutoservError(
695                    'Build %s is older than %s. Server side packaging is '
696                    'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP))
697
698        ds.stage_artifacts(image_name, ['autotest_server_package'])
699        return '%s/static/%s/%s' % (ds.url(), image_name,
700                                    'autotest_server_package.tar.bz2')
701
702
703    def stage_image_for_servo(self, image_name=None, artifact='test_image'):
704        """Stage a build on a devserver and return the update_url.
705
706        @param image_name: a name like lumpy-release/R27-3837.0.0
707        @param artifact: a string like 'test_image'. Requests
708            appropriate image to be staged.
709        @returns a tuple of (image_name, URL) like
710            (lumpy-release/R27-3837.0.0,
711             http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0)
712        """
713        if not image_name:
714            image_name = self.get_cros_repair_image_name()
715        logging.info('Staging build for servo install: %s', image_name)
716        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
717        devserver.stage_artifacts(image_name, [artifact])
718        if artifact == 'test_image':
719            return image_name, devserver.get_test_image_url(image_name)
720        elif artifact == 'recovery_image':
721            return image_name, devserver.get_recovery_image_url(image_name)
722        else:
723            raise error.AutoservError("Bad artifact!")
724
725
726    def stage_factory_image_for_servo(self, image_name):
727        """Stage a build on a devserver and return the update_url.
728
729        @param image_name: a name like <baord>/4262.204.0
730
731        @return: An update URL, eg:
732            http://<devserver>/static/canary-channel/\
733            <board>/4262.204.0/factory_test/chromiumos_factory_image.bin
734
735        @raises: ValueError if the factory artifact name is missing from
736                 the config.
737
738        """
739        if not image_name:
740            logging.error('Need an image_name to stage a factory image.')
741            return
742
743        factory_artifact = CONFIG.get_config_value(
744                'CROS', 'factory_artifact', type=str, default='')
745        if not factory_artifact:
746            raise ValueError('Cannot retrieve the factory artifact name from '
747                             'autotest config, and hence cannot stage factory '
748                             'artifacts.')
749
750        logging.info('Staging build for servo install: %s', image_name)
751        devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
752        devserver.stage_artifacts(
753                image_name,
754                [factory_artifact],
755                archive_url=None)
756
757        return tools.factory_image_url_pattern() % (devserver.url(), image_name)
758
759
760    def prepare_for_update(self):
761        """Prepares the DUT for an update.
762
763        Subclasses may override this to perform any special actions
764        required before updating.
765        """
766        pass
767
768
769    def _clear_fw_version_labels(self, rw_only):
770        """Clear firmware version labels from the machine.
771
772        @param rw_only: True to only clear fwrw_version; otherewise, clear
773                        both fwro_version and fwrw_version.
774        """
775        info = self.host_info_store.get()
776        info.clear_version_labels(provision.FW_RW_VERSION_PREFIX)
777        if not rw_only:
778            info.clear_version_labels(provision.FW_RO_VERSION_PREFIX)
779        self.host_info_store.commit(info)
780
781
782    def _add_fw_version_label(self, build, rw_only):
783        """Add firmware version label to the machine.
784
785        @param build: Build of firmware.
786        @param rw_only: True to only add fwrw_version; otherwise, add both
787                        fwro_version and fwrw_version.
788
789        """
790        info = self.host_info_store.get()
791        info.set_version_label(provision.FW_RW_VERSION_PREFIX, build)
792        if not rw_only:
793            info.set_version_label(provision.FW_RO_VERSION_PREFIX, build)
794        self.host_info_store.commit(info)
795
796
797    def get_latest_release_version(self, platform, ref_board=None):
798        """Search for the latest package release version from the image archive,
799            and return it.
800
801        @param platform: platform name, a.k.a. board or model
802        @param ref_board: reference board name, a.k.a. baseboard, parent
803
804        @return 'firmware-{platform}-{branch}-firmwarebranch/{release-version}/'
805                '{platform}'
806                or None if LATEST release file does not exist.
807        """
808
809        platforms = [ platform ]
810
811        # Search the image path in reference board archive as well.
812        # For example, bob has its binary image under its reference board (gru)
813        # image archive.
814        if ref_board:
815            platforms.append(ref_board)
816
817        for board in platforms:
818            # Read 'LATEST-1.0.0' file
819            branch_dir = provision.FW_BRANCH_GLOB % board
820            latest_file = os.path.join(provision.CROS_IMAGE_ARCHIVE, branch_dir,
821                                       'LATEST-1.0.0')
822
823            try:
824                # The result could be one or more.
825                result = utils.system_output('gsutil ls -d ' +  latest_file)
826
827                candidates = re.findall('gs://.*', result)
828
829                # Found the directory candidates. No need to check the other
830                # board name cadidates. Let's break the loop.
831                break
832            except error.CmdError:
833                # It doesn't exist. Let's move on to the next item.
834                pass
835        else:
836            logging.error('No LATEST release info is available.')
837            return None
838
839        for cand_dir in candidates:
840            result = utils.system_output('gsutil cat ' + cand_dir)
841
842            release_path = cand_dir.replace('LATEST-1.0.0', result)
843            release_path = os.path.join(release_path, platform)
844            try:
845                # Check if release_path does exist.
846                release = utils.system_output('gsutil ls -d ' + release_path)
847                # Now 'release' has a full directory path: e.g.
848                #  gs://chromeos-image-archive/firmware-octopus-11297.B-
849                #  firmwarebranch/RNone-1.0.0-b4395530/octopus/
850
851                # Remove "gs://chromeos-image-archive".
852                release = release.replace(provision.CROS_IMAGE_ARCHIVE, '')
853
854                # Remove CROS_IMAGE_ARCHIVE and any surrounding '/'s.
855                return release.strip('/')
856            except error.CmdError:
857                # The directory might not exist. Let's try next candidate.
858                pass
859        else:
860            raise error.AutoservError('Cannot find the latest firmware')
861
862    @staticmethod
863    def get_version_from_image(host, bios_image, ec_image):
864        """Get version string from binary image using regular expression.
865
866        @param host: An instance of hosts.Host.
867        @param bios_image: Filename of AP BIOS image on the DUT/labstation.
868        @param ec_image: Filename of EC image on the DUT/labstation.
869
870        @return Tuple of bios version and ec version
871        """
872        if not host:
873            return None
874        cmd_args = ['futility', 'update', '--manifest']
875        if bios_image:
876            cmd_args.append('-i')
877            cmd_args.append(bios_image)
878        if ec_image:
879            cmd_args.append('-e')
880            cmd_args.append(ec_image)
881        cmd = ' '.join([utils.sh_quote_word(arg) for arg in cmd_args])
882        stdout = host.run(cmd).stdout
883        if not isinstance(stdout, six.text_type):
884            stdout = stdout.decode('utf-8')
885        io = StringIO(stdout)
886        data = json.load(io)
887        return (
888                data.get('default', {}).get('host', {}).get('versions',
889                                                            {}).get('rw'),
890                data.get('default', {}).get('ec', {}).get('versions',
891                                                          {}).get('rw'),
892        )
893
894
895    def firmware_install(self,
896                         build,
897                         rw_only=False,
898                         dest=None,
899                         local_tarball=None,
900                         verify_version=False,
901                         try_scp=False,
902                         install_ec=True,
903                         install_bios=True,
904                         corrupt_ec=False):
905        """Install firmware to the DUT.
906
907        Use stateful update if the DUT is already running the same build.
908        Stateful update does not update kernel and tends to run much faster
909        than a full reimage. If the DUT is running a different build, or it
910        failed to do a stateful update, full update, including kernel update,
911        will be applied to the DUT.
912
913        Once a host enters firmware_install its fw[ro|rw]_version label will
914        be removed. After the firmware is updated successfully, a new
915        fw[ro|rw]_version label will be added to the host.
916
917        @param build: The build version to which we want to provision the
918                      firmware of the machine,
919                      e.g. 'link-firmware/R22-2695.1.144'.
920        @param rw_only: True to only install firmware to its RW portions. Keep
921                        the RO portions unchanged.
922        @param dest: Directory to store the firmware in.
923        @param local_tarball: Path to local firmware image for installing
924                              without devserver.
925        @param verify_version: True to verify EC and BIOS versions after
926                               programming firmware, default is False.
927        @param try_scp: False to always program using servo, true to try copying
928                        the firmware and programming from the DUT.
929        @param install_ec: True to install EC FW, and False to skip it.
930        @param install_bios: True to install BIOS, and False to skip it.
931        @param corrupt_ec: True to flash EC with a false image (for test purpose).
932
933        TODO(dshi): After bug 381718 is fixed, update here with corresponding
934                    exceptions that could be raised.
935
936        """
937        if not self.servo:
938            raise error.TestError('Host %s does not have servo.' %
939                                  self.hostname)
940
941        # Get the DUT board name from AFE.
942        info = self.host_info_store.get()
943        board = info.board
944        model = info.model
945
946        if board is None or board == '':
947            board = self.servo.get_board()
948
949        if model is None or model == '':
950            try:
951                model = self.get_platform()
952            except Exception as e:
953                logging.warning('Dut is unresponsive: %s', str(e))
954
955        # If local firmware path not provided fetch it from the dev server
956        tmpd = None
957        if not local_tarball:
958            logging.info('Will install firmware from build %s.', build)
959
960            try:
961                ds = dev_server.ImageServer.resolve(build, self.hostname)
962                ds.stage_artifacts(build, ['firmware'])
963
964                if not dest:
965                    tmpd = autotemp.tempdir(unique_id='fwimage')
966                    dest = tmpd.name
967
968                # Download firmware image
969                fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
970                local_tarball = os.path.join(dest, os.path.basename(fwurl))
971                logging.info('Downloading file from %s to %s.', fwurl,
972                             local_tarball)
973                ds.download_file(fwurl,
974                                 local_tarball,
975                                 timeout=self.DEVSERVER_DOWNLOAD_TIMEOUT)
976                logging.info('Done downloading')
977            except Exception as e:
978                raise error.TestError('Failed to download firmware package: %s'
979                                      % str(e))
980
981        ec_image = None
982        if install_ec:
983            # Extract EC image from tarball
984            logging.info('Extracting EC image.')
985            ec_image = self.servo.extract_ec_image(board, model, local_tarball,
986                                                   corrupt_ec)
987            logging.info('Extracted: %s', ec_image)
988
989        bios_image = None
990        if install_bios:
991            # Extract BIOS image from tarball
992            logging.info('Extracting BIOS image.')
993            bios_image = self.servo.extract_bios_image(board, model,
994                                                       local_tarball)
995            logging.info('Extracted: %s', bios_image)
996
997        if not bios_image and not ec_image:
998            raise error.TestError('No firmware installation was processed.')
999
1000        # Clear firmware version labels
1001        self._clear_fw_version_labels(rw_only)
1002
1003        # Install firmware from local tarball
1004        try:
1005            image_ec_version = None
1006            image_bios_version = None
1007
1008            # Check if copying to DUT is enabled and DUT is available
1009            if try_scp and self.is_up():
1010                # DUT is available, make temp firmware directory to store images
1011                logging.info('Making temp folder.')
1012                dest_folder = '/tmp/firmware'
1013                self.run('mkdir -p ' + dest_folder)
1014                dest_bios_path = None
1015                dest_ec_path = None
1016
1017                fw_cmd = self._FW_UPDATE_CMD % ('--wp=1' if rw_only else '')
1018
1019                if bios_image:
1020                    # Send BIOS firmware image to DUT
1021                    logging.info('Sending BIOS firmware.')
1022                    dest_bios_path = os.path.join(dest_folder,
1023                                                  os.path.basename(bios_image))
1024                    self.send_file(bios_image, dest_bios_path)
1025
1026                    # Initialize firmware update command for BIOS image
1027                    fw_cmd += ' -i %s' % dest_bios_path
1028
1029                # Send EC firmware image to DUT when EC image was found
1030                if ec_image:
1031                    logging.info('Sending EC firmware.')
1032                    dest_ec_path = os.path.join(dest_folder,
1033                                                os.path.basename(ec_image))
1034                    self.send_file(ec_image, dest_ec_path)
1035
1036                    # Add EC image to firmware update command
1037                    fw_cmd += ' -e %s' % dest_ec_path
1038
1039                # Make sure command is allowed to finish even if ssh fails.
1040                fw_cmd = "trap '' SIGHUP; %s" % fw_cmd
1041
1042                # Update firmware on DUT
1043                logging.info('Updating firmware.')
1044                try:
1045                    self.run(fw_cmd, options="-o LogLevel=verbose")
1046                except error.AutoservRunError as e:
1047                    if e.result_obj.exit_status != 255:
1048                        raise
1049                    elif ec_image:
1050                        logging.warning("DUT network dropped during update"
1051                                     " (often caused by EC resetting USB)")
1052                    else:
1053                        logging.error("DUT network dropped during update"
1054                                      " (unexpected, since no EC image)")
1055                        raise
1056                image_bios_version, image_ec_version = self.get_version_from_image(
1057                        self, dest_bios_path, dest_ec_path)
1058            else:
1059                # Host is not available, program firmware using servo
1060                dest_bios_path = None
1061                dest_ec_path = None
1062                if ec_image:
1063                    dest_ec_path = self.servo.program_ec(ec_image, rw_only)
1064                if bios_image:
1065                    dest_bios_path = self.servo.program_bios(
1066                            bios_image, rw_only)
1067                if utils.host_is_in_lab_zone(self.hostname):
1068                    self._add_fw_version_label(build, rw_only)
1069                image_bios_version, image_ec_version = self.get_version_from_image(
1070                        self._servo_host, dest_bios_path, dest_ec_path)
1071
1072            # Reboot and wait for DUT after installing firmware
1073            logging.info('Rebooting DUT.')
1074            self.servo.get_power_state_controller().reset()
1075            time.sleep(self.servo.BOOT_DELAY)
1076            self.test_wait_for_boot()
1077
1078            # When enabled verify EC and BIOS firmware version after programming
1079            if verify_version:
1080                # Check programmed EC firmware when EC image was found
1081                if ec_image:
1082                    logging.info('Checking EC firmware version.')
1083                    if image_ec_version is None:
1084                        raise error.TestFail(
1085                                'Could not find EC version in %s' % ec_image)
1086                    dest_ec_version = self.get_ec_version()
1087                    if dest_ec_version != image_ec_version:
1088                        raise error.TestFail(
1089                            'Failed to update EC firmware, version %s '
1090                            '(expected %s)' % (dest_ec_version,
1091                                               image_ec_version))
1092
1093                if bios_image:
1094                    # Check programmed BIOS firmware against expected version
1095                    logging.info('Checking BIOS firmware version.')
1096                    if image_bios_version is None:
1097                        raise error.TestFail(
1098                                'Could not find BIOS version in %s' %
1099                                bios_image)
1100                    dest_bios_version = self.get_firmware_version()
1101                    if dest_bios_version != image_bios_version:
1102                        raise error.TestFail(
1103                            'Failed to update BIOS, version %s '
1104                            '(expected %s)' % (dest_bios_version,
1105                                               image_bios_version))
1106        finally:
1107            if tmpd:
1108                tmpd.clean()
1109
1110
1111    def install_image_to_servo_usb(self, image_url=None):
1112        """Installing a test image on a USB storage device.
1113
1114        Download image to USB-storage attached to the Servo board.
1115
1116        @param image_url:       If specified use as the url to download to
1117                                USB-storage.
1118
1119        @raises AutoservError if the image fails to download.
1120
1121        """
1122        if not image_url:
1123            logging.debug('Skip download as image_url not provided!')
1124            return
1125
1126        logging.info('Downloading image to USB')
1127        metrics_field = {'download': bool(image_url)}
1128        metrics.Counter(
1129                'chromeos/autotest/provision/servo_install/download_image'
1130        ).increment(fields=metrics_field)
1131        with metrics.SecondsTimer(
1132                'chromeos/autotest/servo_install/download_image_time'):
1133            try:
1134                self.servo.image_to_servo_usb(image_path=image_url,
1135                                              power_off_dut=False)
1136            except error.AutotestError as e:
1137                metrics.Counter('chromeos/autotest/repair/image_to_usb_error'
1138                                ).increment(
1139                                        fields={'host': self.hostname or ''})
1140                six.reraise(error.AutotestError, str(e), sys.exc_info()[2])
1141
1142    def boot_in_recovery_mode(self,
1143                              usb_boot_timeout=USB_BOOT_TIMEOUT,
1144                              need_snk=False):
1145        """Booting host  in recovery mode.
1146
1147        Boot device in recovery mode and verify that device booted from
1148        external storage as expected.
1149
1150        @param usb_boot_timeout:    The usb_boot_timeout to use wait the host
1151                                    to boot. Factory images need a longer
1152                                    usb_boot_timeout than regular cros images.
1153        @param snk_mode:            If True, switch servo_v4 role to 'snk'
1154                                    mode before boot DUT into recovery mode.
1155
1156        @raises AutoservError if the image fails to boot.
1157
1158        """
1159        logging.info('Booting from USB directly. Usb boot timeout: %s',
1160                     usb_boot_timeout)
1161        with metrics.SecondsTimer(
1162                'chromeos/autotest/provision/servo_install/boot_duration'):
1163            self.servo.boot_in_recovery_mode(snk_mode=need_snk)
1164            if not self.wait_up(timeout=usb_boot_timeout):
1165                if need_snk:
1166                    # Attempt to restore servo_v4 role to 'src' mode.
1167                    self.servo.set_servo_v4_role('src')
1168                raise hosts.AutoservRepairError(
1169                        'DUT failed to boot from USB after %d seconds' %
1170                        usb_boot_timeout, 'failed_to_boot_pre_install')
1171
1172        # Make sure the DUT is boot from an external device.
1173        if not self.is_boot_from_external_device():
1174            raise hosts.AutoservRepairError(
1175                    'DUT is expected to boot from an external device(e.g. '
1176                    'a usb stick), however it seems still boot from an'
1177                    ' internal storage.', 'boot_from_internal_storage')
1178
1179    def run_install_image(self,
1180                          install_timeout=INSTALL_TIMEOUT,
1181                          need_snk=False,
1182                          is_repair=False):
1183        """Installing the image with chromeos-install.
1184
1185        Steps included:
1186        1) Recover TPM on the device
1187        2) Run chromeos-install
1188        2.a) if success: power off/on the device
1189        2.b) if fail:
1190        2.b.1) Mark for replacement if fail with hardware issue
1191        2.b.2) Run internal storage check. (Only if is_repair=True)
1192        3) Wait the device to boot as verifier of success install
1193
1194        Device has to booted from external storage.
1195
1196        @param install_timeout:     The timeout to use when installing the
1197                                    chromeos image. Factory images need a
1198                                    longer install_timeout.
1199        @param snk_mode:            If True, switch servo_v4 role to 'snk'
1200                                    mode before boot DUT into recovery mode.
1201        @param is_repair:           Indicates if the method is called from a
1202                                    repair task.
1203
1204        @raises AutoservError if the fail in process of install image.
1205        @raises AutoservRepairError if fail to boot after install image.
1206
1207        """
1208        # The new chromeos-tpm-recovery has been merged since R44-7073.0.0.
1209        # In old CrOS images, this command fails. Skip the error.
1210        logging.info('Resetting the TPM status')
1211        try:
1212            self.run('chromeos-tpm-recovery')
1213        except error.AutoservRunError:
1214            logging.warning('chromeos-tpm-recovery is too old.')
1215
1216        with metrics.SecondsTimer(
1217                'chromeos/autotest/provision/servo_install/install_duration'):
1218            logging.info('Installing image through chromeos-install.')
1219            try:
1220                self.run('chromeos-install --yes',timeout=install_timeout)
1221                self.halt()
1222            except Exception as e:
1223                storage_errors = [
1224                   'No space left on device',
1225                   'I/O error when trying to write primary GPT',
1226                   'Input/output error while writing out',
1227                   'cannot read GPT header',
1228                   'can not determine destination device',
1229                   'wrong fs type',
1230                   'bad superblock on',
1231                ]
1232                has_error = [msg for msg in storage_errors if(msg in str(e))]
1233                if has_error:
1234                    info = self.host_info_store.get()
1235                    info.set_version_label(
1236                        audit_const.DUT_STORAGE_STATE_PREFIX,
1237                        audit_const.HW_STATE_NEED_REPLACEMENT)
1238                    self.host_info_store.commit(info)
1239                    self.set_device_repair_state(
1240                        cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT)
1241                    logging.debug(
1242                        'Fail install image from USB; Storage error; %s', e)
1243                    raise error.AutoservError(
1244                        'Failed to install image from USB due to a suspect '
1245                        'disk failure, DUT storage state changed to '
1246                        'need_replacement, please check debug log '
1247                        'for details.')
1248                else:
1249                    if is_repair:
1250                        # DUT will be marked for replacement if storage is bad.
1251                        audit_verify.VerifyDutStorage(self).verify()
1252
1253                    logging.debug('Fail install image from USB; %s', e)
1254                    raise error.AutoservError(
1255                        'Failed to install image from USB due to unexpected '
1256                        'error, please check debug log for details.')
1257            finally:
1258                # We need reset the DUT no matter re-install success or not,
1259                # as we don't want leave the DUT in boot from usb state.
1260                logging.info('Power cycling DUT through servo.')
1261                self.servo.get_power_state_controller().power_off()
1262                self.servo.switch_usbkey('off')
1263                if need_snk:
1264                    # Attempt to restore servo_v4 role to 'src' mode.
1265                    self.servo.set_servo_v4_role('src')
1266                # N.B. The Servo API requires that we use power_on() here
1267                # for two reasons:
1268                #  1) After turning on a DUT in recovery mode, you must turn
1269                #     it off and then on with power_on() once more to
1270                #     disable recovery mode (this is a Parrot specific
1271                #     requirement).
1272                #  2) After power_off(), the only way to turn on is with
1273                #     power_on() (this is a Storm specific requirement).
1274                self.servo.get_power_state_controller().power_on()
1275
1276        logging.info('Waiting for DUT to come back up.')
1277        if not self.wait_up(timeout=self.BOOT_TIMEOUT):
1278            raise hosts.AutoservRepairError('DUT failed to reboot installed '
1279                                            'test image after %d seconds' %
1280                                            self.BOOT_TIMEOUT,
1281                                            'failed_to_boot_post_install')
1282
1283    def servo_install(self,
1284                      image_url=None,
1285                      usb_boot_timeout=USB_BOOT_TIMEOUT,
1286                      install_timeout=INSTALL_TIMEOUT,
1287                      is_repair=False):
1288        """Re-install the OS on the DUT by:
1289
1290        Steps:
1291        1) Power off the host
1292        2) Installing an image on a USB-storage attached to the Servo board
1293        3) Booting that image in recovery mode
1294        4) Installing the image with chromeos-install.
1295
1296        @param image_url:           If specified use as the url to install on
1297                                    the DUT otherwise boot the currently
1298                                    staged image on the USB stick.
1299        @param usb_boot_timeout:    The usb_boot_timeout to use during
1300                                    re-image. Factory images need a longer
1301                                    usb_boot_timeout than regular cros images.
1302        @param install_timeout:     The timeout to use when installing the
1303                                    chromeos image. Factory images need a
1304                                    longer install_timeout.
1305        @param is_repair:           Indicates if the method is called from a
1306                                    repair task.
1307
1308        @raises AutoservError if the image fails to boot.
1309
1310        """
1311        self.servo.get_power_state_controller().power_off()
1312        if image_url:
1313            self.install_image_to_servo_usb(image_url=image_url)
1314        else:
1315            # Give the DUT some time to power_off if we skip
1316            # download image to usb. (crbug.com/982993)
1317            time.sleep(10)
1318
1319        need_snk = self.require_snk_mode_in_recovery()
1320
1321        self.boot_in_recovery_mode(usb_boot_timeout=usb_boot_timeout,
1322                                   need_snk=need_snk)
1323
1324        self.run_install_image(install_timeout=install_timeout,
1325                               need_snk=need_snk,
1326                               is_repair=is_repair)
1327
1328    def set_servo_host(self, host, servo_state=None):
1329        """Set our servo host member, and associated servo.
1330
1331        @param host  Our new `ServoHost`.
1332        """
1333        self._servo_host = host
1334        self.servo_pwr_supported = None
1335        if self._servo_host is not None:
1336            self.servo = self._servo_host.get_servo()
1337            servo_state = self._servo_host.get_servo_state()
1338            self._set_smart_usbhub_label(self._servo_host.smart_usbhub)
1339            try:
1340                self.servo_pwr_supported = self.servo.has_control('power_state')
1341            except Exception as e:
1342                logging.debug(
1343                    "Could not get servo power state due to {}".format(e))
1344        else:
1345            self.servo = None
1346            self.servo_pwr_supported = False
1347        self.set_servo_type()
1348        self.set_servo_state(servo_state)
1349        self._set_servo_topology()
1350
1351
1352    def repair_servo(self):
1353        """
1354        Confirm that servo is initialized and verified.
1355
1356        If the servo object is missing, attempt to repair the servo
1357        host.  Repair failures are passed back to the caller.
1358
1359        @raise AutoservError: If there is no servo host for this CrOS
1360                              host.
1361        """
1362        if self.servo:
1363            return
1364        if not self._servo_host:
1365            raise error.AutoservError('No servo host for %s.' %
1366                                      self.hostname)
1367        try:
1368            self._servo_host.repair()
1369        except:
1370            raise
1371        finally:
1372            self.set_servo_host(self._servo_host)
1373
1374
1375    def set_servo_type(self):
1376        """Set servo info labels to dut host_info"""
1377        if not self.servo:
1378            logging.debug('Servo is not initialized to get servo_type.')
1379            return
1380        if not self.is_servo_in_working_state():
1381            logging.debug('Servo is not good, skip update servo_type.')
1382            return
1383        servo_type = self.servo.get_servo_type()
1384        if not servo_type:
1385            logging.debug('Cannot collect servo_type from servo'
1386                ' by `dut-control servo_type`! Please file a bug'
1387                ' and inform infra team as we are not expected '
1388                ' to reach this point.')
1389            return
1390        host_info = self.host_info_store.get()
1391        prefix = servo_constants.SERVO_TYPE_LABEL_PREFIX
1392        old_type = host_info.get_label_value(prefix)
1393        if old_type == servo_type:
1394            # do not need update
1395            return
1396        host_info.set_version_label(prefix, servo_type)
1397        self.host_info_store.commit(host_info)
1398        logging.info('ServoHost: servo_type updated to %s '
1399                    '(previous: %s)', servo_type, old_type)
1400
1401
1402    def set_servo_state(self, servo_state):
1403        """Set servo info labels to dut host_info"""
1404        if servo_state is not None:
1405            host_info = self.host_info_store.get()
1406            servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX
1407            old_state = host_info.get_label_value(servo_state_prefix)
1408            if old_state == servo_state:
1409                # do not need update
1410                return
1411            host_info.set_version_label(servo_state_prefix, servo_state)
1412            self.host_info_store.commit(host_info)
1413            logging.info('ServoHost: servo_state updated to %s (previous: %s)',
1414                         servo_state, old_state)
1415
1416
1417    def get_servo_state(self):
1418        host_info = self.host_info_store.get()
1419        servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX
1420        return host_info.get_label_value(servo_state_prefix)
1421
1422    def is_servo_in_working_state(self):
1423        """Validate servo is in WORKING state."""
1424        servo_state = self.get_servo_state()
1425        return servo_state == servo_constants.SERVO_STATE_WORKING
1426
1427    def get_servo_usb_state(self):
1428        """Get the label value indicating the health of the USB drive.
1429
1430        @return: The label value if defined, otherwise '' (empty string).
1431        @rtype: str
1432        """
1433        host_info = self.host_info_store.get()
1434        servo_usb_state_prefix = audit_const.SERVO_USB_STATE_PREFIX
1435        return host_info.get_label_value(servo_usb_state_prefix)
1436
1437    def is_servo_usb_usable(self):
1438        """Check if the servo USB storage device is usable for FAFT.
1439
1440        @return: False if the label indicates a state that will break FAFT.
1441                 True if state is okay, or if state is not defined.
1442        @rtype: bool
1443        """
1444        usb_state = self.get_servo_usb_state()
1445        return usb_state in ('', audit_const.HW_STATE_ACCEPTABLE,
1446                             audit_const.HW_STATE_NORMAL,
1447                             audit_const.HW_STATE_UNKNOWN)
1448
1449    def _set_smart_usbhub_label(self, smart_usbhub_detected):
1450        if smart_usbhub_detected is None:
1451            # skip the label update here as this indicate we wasn't able
1452            # to confirm usbhub type.
1453            return
1454        host_info = self.host_info_store.get()
1455        if (smart_usbhub_detected ==
1456                (servo_constants.SMART_USBHUB_LABEL in host_info.labels)):
1457            # skip label update if current label match the truth.
1458            return
1459        if smart_usbhub_detected:
1460            logging.info('Adding %s label to host %s',
1461                         servo_constants.SMART_USBHUB_LABEL,
1462                         self.hostname)
1463            host_info.labels.append(servo_constants.SMART_USBHUB_LABEL)
1464        else:
1465            logging.info('Removing %s label from host %s',
1466                         servo_constants.SMART_USBHUB_LABEL,
1467                         self.hostname)
1468            host_info.labels.remove(servo_constants.SMART_USBHUB_LABEL)
1469        self.host_info_store.commit(host_info)
1470
1471    def repair(self):
1472        """Attempt to get the DUT to pass `self.verify()`.
1473
1474        This overrides the base class function for repair; it does
1475        not call back to the parent class, but instead relies on
1476        `self._repair_strategy` to coordinate the verification and
1477        repair steps needed to get the DUT working.
1478        """
1479        message = 'Beginning repair for host %s board %s model %s'
1480        info = self.host_info_store.get()
1481        message %= (self.hostname, info.board, info.model)
1482        self.record('INFO', None, None, message)
1483        profile_state = profile_constants.DUT_STATE_READY
1484        # Initialize bluetooth peers
1485        self.initialize_btpeer()
1486        try:
1487            self._repair_strategy.repair(self)
1488        except hosts.AutoservVerifyDependencyError as e:
1489            # TODO(otabek): remove when finish b/174191325
1490            self._stat_if_pingable_but_not_sshable()
1491            # We don't want flag a DUT as failed if only non-critical
1492            # verifier(s) failed during the repair.
1493            if e.is_critical():
1494                profile_state = profile_constants.DUT_STATE_REPAIR_FAILED
1495                self._reboot_labstation_if_needed()
1496                self.try_set_device_needs_manual_repair()
1497                raise
1498        finally:
1499            self.set_health_profile_dut_state(profile_state)
1500
1501    def get_verifier_state(self, tag):
1502        """Return the state of host verifier by tag.
1503
1504        @returns: bool or None
1505        """
1506        return self._repair_strategy.verifier_is_good(tag)
1507
1508    def get_repair_strategy_node(self, tag):
1509        """Return the instance of verifier/repair node for host by tag.
1510
1511        @returns: _DependencyNode or None
1512        """
1513        return self._repair_strategy.node_by_tag(tag)
1514
1515    def close(self):
1516        """Close connection."""
1517        super(CrosHost, self).close()
1518
1519        if self._chameleon_host:
1520            self._chameleon_host.close()
1521
1522        if self.health_profile:
1523            try:
1524                self.health_profile.close()
1525            except Exception as e:
1526                logging.warning(
1527                    'Failed to finalize device health profile; %s', e)
1528
1529        if self._servo_host:
1530            self._servo_host.close()
1531
1532    def get_power_supply_info(self):
1533        """Get the output of power_supply_info.
1534
1535        power_supply_info outputs the info of each power supply, e.g.,
1536        Device: Line Power
1537          online:                  no
1538          type:                    Mains
1539          voltage (V):             0
1540          current (A):             0
1541        Device: Battery
1542          state:                   Discharging
1543          percentage:              95.9276
1544          technology:              Li-ion
1545
1546        Above output shows two devices, Line Power and Battery, with details of
1547        each device listed. This function parses the output into a dictionary,
1548        with key being the device name, and value being a dictionary of details
1549        of the device info.
1550
1551        @return: The dictionary of power_supply_info, e.g.,
1552                 {'Line Power': {'online': 'yes', 'type': 'main'},
1553                  'Battery': {'vendor': 'xyz', 'percentage': '100'}}
1554        @raise error.AutoservRunError if power_supply_info tool is not found in
1555               the DUT. Caller should handle this error to avoid false failure
1556               on verification.
1557        """
1558        result = self.run('power_supply_info').stdout.strip()
1559        info = {}
1560        device_name = None
1561        device_info = {}
1562        for line in result.split('\n'):
1563            pair = [v.strip() for v in line.split(':')]
1564            if len(pair) != 2:
1565                continue
1566            if pair[0] == 'Device':
1567                if device_name:
1568                    info[device_name] = device_info
1569                device_name = pair[1]
1570                device_info = {}
1571            else:
1572                device_info[pair[0]] = pair[1]
1573        if device_name and not device_name in info:
1574            info[device_name] = device_info
1575        return info
1576
1577
1578    def get_battery_percentage(self):
1579        """Get the battery percentage.
1580
1581        @return: The percentage of battery level, value range from 0-100. Return
1582                 None if the battery info cannot be retrieved.
1583        """
1584        try:
1585            info = self.get_power_supply_info()
1586            logging.info(info)
1587            return float(info['Battery']['percentage'])
1588        except (KeyError, ValueError, error.AutoservRunError):
1589            return None
1590
1591
1592    def get_battery_state(self):
1593        """Get the battery charging state.
1594
1595        @return: A string representing the battery charging state. It can be
1596                 'Charging', 'Fully charged', or 'Discharging'.
1597        """
1598        try:
1599            info = self.get_power_supply_info()
1600            logging.info(info)
1601            return info['Battery']['state']
1602        except (KeyError, ValueError, error.AutoservRunError):
1603            return None
1604
1605
1606    def get_battery_display_percentage(self):
1607        """Get the battery display percentage.
1608
1609        @return: The display percentage of battery level, value range from
1610                 0-100. Return None if the battery info cannot be retrieved.
1611        """
1612        try:
1613            info = self.get_power_supply_info()
1614            logging.info(info)
1615            return float(info['Battery']['display percentage'])
1616        except (KeyError, ValueError, error.AutoservRunError):
1617            return None
1618
1619
1620    def is_ac_connected(self):
1621        """Check if the dut has power adapter connected and charging.
1622
1623        @return: True if power adapter is connected and charging.
1624        """
1625        try:
1626            info = self.get_power_supply_info()
1627            return info['Line Power']['online'] == 'yes'
1628        except (KeyError, error.AutoservRunError):
1629            return None
1630
1631
1632    def _cleanup_poweron(self):
1633        """Special cleanup method to make sure hosts always get power back."""
1634        info = self.host_info_store.get()
1635        if self._RPM_OUTLET_CHANGED not in info.attributes:
1636            return
1637        logging.debug('This host has recently interacted with the RPM'
1638                      ' Infrastructure. Ensuring power is on.')
1639        try:
1640            self.power_on()
1641            self._remove_rpm_changed_tag()
1642        except rpm_client.RemotePowerException:
1643            logging.error('Failed to turn Power On for this host after '
1644                          'cleanup through the RPM Infrastructure.')
1645
1646            battery_percentage = self.get_battery_percentage()
1647            if (
1648                    battery_percentage
1649                    and battery_percentage < cros_constants.MIN_BATTERY_LEVEL):
1650                raise
1651            elif self.is_ac_connected():
1652                logging.info('The device has power adapter connected and '
1653                             'charging. No need to try to turn RPM on '
1654                             'again.')
1655                self._remove_rpm_changed_tag()
1656            logging.info('Battery level is now at %s%%. The device may '
1657                         'still have enough power to run test, so no '
1658                         'exception will be raised.', battery_percentage)
1659
1660
1661    def _remove_rpm_changed_tag(self):
1662        info = self.host_info_store.get()
1663        del info.attributes[self._RPM_OUTLET_CHANGED]
1664        self.host_info_store.commit(info)
1665
1666
1667    def _add_rpm_changed_tag(self):
1668        info = self.host_info_store.get()
1669        info.attributes[self._RPM_OUTLET_CHANGED] = 'true'
1670        self.host_info_store.commit(info)
1671
1672
1673
1674    def _is_factory_image(self):
1675        """Checks if the image on the DUT is a factory image.
1676
1677        @return: True if the image on the DUT is a factory image.
1678                 False otherwise.
1679        """
1680        result = self.run('[ -f /root/.factory_test ]', ignore_status=True)
1681        return result.exit_status == 0
1682
1683
1684    def _restart_ui(self):
1685        """Restart the Chrome UI.
1686
1687        @raises: FactoryImageCheckerException for factory images, since
1688                 we cannot attempt to restart ui on them.
1689                 error.AutoservRunError for any other type of error that
1690                 occurs while restarting ui.
1691        """
1692        if self._is_factory_image():
1693            raise FactoryImageCheckerException('Cannot restart ui on factory '
1694                                               'images')
1695
1696        # TODO(jrbarnette):  The command to stop/start the ui job
1697        # should live inside cros_ui, too.  However that would seem
1698        # to imply interface changes to the existing start()/restart()
1699        # functions, which is a bridge too far (for now).
1700        prompt = cros_ui.get_chrome_session_ident(self)
1701        self.run('stop ui; start ui')
1702        cros_ui.wait_for_chrome_ready(prompt, self)
1703
1704
1705    def _start_powerd_if_needed(self):
1706        """Start powerd if it isn't already running."""
1707        self.run('start powerd', ignore_status=True)
1708
1709    def _read_arc_prop_file(self, filename):
1710        for path in [
1711                '/usr/share/arcvm/properties/', '/usr/share/arc/properties/'
1712        ]:
1713            if self.path_exists(path + filename):
1714                return utils.parse_cmd_output('cat ' + path + filename,
1715                                              run_method=self.run)
1716        return None
1717
1718    def _get_arc_build_info(self):
1719        """Returns a dictionary mapping build properties to their values."""
1720        build_info = None
1721        for filename in ['build.prop', 'vendor_build.prop']:
1722            properties = self._read_arc_prop_file(filename)
1723            if properties:
1724                if build_info:
1725                    build_info.update(properties)
1726                else:
1727                    build_info = properties
1728            else:
1729                logging.error('Failed to find %s in device.', filename)
1730        return build_info
1731
1732    def has_arc_hardware_vulkan(self):
1733        """Returns a boolean whether device has hardware vulkan."""
1734        return self._get_arc_build_info().get('ro.hardware.vulkan')
1735
1736    def get_arc_build_type(self):
1737        """Returns the ARC build type of the host."""
1738        return self._get_arc_build_info().get('ro.build.type')
1739
1740    def get_arc_primary_abi(self):
1741        """Returns the primary abi of the host."""
1742        return self._get_arc_build_info().get('ro.product.cpu.abi')
1743
1744    def get_arc_security_patch(self):
1745        """Returns the security patch of the host."""
1746        return self._get_arc_build_info().get('ro.build.version.security_patch')
1747
1748    def get_arc_first_api_level(self):
1749        """Returns the security patch of the host."""
1750        return self._get_arc_build_info().get('ro.product.first_api_level')
1751
1752    def _get_lsb_release_content(self):
1753        """Return the content of lsb-release file of host."""
1754        return self.run(
1755                'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
1756
1757
1758    def get_release_version(self):
1759        """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
1760
1761        @returns The version string in lsb-release, under attribute
1762                 CHROMEOS_RELEASE_VERSION.
1763        """
1764        return lsbrelease_utils.get_chromeos_release_version(
1765                lsb_release_content=self._get_lsb_release_content())
1766
1767
1768    def get_release_builder_path(self):
1769        """Get the value of CHROMEOS_RELEASE_BUILDER_PATH from lsb-release.
1770
1771        @returns The version string in lsb-release, under attribute
1772                 CHROMEOS_RELEASE_BUILDER_PATH.
1773        """
1774        return lsbrelease_utils.get_chromeos_release_builder_path(
1775                lsb_release_content=self._get_lsb_release_content())
1776
1777
1778    def get_chromeos_release_milestone(self):
1779        """Get the value of attribute CHROMEOS_RELEASE_BUILD_TYPE
1780        from lsb-release.
1781
1782        @returns The version string in lsb-release, under attribute
1783                 CHROMEOS_RELEASE_BUILD_TYPE.
1784        """
1785        return lsbrelease_utils.get_chromeos_release_milestone(
1786                lsb_release_content=self._get_lsb_release_content())
1787
1788
1789    def verify_cros_version_label(self):
1790        """Verify if host's cros-version label match the actual image in dut.
1791
1792        @returns True if the label match with image in dut, otherwise False
1793        """
1794        os_from_host = self.get_release_builder_path()
1795        info = self.host_info_store.get()
1796        os_from_label = info.get_label_value(self.VERSION_PREFIX)
1797        if not os_from_label:
1798            logging.debug('No existing %s label detected', self.VERSION_PREFIX)
1799            return True
1800
1801        # known cases where the version label will not match the
1802        # original CHROMEOS_RELEASE_BUILDER_PATH setting:
1803        #  * Tests for the `arc-presubmit` append "-cheetsth" to the label.
1804        if os_from_label.endswith(provision.CHEETS_SUFFIX):
1805            logging.debug('%s label with %s suffix detected, this suffix will'
1806                          ' be ignored when comparing label.',
1807                          self.VERSION_PREFIX, provision.CHEETS_SUFFIX)
1808            os_from_label = os_from_label[:-len(provision.CHEETS_SUFFIX)]
1809        logging.debug('OS version from host: %s; OS verision cached in '
1810                      'label: %s', os_from_host, os_from_label)
1811        return os_from_label == os_from_host
1812
1813
1814    def cleanup_services(self):
1815        """Reinitializes the device for cleanup.
1816
1817        Subclasses may override this to customize the cleanup method.
1818
1819        To indicate failure of the reset, the implementation may raise
1820        any of:
1821            error.AutoservRunError
1822            error.AutotestRunError
1823            FactoryImageCheckerException
1824
1825        @raises error.AutoservRunError
1826        @raises error.AutotestRunError
1827        @raises error.FactoryImageCheckerException
1828        """
1829        self._restart_ui()
1830        self._start_powerd_if_needed()
1831
1832
1833    def cleanup(self):
1834        """Cleanup state on device."""
1835        self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
1836        try:
1837            self.cleanup_services()
1838        except (error.AutotestRunError, error.AutoservRunError,
1839                FactoryImageCheckerException):
1840            logging.warning('Unable to restart ui.')
1841
1842        # cleanup routines, i.e. reboot the machine.
1843        super(CrosHost, self).cleanup()
1844
1845        # Check if the rpm outlet was manipulated.
1846        if self.has_power():
1847            self._cleanup_poweron()
1848
1849
1850    def reboot(self, **dargs):
1851        """
1852        This function reboots the site host. The more generic
1853        RemoteHost.reboot() performs sync and sleeps for 5
1854        seconds. This is not necessary for ChromeOS devices as the
1855        sync should be finished in a short time during the reboot
1856        command.
1857        """
1858        if 'reboot_cmd' not in dargs:
1859            reboot_timeout = dargs.get('reboot_timeout', 10)
1860            dargs['reboot_cmd'] = ('sleep 1; '
1861                                   'reboot & sleep %d; '
1862                                   'reboot -f' % reboot_timeout)
1863        # Enable fastsync to avoid running extra sync commands before reboot.
1864        if 'fastsync' not in dargs:
1865            dargs['fastsync'] = True
1866
1867        dargs['board'] = self.host_info_store.get().board
1868        # Record who called us
1869        orig = sys._getframe(1).f_code
1870        metric_fields = {'board' : dargs['board'],
1871                         'dut_host_name' : self.hostname,
1872                         'success' : True}
1873        metric_debug_fields = {'board' : dargs['board'],
1874                               'caller' : "%s:%s" % (orig.co_filename,
1875                                                     orig.co_name),
1876                               'success' : True,
1877                               'error' : ''}
1878
1879        t0 = time.time()
1880        logging.debug('Pre reboot lsb-release {}'.format(
1881                self._get_lsb_release_content()))
1882        try:
1883            super(CrosHost, self).reboot(**dargs)
1884        except Exception as e:
1885            metric_fields['success'] = False
1886            metric_debug_fields['success'] = False
1887            metric_debug_fields['error'] = type(e).__name__
1888            raise
1889        finally:
1890            duration = int(time.time() - t0)
1891            logging.debug('Post reboot lsb-release {}'.format(
1892                    self._get_lsb_release_content()))
1893
1894            metrics.Counter(
1895                    'chromeos/autotest/autoserv/reboot_count').increment(
1896                    fields=metric_fields)
1897            metrics.Counter(
1898                    'chromeos/autotest/autoserv/reboot_debug').increment(
1899                    fields=metric_debug_fields)
1900            metrics.SecondsDistribution(
1901                    'chromeos/autotest/autoserv/reboot_duration').add(
1902                    duration, fields=metric_fields)
1903
1904    def _default_suspend_cmd(self, suspend_time=60, delay_seconds=0):
1905        """
1906        Return the default suspend command
1907
1908        @param suspend_time: How long to suspend as integer seconds.
1909        @param suspend_cmd: Suspend command to execute.
1910
1911        @returns formatted suspend_cmd string to execute
1912        """
1913        suspend_cmd = ' && '.join([
1914            'echo 0 > /sys/class/rtc/rtc0/wakealarm',
1915            'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
1916            'powerd_dbus_suspend --delay=%d' % delay_seconds])
1917        return suspend_cmd
1918
1919    def suspend_bg(self, suspend_time=60, delay_seconds=0,
1920                suspend_cmd=None):
1921        """
1922        This function suspends the site host and returns right away.
1923
1924        Note: use this when you need to perform work *while* the host is
1925        suspended.
1926
1927        @param suspend_time: How long to suspend as integer seconds.
1928        @param suspend_cmd: Suspend command to execute.
1929
1930        @exception AutoservSuspendError: if |suspend_cmd| fails
1931        """
1932        if suspend_cmd is None:
1933            suspend_cmd = self._default_suspend_cmd(suspend_time, delay_seconds)
1934        try:
1935            self.run_background(suspend_cmd)
1936        except error.AutoservRunError:
1937            raise error.AutoservSuspendError("suspend command failed")
1938
1939    def suspend(self, suspend_time=60, delay_seconds=0,
1940                suspend_cmd=None, allow_early_resume=False):
1941        """
1942        This function suspends the site host.
1943
1944        @param suspend_time: How long to suspend as integer seconds.
1945        @param suspend_cmd: Suspend command to execute.
1946        @param allow_early_resume: If False and if device resumes before
1947                                   |suspend_time|, throw an error.
1948
1949        @exception AutoservSuspendError Host resumed earlier than
1950                                         |suspend_time|.
1951        """
1952
1953        if suspend_cmd is None:
1954            suspend_cmd = self._default_suspend_cmd(suspend_time, delay_seconds)
1955        super(CrosHost, self).suspend(suspend_time, suspend_cmd,
1956                                      allow_early_resume);
1957
1958
1959    def upstart_status(self, service_name):
1960        """Check the status of an upstart init script.
1961
1962        @param service_name: Service to look up.
1963
1964        @returns True if the service is running, False otherwise.
1965        """
1966        return 'start/running' in self.run('status %s' % service_name,
1967                                           ignore_status=True).stdout
1968
1969    def upstart_stop(self, service_name):
1970        """Stops an upstart job if it's running.
1971
1972        @param service_name: Service to stop
1973
1974        @returns True if service has been stopped or was already stopped
1975                 False otherwise.
1976        """
1977        if not self.upstart_status(service_name):
1978            return True
1979
1980        result = self.run('stop %s' % service_name, ignore_status=True)
1981        if result.exit_status != 0:
1982            return False
1983        return True
1984
1985    def upstart_restart(self, service_name):
1986        """Restarts (or starts) an upstart job.
1987
1988        @param service_name: Service to start/restart
1989
1990        @returns True if service has been started/restarted, False otherwise.
1991        """
1992        cmd = 'start'
1993        if self.upstart_status(service_name):
1994            cmd = 'restart'
1995        cmd = cmd + ' %s' % service_name
1996        result = self.run(cmd)
1997        if result.exit_status != 0:
1998            return False
1999        return True
2000
2001    def verify_software(self):
2002        """Verify working software on a ChromeOS system.
2003
2004        Tests for the following conditions:
2005         1. All conditions tested by the parent version of this
2006            function.
2007         2. Sufficient space in /mnt/stateful_partition.
2008         3. Sufficient space in /mnt/stateful_partition/encrypted.
2009         4. update_engine answers a simple status request over DBus.
2010
2011        """
2012        super(CrosHost, self).verify_software()
2013        default_kilo_inodes_required = CONFIG.get_config_value(
2014                'SERVER', 'kilo_inodes_required', type=int, default=100)
2015        board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
2016        kilo_inodes_required = CONFIG.get_config_value(
2017                'SERVER', 'kilo_inodes_required_%s' % board,
2018                type=int, default=default_kilo_inodes_required)
2019        self.check_inodes('/mnt/stateful_partition', kilo_inodes_required)
2020        self.check_diskspace(
2021            '/mnt/stateful_partition',
2022            CONFIG.get_config_value(
2023                'SERVER', 'gb_diskspace_required', type=float,
2024                default=20.0))
2025        encrypted_stateful_path = '/mnt/stateful_partition/encrypted'
2026        # Not all targets build with encrypted stateful support.
2027        if self.path_exists(encrypted_stateful_path):
2028            self.check_diskspace(
2029                encrypted_stateful_path,
2030                CONFIG.get_config_value(
2031                    'SERVER', 'gb_encrypted_diskspace_required', type=float,
2032                    default=0.1))
2033
2034        self.wait_for_system_services()
2035
2036        # Factory images don't run update engine,
2037        # goofy controls dbus on these DUTs.
2038        if not self._is_factory_image():
2039            self.run('update_engine_client --status')
2040
2041
2042    @retry.retry(error.AutoservError, timeout_min=5, delay_sec=10)
2043    def wait_for_service(self, service_name):
2044        """Wait for target status of an upstart init script.
2045
2046        @param service_name: Service to wait for.
2047        """
2048        if not self.upstart_status(service_name):
2049            raise error.AutoservError('Service %s not running.' % service_name)
2050
2051    def wait_for_system_services(self):
2052        """Waits for system-services to be running.
2053
2054        Sometimes, update_engine will take a while to update firmware, so we
2055        should give this some time to finish. See crbug.com/765686#c38 for
2056        details.
2057        """
2058        self.wait_for_service('system-services')
2059
2060
2061    def verify(self):
2062        """Verify ChromeOS system is in good state."""
2063        message = 'Beginning verify for host %s board %s model %s'
2064        info = self.host_info_store.get()
2065        message %= (self.hostname, info.board, info.model)
2066        self.record('INFO', None, None, message)
2067        try:
2068            self._repair_strategy.verify(self)
2069        except hosts.AutoservVerifyDependencyError as e:
2070            # We don't want flag a DUT as failed if only non-critical
2071            # verifier(s) failed during the repair.
2072            if e.is_critical():
2073                raise
2074
2075
2076    def make_ssh_command(self,
2077                         user='root',
2078                         port=None,
2079                         opts='',
2080                         hosts_file=None,
2081                         connect_timeout=None,
2082                         alive_interval=None,
2083                         alive_count_max=None,
2084                         connection_attempts=None):
2085        """Override default make_ssh_command to use options tuned for ChromeOS.
2086
2087        Tuning changes:
2088          - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
2089          connection failure.  Consistency with remote_access.sh.
2090
2091          - ServerAliveInterval=900; which causes SSH to ping connection every
2092          900 seconds. In conjunction with ServerAliveCountMax ensures
2093          that if the connection dies, Autotest will bail out.
2094          Originally tried 60 secs, but saw frequent job ABORTS where
2095          the test completed successfully. Later increased from 180 seconds to
2096          900 seconds to account for tests where the DUT is suspended for
2097          longer periods of time.
2098
2099          - ServerAliveCountMax=3; consistency with remote_access.sh.
2100
2101          - ConnectAttempts=4; reduce flakiness in connection errors;
2102          consistency with remote_access.sh.
2103
2104          - UserKnownHostsFile=/dev/null; we don't care about the keys.
2105          Host keys change with every new installation, don't waste
2106          memory/space saving them.
2107
2108          - SSH protocol forced to 2; needed for ServerAliveInterval.
2109
2110        @param user User name to use for the ssh connection.
2111        @param port Port on the target host to use for ssh connection.
2112        @param opts Additional options to the ssh command.
2113        @param hosts_file Ignored.
2114        @param connect_timeout Ignored.
2115        @param alive_interval Ignored.
2116        @param alive_count_max Ignored.
2117        @param connection_attempts Ignored.
2118        """
2119        options = ' '.join([opts, '-o Protocol=2'])
2120        return super(CrosHost, self).make_ssh_command(
2121            user=user, port=port, opts=options, hosts_file='/dev/null',
2122            connect_timeout=30, alive_interval=900, alive_count_max=3,
2123            connection_attempts=4)
2124
2125
2126    def syslog(self, message, tag='autotest'):
2127        """Logs a message to syslog on host.
2128
2129        @param message String message to log into syslog
2130        @param tag String tag prefix for syslog
2131
2132        """
2133        self.run('logger -t "%s" "%s"' % (tag, message))
2134
2135
2136    def _ping_check_status(self, status):
2137        """Ping the host once, and return whether it has a given status.
2138
2139        @param status Check the ping status against this value.
2140        @return True iff `status` and the result of ping are the same
2141                (i.e. both True or both False).
2142
2143        """
2144        ping_val = utils.ping(self.hostname,
2145                              tries=1,
2146                              deadline=1,
2147                              timeout=2,
2148                              ignore_timeout=True)
2149        return not (status ^ (ping_val == 0))
2150
2151    def _ping_wait_for_status(self, status, timeout):
2152        """Wait for the host to have a given status (UP or DOWN).
2153
2154        Status is checked by polling.  Polling will not last longer
2155        than the number of seconds in `timeout`.  The polling
2156        interval will be long enough that only approximately
2157        _PING_WAIT_COUNT polling cycles will be executed, subject
2158        to a maximum interval of about one minute.
2159
2160        @param status Waiting will stop immediately if `ping` of the
2161                      host returns this status.
2162        @param timeout Poll for at most this many seconds.
2163        @return True iff the host status from `ping` matched the
2164                requested status at the time of return.
2165
2166        """
2167        # _ping_check_status() takes about 1 second, hence the
2168        # "- 1" in the formula below.
2169        # FIXME: if the ping command errors then _ping_check_status()
2170        # returns instantly. If timeout is also smaller than twice
2171        # _PING_WAIT_COUNT then the while loop below forks many
2172        # thousands of ping commands (see /tmp/test_that_results_XXXXX/
2173        # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one
2174        # CPU core for 60 seconds.
2175        poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1
2176        end_time = time.time() + timeout
2177        while time.time() <= end_time:
2178            if self._ping_check_status(status):
2179                return True
2180            if poll_interval > 0:
2181                time.sleep(poll_interval)
2182
2183        # The last thing we did was sleep(poll_interval), so it may
2184        # have been too long since the last `ping`.  Check one more
2185        # time, just to be sure.
2186        return self._ping_check_status(status)
2187
2188    def ping_wait_up(self, timeout):
2189        """Wait for the host to respond to `ping`.
2190
2191        N.B.  This method is not a reliable substitute for
2192        `wait_up()`, because a host that responds to ping will not
2193        necessarily respond to ssh.  This method should only be used
2194        if the target DUT can be considered functional even if it
2195        can't be reached via ssh.
2196
2197        @param timeout Minimum time to allow before declaring the
2198                       host to be non-responsive.
2199        @return True iff the host answered to ping before the timeout.
2200
2201        """
2202        if self.use_icmp:
2203            return self._ping_wait_for_status(self._PING_STATUS_UP, timeout)
2204        else:
2205            logging.debug('Using SSH instead of ICMP for ping_wait_up.')
2206            return self.wait_up(timeout)
2207
2208    def ping_wait_down(self, timeout):
2209        """Wait until the host no longer responds to `ping`.
2210
2211        This function can be used as a slightly faster version of
2212        `wait_down()`, by avoiding potentially long ssh timeouts.
2213
2214        @param timeout Minimum time to allow for the host to become
2215                       non-responsive.
2216        @return True iff the host quit answering ping before the
2217                timeout.
2218
2219        """
2220        if self.use_icmp:
2221            return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout)
2222        else:
2223            logging.debug('Using SSH instead of ICMP for ping_wait_down.')
2224            return self.wait_down(timeout)
2225
2226    def _is_host_port_forwarded(self):
2227        """Checks if the dut is connected over port forwarding.
2228
2229      N.B. This method does not detect all situations where port forwarding is
2230      occurring. Namely, running autotest on the dut may result in a
2231      false-positive, and port forwarding using a different machine on the
2232      same network will be a false-negative.
2233
2234      @return True if the dut is connected over port forwarding
2235              False otherwise
2236      """
2237        is_localhost = self.hostname in ['localhost', '127.0.0.1']
2238        is_forwarded = is_localhost and not self.is_default_port
2239        if is_forwarded:
2240            logging.info('Detected DUT connected by port forwarding')
2241        return is_forwarded
2242
2243    def test_wait_for_sleep(self, sleep_timeout=None):
2244        """Wait for the client to enter low-power sleep mode.
2245
2246        The test for "is asleep" can't distinguish a system that is
2247        powered off; to confirm that the unit was asleep, it is
2248        necessary to force resume, and then call
2249        `test_wait_for_resume()`.
2250
2251        This function is expected to be called from a test as part
2252        of a sequence like the following:
2253
2254        ~~~~~~~~
2255            boot_id = host.get_boot_id()
2256            # trigger sleep on the host
2257            host.test_wait_for_sleep()
2258            # trigger resume on the host
2259            host.test_wait_for_resume(boot_id)
2260        ~~~~~~~~
2261
2262        @param sleep_timeout time limit in seconds to allow the host sleep.
2263
2264        @exception TestFail The host did not go to sleep within
2265                            the allowed time.
2266        """
2267        if sleep_timeout is None:
2268            sleep_timeout = self.SLEEP_TIMEOUT
2269
2270        # If the dut is accessed over SSH port-forwarding, `ping` is not useful
2271        # for detecting the dut is down since a ping to localhost will always
2272        # succeed. In this case, fall back to wait_down() which uses SSH.
2273        if self._is_host_port_forwarded():
2274            success = self.wait_down(timeout=sleep_timeout)
2275        else:
2276            success = self.ping_wait_down(timeout=sleep_timeout)
2277
2278        if not success:
2279            raise error.TestFail(
2280                'client failed to sleep after %d seconds' % sleep_timeout)
2281
2282
2283    def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
2284        """Wait for the client to resume from low-power sleep mode.
2285
2286        The `old_boot_id` parameter should be the value from
2287        `get_boot_id()` obtained prior to entering sleep mode.  A
2288        `TestFail` exception is raised if the boot id changes.
2289
2290        See @ref test_wait_for_sleep for more on this function's
2291        usage.
2292
2293        @param old_boot_id A boot id value obtained before the
2294                               target host went to sleep.
2295        @param resume_timeout time limit in seconds to allow the host up.
2296
2297        @exception TestFail The host did not respond within the
2298                            allowed time.
2299        @exception TestFail The host responded, but the boot id test
2300                            indicated a reboot rather than a sleep
2301                            cycle.
2302        """
2303        if resume_timeout is None:
2304            resume_timeout = self.RESUME_TIMEOUT
2305
2306        if not self.wait_up(timeout=resume_timeout):
2307            raise error.TestFail(
2308                'client failed to resume from sleep after %d seconds' %
2309                    resume_timeout)
2310        else:
2311            new_boot_id = self.get_boot_id()
2312            if new_boot_id != old_boot_id:
2313                logging.error('client rebooted (old boot %s, new boot %s)',
2314                              old_boot_id, new_boot_id)
2315                raise error.TestFail(
2316                    'client rebooted, but sleep was expected')
2317
2318
2319    def test_wait_for_shutdown(self, shutdown_timeout=None):
2320        """Wait for the client to shut down.
2321
2322        The test for "has shut down" can't distinguish a system that
2323        is merely asleep; to confirm that the unit was down, it is
2324        necessary to force boot, and then call test_wait_for_boot().
2325
2326        This function is expected to be called from a test as part
2327        of a sequence like the following:
2328
2329        ~~~~~~~~
2330            boot_id = host.get_boot_id()
2331            # trigger shutdown on the host
2332            host.test_wait_for_shutdown()
2333            # trigger boot on the host
2334            host.test_wait_for_boot(boot_id)
2335        ~~~~~~~~
2336
2337        @param shutdown_timeout time limit in seconds to allow the host down.
2338        @exception TestFail The host did not shut down within the
2339                            allowed time.
2340        """
2341        if shutdown_timeout is None:
2342            shutdown_timeout = self.SHUTDOWN_TIMEOUT
2343
2344        if self._is_host_port_forwarded():
2345            success = self.wait_down(timeout=shutdown_timeout)
2346        else:
2347            success = self.ping_wait_down(timeout=shutdown_timeout)
2348
2349        if not success:
2350            raise error.TestFail(
2351                'client failed to shut down after %d seconds' %
2352                    shutdown_timeout)
2353
2354
2355    def test_wait_for_boot(self, old_boot_id=None):
2356        """Wait for the client to boot from cold power.
2357
2358        The `old_boot_id` parameter should be the value from
2359        `get_boot_id()` obtained prior to shutting down.  A
2360        `TestFail` exception is raised if the boot id does not
2361        change.  The boot id test is omitted if `old_boot_id` is not
2362        specified.
2363
2364        See @ref test_wait_for_shutdown for more on this function's
2365        usage.
2366
2367        @param old_boot_id A boot id value obtained before the
2368                               shut down.
2369
2370        @exception TestFail The host did not respond within the
2371                            allowed time.
2372        @exception TestFail The host responded, but the boot id test
2373                            indicated that there was no reboot.
2374        """
2375        if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
2376            raise error.TestFail(
2377                'client failed to reboot after %d seconds' %
2378                    self.REBOOT_TIMEOUT)
2379        elif old_boot_id:
2380            if self.get_boot_id() == old_boot_id:
2381                logging.error('client not rebooted (boot %s)',
2382                              old_boot_id)
2383                raise error.TestFail(
2384                    'client is back up, but did not reboot')
2385
2386
2387    @staticmethod
2388    def check_for_rpm_support(hostname):
2389        """For a given hostname, return whether or not it is powered by an RPM.
2390
2391        @param hostname: hostname to check for rpm support.
2392
2393        @return None if this host does not follows the defined naming format
2394                for RPM powered DUT's in the lab. If it does follow the format,
2395                it returns a regular expression MatchObject instead.
2396        """
2397        return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
2398
2399
2400    def has_power(self):
2401        """For this host, return whether or not it is powered by an RPM.
2402
2403        @return True if this host is in the CROS lab and follows the defined
2404                naming format.
2405        """
2406        return CrosHost.check_for_rpm_support(self.hostname)
2407
2408
2409    def _set_power(self, state, power_method):
2410        """Sets the power to the host via RPM, CCD, Servo or manual.
2411
2412        @param state Specifies which power state to set to DUT
2413        @param power_method Specifies which method of power control to
2414                            use. By default "RPM" or "CCD" will be used based
2415                            on servo type. Valid values from
2416                            POWER_CONTROL_VALID_ARGS, or None to use default.
2417
2418        """
2419        ACCEPTABLE_STATES = ['ON', 'OFF']
2420
2421        if not power_method:
2422            power_method = self.get_default_power_method()
2423
2424        state = state.upper()
2425        if state not in ACCEPTABLE_STATES:
2426            raise error.TestError('State must be one of: %s.'
2427                                   % (ACCEPTABLE_STATES,))
2428
2429        if power_method == self.POWER_CONTROL_SERVO:
2430            logging.info('Setting servo port J10 to %s', state)
2431            self.servo.set('prtctl3_pwren', state.lower())
2432            time.sleep(self._USB_POWER_TIMEOUT)
2433        elif power_method == self.POWER_CONTROL_MANUAL:
2434            logging.info('You have %d seconds to set the AC power to %s.',
2435                         self._POWER_CYCLE_TIMEOUT, state)
2436            time.sleep(self._POWER_CYCLE_TIMEOUT)
2437        elif power_method == self.POWER_CONTROL_CCD:
2438            servo_role = 'src' if state == 'ON' else 'snk'
2439            logging.info('servo ccd power pass through detected,'
2440                         ' changing servo_role to %s.', servo_role)
2441            self.servo.set_servo_v4_role(servo_role)
2442            if not self.ping_wait_up(timeout=self._CHANGE_SERVO_ROLE_TIMEOUT):
2443                # Make sure we don't leave DUT with no power(servo_role=snk)
2444                # when DUT is not pingable, as we raise a exception here
2445                # that may break a power cycle in the middle.
2446                self.servo.set_servo_v4_role('src')
2447                raise error.AutoservError(
2448                    'DUT failed to regain network connection after %d seconds.'
2449                    % self._CHANGE_SERVO_ROLE_TIMEOUT)
2450        else:
2451            if not self.has_power():
2452                raise error.TestFail('DUT does not have RPM connected.')
2453            self._add_rpm_changed_tag()
2454            rpm_client.set_power(self, state, timeout_mins=5)
2455
2456
2457    def power_off(self, power_method=None):
2458        """Turn off power to this host via RPM, CCD, Servo or manual.
2459
2460        @param power_method Specifies which method of power control to
2461                            use. By default "RPM" or "CCD" will be used based
2462                            on servo type. Valid values from
2463                            POWER_CONTROL_VALID_ARGS, or None to use default.
2464
2465        """
2466        self._sync_if_up()
2467        self._set_power('OFF', power_method)
2468
2469    def _check_supported(self):
2470        """Throw an error if dts mode control is not supported."""
2471        if not self.servo_pwr_supported:
2472            raise error.TestFail('power_state controls not supported')
2473
2474    def _sync_if_up(self):
2475        """Run sync on the DUT and wait for completion if the DUT is up.
2476
2477        Additionally, try to sync and ignore status if its not up.
2478
2479        Useful prior to reboots to ensure files are written to disc.
2480
2481        """
2482        if self.is_up_fast():
2483            self.run("sync")
2484            return
2485        # If it is not up, attempt to sync in the rare event the DUT is up but
2486        # doesn't respond to a ping. Ignore any errors.
2487        try:
2488            self.run("sync", ignore_status=True, timeout=1)
2489        except Exception:
2490            pass
2491
2492    def power_off_via_servo(self):
2493        """Force the DUT to power off.
2494
2495        The DUT is guaranteed to be off at the end of this call,
2496        regardless of its previous state, provided that there is
2497        working EC and boot firmware.  There is no requirement for
2498        working OS software.
2499
2500        """
2501        self._check_supported()
2502        self._sync_if_up()
2503        self.servo.set_nocheck('power_state', 'off')
2504
2505    def power_on_via_servo(self, rec_mode='on'):
2506        """Force the DUT to power on.
2507
2508        Prior to calling this function, the DUT must be powered off,
2509        e.g. with a call to `power_off()`.
2510
2511        At power on, recovery mode is set as specified by the
2512        corresponding argument.  When booting with recovery mode on, it
2513        is the caller's responsibility to unplug/plug in a bootable
2514        external storage device.
2515
2516        If the DUT requires a delay after powering on but before
2517        processing inputs such as USB stick insertion, the delay is
2518        handled by this method; the caller is not responsible for such
2519        delays.
2520
2521        @param rec_mode Setting of recovery mode to be applied at
2522                        power on. default: REC_OFF aka 'off'
2523
2524        """
2525        self._check_supported()
2526        self.servo.set_nocheck('power_state', rec_mode)
2527
2528    def reset_via_servo(self):
2529        """Force the DUT to reset.
2530
2531        The DUT is guaranteed to be on at the end of this call,
2532        regardless of its previous state, provided that there is
2533        working OS software. This also guarantees that the EC has
2534        been restarted.
2535
2536        """
2537        self._check_supported()
2538        self._sync_if_up()
2539        self.servo.set_nocheck('power_state', 'reset')
2540
2541
2542    def power_on(self, power_method=None):
2543        """Turn on power to this host via RPM, CCD, Servo or manual.
2544
2545        @param power_method Specifies which method of power control to
2546                            use. By default "RPM" or "CCD" will be used based
2547                            on servo type. Valid values from
2548                            POWER_CONTROL_VALID_ARGS, or None to use default.
2549
2550        """
2551        self._set_power('ON', power_method)
2552
2553
2554    def power_cycle(self, power_method=None):
2555        """Cycle power to this host by turning it OFF, then ON.
2556
2557        @param power_method Specifies which method of power control to
2558                            use. By default "RPM" or "CCD" will be used based
2559                            on servo type. Valid values from
2560                            POWER_CONTROL_VALID_ARGS, or None to use default.
2561
2562        """
2563        if not power_method:
2564            power_method = self.get_default_power_method()
2565
2566        if power_method in (self.POWER_CONTROL_SERVO,
2567                            self.POWER_CONTROL_MANUAL,
2568                            self.POWER_CONTROL_CCD):
2569            self.power_off(power_method=power_method)
2570            time.sleep(self._POWER_CYCLE_TIMEOUT)
2571            self.power_on(power_method=power_method)
2572        else:
2573            self._add_rpm_changed_tag()
2574            rpm_client.set_power(self, 'CYCLE')
2575
2576
2577    def get_platform_from_fwid(self):
2578        """Determine the platform from the crossystem fwid.
2579
2580        @returns a string representing this host's platform.
2581        """
2582        # Look at the firmware for non-unibuild cases or if cros_config fails.
2583        crossystem = utils.Crossystem(self)
2584        crossystem.init()
2585        # Extract fwid value and use the leading part as the platform id.
2586        # fwid generally follow the format of {platform}.{firmware version}
2587        # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z
2588        platform = crossystem.fwid().split('.')[0].lower()
2589        # Newer platforms start with 'Google_' while the older ones do not.
2590        return platform.replace('google_', '')
2591
2592
2593    def get_platform(self):
2594        """Determine the correct platform label for this host.
2595
2596        @returns a string representing this host's platform.
2597        """
2598        release_info = utils.parse_cmd_output('cat /etc/lsb-release',
2599                                              run_method=self.run)
2600        platform = ''
2601        if release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1':
2602            platform = self.get_model_from_cros_config()
2603        return platform if platform else self.get_platform_from_fwid()
2604
2605
2606    def get_model_from_cros_config(self):
2607        """Get the host model from cros_config command.
2608
2609        @returns a string representing this host's model.
2610        """
2611        return cros_config.call_cros_config_get_output('/ name',
2612                self.run, ignore_status=True)
2613
2614
2615    def get_architecture(self):
2616        """Determine the correct architecture label for this host.
2617
2618        @returns a string representing this host's architecture.
2619        """
2620        crossystem = utils.Crossystem(self)
2621        crossystem.init()
2622        return crossystem.arch()
2623
2624
2625    def get_chrome_version(self):
2626        """Gets the Chrome version number and milestone as strings.
2627
2628        Invokes "chrome --version" to get the version number and milestone.
2629
2630        @return A tuple (chrome_ver, milestone) where "chrome_ver" is the
2631            current Chrome version number as a string (in the form "W.X.Y.Z")
2632            and "milestone" is the first component of the version number
2633            (the "W" from "W.X.Y.Z").  If the version number cannot be parsed
2634            in the "W.X.Y.Z" format, the "chrome_ver" will be the full output
2635            of "chrome --version" and the milestone will be the empty string.
2636
2637        """
2638        version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout
2639        return utils.parse_chrome_version(version_string)
2640
2641
2642    def get_ec_version(self):
2643        """Get the ec version as strings.
2644
2645        @returns a string representing this host's ec version.
2646        """
2647        command = 'mosys ec info -s fw_version'
2648        result = self.run(command, ignore_status=True)
2649        if result.exit_status != 0:
2650            return ''
2651        return result.stdout.strip()
2652
2653
2654    def get_firmware_version(self):
2655        """Get the firmware version as strings.
2656
2657        @returns a string representing this host's firmware version.
2658        """
2659        crossystem = utils.Crossystem(self)
2660        crossystem.init()
2661        return crossystem.fwid()
2662
2663
2664    def get_hardware_id(self):
2665        """Get hardware id as strings.
2666
2667        @returns a string representing this host's hardware id.
2668        """
2669        crossystem = utils.Crossystem(self)
2670        crossystem.init()
2671        return crossystem.hwid()
2672
2673    def get_hardware_revision(self):
2674        """Get the hardware revision as strings.
2675
2676        @returns a string representing this host's hardware revision.
2677        """
2678        command = 'mosys platform version'
2679        result = self.run(command, ignore_status=True)
2680        if result.exit_status != 0:
2681            return ''
2682        return result.stdout.strip()
2683
2684
2685    def get_kernel_version(self):
2686        """Get the kernel version as strings.
2687
2688        @returns a string representing this host's kernel version.
2689        """
2690        return self.run('uname -r').stdout.strip()
2691
2692
2693    def get_cpu_name(self):
2694        """Get the cpu name as strings.
2695
2696        @returns a string representing this host's cpu name.
2697        """
2698
2699        # Try get cpu name from device tree first
2700        if self.path_exists('/proc/device-tree/compatible'):
2701            command = ' | '.join(
2702                    ["sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible",
2703                     'tail -1'])
2704            return self.run(command).stdout.strip().replace(',', ' ')
2705
2706        # Get cpu name from uname -p
2707        command = 'uname -p'
2708        ret = self.run(command).stdout.strip()
2709
2710        # 'uname -p' return variant of unknown or amd64 or x86_64 or i686
2711        # Try get cpu name from /proc/cpuinfo instead
2712        if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE):
2713            command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1"
2714            self = self.run(command).stdout.strip()
2715
2716        # Remove bloat from CPU name, for example
2717        # Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz       -> Intel Core i5-7Y57
2718        # Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz     -> Intel Xeon E5-2690 v4
2719        # AMD A10-7850K APU with Radeon(TM) R7 Graphics -> AMD A10-7850K
2720        # AMD GX-212JC SOC with Radeon(TM) R2E Graphics -> AMD GX-212JC
2721        trim_re = r' (@|processor|apu|soc|radeon).*|\(.*?\)| cpu'
2722        return re.sub(trim_re, '', ret, flags=re.IGNORECASE)
2723
2724
2725    def get_screen_resolution(self):
2726        """Get the screen(s) resolution as strings.
2727        In case of more than 1 monitor, return resolution for each monitor
2728        separate with plus sign.
2729
2730        @returns a string representing this host's screen(s) resolution.
2731        """
2732        command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done'
2733        ret = self.run(command, ignore_status=True)
2734        # We might have Chromebox without a screen
2735        if ret.exit_status != 0:
2736            return ''
2737        return ret.stdout.strip().replace('\n', '+')
2738
2739
2740    def get_mem_total_gb(self):
2741        """Get total memory available in the system in GiB (2^20).
2742
2743        @returns an integer representing total memory
2744        """
2745        mem_total_kb = self.read_from_meminfo('MemTotal')
2746        kb_in_gb = float(2 ** 20)
2747        return int(round(mem_total_kb / kb_in_gb))
2748
2749
2750    def get_disk_size_gb(self):
2751        """Get size of disk in GB (10^9)
2752
2753        @returns an integer representing  size of disk, 0 in Error Case
2754        """
2755        command = 'grep $(rootdev -s -d | cut -f3 -d/)$ /proc/partitions'
2756        result = self.run(command, ignore_status=True)
2757        if result.exit_status != 0:
2758            return 0
2759        _, _, block, _ = re.split(r' +', result.stdout.strip())
2760        byte_per_block = 1024.0
2761        disk_kb_in_gb = 1e9
2762        return int(int(block) * byte_per_block / disk_kb_in_gb + 0.5)
2763
2764
2765    def get_battery_size(self):
2766        """Get size of battery in Watt-hour via sysfs
2767
2768        This method assumes that battery support voltage_min_design and
2769        charge_full_design sysfs.
2770
2771        @returns a float representing Battery size, 0 if error.
2772        """
2773        # sysfs report data in micro scale
2774        battery_scale = 1e6
2775
2776        command = 'cat /sys/class/power_supply/*/voltage_min_design'
2777        result = self.run(command, ignore_status=True)
2778        if result.exit_status != 0:
2779            return 0
2780        voltage = float(result.stdout.strip()) / battery_scale
2781
2782        command = 'cat /sys/class/power_supply/*/charge_full_design'
2783        result = self.run(command, ignore_status=True)
2784        if result.exit_status != 0:
2785            return 0
2786        amphereHour = float(result.stdout.strip()) / battery_scale
2787
2788        return voltage * amphereHour
2789
2790
2791    def get_low_battery_shutdown_percent(self):
2792        """Get the percent-based low-battery shutdown threshold.
2793
2794        @returns a float representing low-battery shutdown percent, 0 if error.
2795        """
2796        ret = 0.0
2797        try:
2798            command = 'check_powerd_config --low_battery_shutdown_percent'
2799            ret = float(self.run(command).stdout)
2800        except error.CmdError:
2801            logging.debug("Can't run %s", command)
2802        except ValueError:
2803            logging.debug("Didn't get number from %s", command)
2804
2805        return ret
2806
2807
2808    def has_hammer(self):
2809        """Check whether DUT has hammer device or not.
2810
2811        @returns boolean whether device has hammer or not
2812        """
2813        command = 'grep Hammer /sys/bus/usb/devices/*/product'
2814        return self.run(command, ignore_status=True).exit_status == 0
2815
2816
2817    def is_chrome_switch_present(self, switch):
2818        """Returns True if the specified switch was provided to Chrome.
2819
2820        @param switch The chrome switch to search for.
2821        """
2822
2823        command = 'pgrep -x -f -c "/opt/google/chrome/chrome.*%s.*"' % switch
2824        return self.run(command, ignore_status=True).exit_status == 0
2825
2826
2827    def oobe_triggers_update(self):
2828        """Returns True if this host has an OOBE flow during which
2829        it will perform an update check and perhaps an update.
2830        One example of such a flow is Hands-Off Zero-Touch Enrollment.
2831        As more such flows are developed, code handling them needs
2832        to be added here.
2833
2834        @return Boolean indicating whether this host's OOBE triggers an update.
2835        """
2836        return self.is_chrome_switch_present(
2837            '--enterprise-enable-zero-touch-enrollment=hands-off')
2838
2839
2840    # TODO(kevcheng): change this to just return the board without the
2841    # 'board:' prefix and fix up all the callers.  Also look into removing the
2842    # need for this method.
2843    def get_board(self):
2844        """Determine the correct board label for this host.
2845
2846        @returns a string representing this host's board.
2847        """
2848        release_info = utils.parse_cmd_output('cat /etc/lsb-release',
2849                                              run_method=self.run)
2850        return (ds_constants.BOARD_PREFIX +
2851                release_info['CHROMEOS_RELEASE_BOARD'])
2852
2853    def get_channel(self):
2854        """Determine the correct channel label for this host.
2855
2856        @returns: a string represeting this host's build channel.
2857                  (stable, dev, beta). None on fail.
2858        """
2859        return lsbrelease_utils.get_chromeos_channel(
2860                lsb_release_content=self._get_lsb_release_content())
2861
2862    def get_power_supply(self):
2863        """
2864        Determine what type of power supply the host has
2865
2866        @returns a string representing this host's power supply.
2867                 'power:battery' when the device has a battery intended for
2868                        extended use
2869                 'power:AC_primary' when the device has a battery not intended
2870                        for extended use (for moving the machine, etc)
2871                 'power:AC_only' when the device has no battery at all.
2872        """
2873        psu = self.run(command='cros_config /hardware-properties psu-type',
2874                       ignore_status=True)
2875        if psu.exit_status:
2876            # Assume battery if unspecified in cros_config.
2877            return 'power:battery'
2878
2879        psu_str = psu.stdout.strip()
2880        if psu_str == 'unknown':
2881            return None
2882
2883        return 'power:%s' % psu_str
2884
2885
2886    def has_battery(self):
2887        """Determine if DUT has a battery.
2888
2889        Returns:
2890            Boolean, False if known not to have battery, True otherwise.
2891        """
2892        return self.get_power_supply() == 'power:battery'
2893
2894
2895    def get_servo(self):
2896        """Determine if the host has a servo attached.
2897
2898        If the host has a working servo attached, it should have a servo label.
2899
2900        @return: string 'servo' if the host has servo attached. Otherwise,
2901                 returns None.
2902        """
2903        return 'servo' if self._servo_host else None
2904
2905    def _has_display(self, internal):
2906        """ Determine if the device under test is equipped with a display
2907        @params internal: True if checking internal display else checking
2908                          external display.
2909        @return: 'internal_display' if internal is true and internal display
2910                 present;
2911                 'external_display' if internal is false and external display
2912                 present;
2913                 None otherwise.
2914        """
2915        from autotest_lib.client.cros.graphics import graphics_utils
2916        from autotest_lib.client.common_lib import utils as common_utils
2917
2918        def __system_output(cmd):
2919            return self.run(cmd).stdout
2920
2921        def __read_file(remote_path):
2922            return self.run('cat %s' % remote_path).stdout
2923
2924        # Hijack the necessary client functions so that we can take advantage
2925        # of the client lib here.
2926        # FIXME: find a less hacky way than this
2927        original_system_output = utils.system_output
2928        original_read_file = common_utils.read_file
2929        utils.system_output = __system_output
2930        common_utils.read_file = __read_file
2931        try:
2932            if internal:
2933                return ('internal_display'
2934                        if graphics_utils.has_internal_display() else None)
2935            else:
2936                return ('external_display'
2937                        if graphics_utils.has_external_display() else None)
2938        finally:
2939            utils.system_output = original_system_output
2940            common_utils.read_file = original_read_file
2941
2942
2943    def has_internal_display(self):
2944        """Determine if the device under test is equipped with an internal
2945        display.
2946
2947        @return: 'internal_display' if one is present; None otherwise.
2948        """
2949        return self._has_display(True)
2950
2951    def has_external_display(self):
2952        """Determine if the device under test is equipped with an external
2953        display.
2954
2955        @return: 'external_display' if one is present; None otherwise.
2956        """
2957        return self._has_display(False)
2958
2959    def is_boot_from_usb(self):
2960        """Check if DUT is boot from USB.
2961
2962        @return: True if DUT is boot from usb.
2963        """
2964        device = self.run('rootdev -s -d').stdout.strip()
2965        removable = int(self.run('cat /sys/block/%s/removable' %
2966                                 os.path.basename(device)).stdout.strip())
2967        return removable == 1
2968
2969    def is_boot_from_external_device(self):
2970        """Check if DUT is boot from external storage.
2971
2972        @return: True if DUT is boot from external storage.
2973        """
2974        boot_device = self.run('rootdev -s -d', ignore_status=True,
2975                               timeout=60).stdout.strip()
2976        if not boot_device:
2977            logging.debug('Boot storage not detected on the host.')
2978            return False
2979        main_storage_cmd = ('. /usr/sbin/write_gpt.sh;'
2980                            ' . /usr/share/misc/chromeos-common.sh;'
2981                            ' load_base_vars; get_fixed_dst_drive')
2982        main_storage = self.run(main_storage_cmd,
2983                                ignore_status=True,
2984                                timeout=60).stdout.strip()
2985        if not main_storage or boot_device != main_storage:
2986            logging.debug('Device booted from external storage storage.')
2987            return True
2988        logging.debug('Device booted from main storage.')
2989        return False
2990
2991    def read_from_meminfo(self, key):
2992        """Return the memory info from /proc/meminfo
2993
2994        @param key: meminfo requested
2995
2996        @return the memory value as a string
2997
2998        """
2999        meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip()
3000        logging.debug('%s', meminfo)
3001        return int(re.search(r'\d+', meminfo).group(0))
3002
3003
3004    def get_cpu_arch(self):
3005        """Returns CPU arch of the device.
3006
3007        @return CPU architecture of the DUT.
3008        """
3009        # Add CPUs by following logic in client/bin/utils.py.
3010        if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo",
3011                ignore_status=True).stdout:
3012            return 'x86_64'
3013        if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo",
3014                ignore_status=True).stdout:
3015            return 'arm'
3016        return 'i386'
3017
3018
3019    def get_board_type(self):
3020        """
3021        Get the DUT's device type / form factor from cros_config. It can be one
3022        of CHROMEBOX, CHROMEBASE, CHROMEBOOK, or CHROMEBIT.
3023
3024        @return form factor value from cros_config.
3025        """
3026
3027        device_type = self.run('cros_config /hardware-properties form-factor',
3028                ignore_status=True).stdout
3029        if device_type:
3030            return device_type
3031
3032        # TODO: remove lsb-release fallback once cros_config works everywhere
3033        device_type = self.run('grep DEVICETYPE /etc/lsb-release',
3034                               ignore_status=True).stdout
3035        if device_type:
3036            return device_type.split('=')[-1].strip()
3037        return ''
3038
3039
3040    def get_arc_version(self):
3041        """Return ARC version installed on the DUT.
3042
3043        @returns ARC version as string if the CrOS build has ARC, else None.
3044        """
3045        arc_version = self.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release',
3046                               ignore_status=True).stdout
3047        if arc_version:
3048            return arc_version.split('=')[-1].strip()
3049        return None
3050
3051
3052    def get_os_type(self):
3053        return 'cros'
3054
3055
3056    def get_labels(self):
3057        """Return the detected labels on the host."""
3058        return self.labels.get_labels(self)
3059
3060
3061    def get_default_power_method(self):
3062        """
3063        Get the default power method for power_on/off/cycle() methods.
3064        @return POWER_CONTROL_RPM or POWER_CONTROL_CCD
3065        """
3066        if not self._default_power_method:
3067            self._default_power_method = self.POWER_CONTROL_RPM
3068            if self.servo and self.servo.supports_built_in_pd_control():
3069                self._default_power_method = self.POWER_CONTROL_CCD
3070            else:
3071                logging.debug('Either servo is unitialized or the servo '
3072                              'setup does not support pd controls. Falling '
3073                              'back to default RPM method.')
3074        return self._default_power_method
3075
3076
3077    def find_usb_devices(self, idVendor, idProduct):
3078        """
3079        Get usb device sysfs name for specific device.
3080
3081        @param idVendor  Vendor ID to search in sysfs directory.
3082        @param idProduct Product ID to search in sysfs directory.
3083
3084        @return Usb node names in /sys/bus/usb/drivers/usb/ that match.
3085        """
3086        # Look for matching file and cut at position 7 to get dir name.
3087        grep_cmd = 'grep {} /sys/bus/usb/drivers/usb/*/{} | cut -f 7 -d /'
3088
3089        vendor_cmd = grep_cmd.format(idVendor, 'idVendor')
3090        product_cmd = grep_cmd.format(idProduct, 'idProduct')
3091
3092        # Use uniq -d to print duplicate line from both command
3093        cmd = 'sort <({}) <({}) | uniq -d'.format(vendor_cmd, product_cmd)
3094
3095        return self.run(cmd, ignore_status=True).stdout.strip().split('\n')
3096
3097
3098    def bind_usb_device(self, usb_node):
3099        """
3100        Bind usb device
3101
3102        @param usb_node Node name in /sys/bus/usb/drivers/usb/
3103        """
3104        cmd = 'echo {} > /sys/bus/usb/drivers/usb/bind'.format(usb_node)
3105        self.run(cmd, ignore_status=True)
3106
3107
3108    def unbind_usb_device(self, usb_node):
3109        """
3110        Unbind usb device
3111
3112        @param usb_node Node name in /sys/bus/usb/drivers/usb/
3113        """
3114        cmd = 'echo {} > /sys/bus/usb/drivers/usb/unbind'.format(usb_node)
3115        self.run(cmd, ignore_status=True)
3116
3117
3118    def get_wlan_ip(self):
3119        """
3120        Get ip address of wlan interface.
3121
3122        @return ip address of wlan or empty string if wlan is not connected.
3123        """
3124        cmds = [
3125            'iw dev',                   # List wlan physical device
3126            'grep Interface',           # Grep only interface name
3127            'cut -f 2 -d" "',           # Cut the name part
3128            'xargs ifconfig',           # Feed it to ifconfig to get ip
3129            'grep -oE "inet [0-9.]+"',  # Grep only ipv4
3130            'cut -f 2 -d " "'           # Cut the ip part
3131        ]
3132        return self.run(' | '.join(cmds), ignore_status=True).stdout.strip()
3133
3134    def connect_to_wifi(self, ssid, passphrase=None, security=None):
3135        """
3136        Connect to wifi network
3137
3138        @param ssid       SSID of the wifi network.
3139        @param passphrase Passphrase of the wifi network. None if not existed.
3140        @param security   Security of the wifi network. Default to "psk" if
3141                          passphase is given without security. Possible values
3142                          are "none", "psk", "802_1x".
3143
3144        @return True if succeed, False if not.
3145        """
3146        cmd = '/usr/local/autotest/cros/scripts/wifi connect ' + ssid
3147        if passphrase:
3148            cmd += ' ' + passphrase
3149            if security:
3150                cmd += ' ' + security
3151        return self.run(cmd, ignore_status=True).exit_status == 0
3152
3153    def get_device_repair_state(self):
3154        """Get device repair state"""
3155        return self._device_repair_state
3156
3157    def is_marked_for_replacement(self):
3158        """Verify if device was marked for replacemnet during admin task."""
3159        expected_state = cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT
3160        return self.get_device_repair_state() == expected_state
3161
3162    def set_device_repair_state(self, state, resultdir=None):
3163        """Set device repair state.
3164
3165        The special device state will be written to the 'dut_state.repair'
3166        file in result directory. The file will be read by Lucifer. The
3167        file will not be created if result directory not specified.
3168
3169        @params state:      The new state for the device.
3170        @params resultdir:  The path to result directory. If path not provided
3171                            will be attempt to get retrieve it from job
3172                            if present.
3173        """
3174        resultdir = resultdir or getattr(self.job, 'resultdir', '')
3175        if resultdir:
3176            target = os.path.join(resultdir, 'dut_state.repair')
3177            common_utils.open_write_close(target, state)
3178            logging.info('Set device state as %s. '
3179                         'Created dut_state.repair file.', state)
3180        else:
3181            logging.debug('Cannot write the device state due missing info '
3182                          'about result dir.')
3183        self._device_repair_state = state
3184
3185    def set_device_needs_replacement(self, resultdir=None):
3186        """Set device as required replacement.
3187
3188        @params resultdir:  The path to result directory. If path not provided
3189                            will be attempt to get retrieve it from job
3190                            if present.
3191        """
3192        self.set_device_repair_state(
3193            cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT,
3194            resultdir=resultdir)
3195
3196    def _dut_is_accessible_by_verifier(self):
3197        """Check if DUT accessible by SSH or PING verifier.
3198
3199        @returns: bool, True - verifier marked as success.
3200                        False - result not reachable, verifier did not success.
3201        """
3202        if not self._repair_strategy:
3203            return False
3204        dut_ssh = self._repair_strategy.verifier_is_good('ssh')
3205        dut_ping = self._repair_strategy.verifier_is_good('ping')
3206        return dut_ssh == hosts.VERIFY_SUCCESS or dut_ssh == hosts.VERIFY_SUCCESS
3207
3208    def _stat_if_pingable_but_not_sshable(self):
3209        """Check if DUT pingable but failed SSH verifier."""
3210        if not self._repair_strategy:
3211            return
3212        dut_ssh = self._repair_strategy.verifier_is_good('ssh')
3213        dut_ping = self._repair_strategy.verifier_is_good('ping')
3214        if (dut_ping == hosts.VERIFY_FAILED
3215                    and dut_ssh == hosts.VERIFY_FAILED):
3216            metrics.Counter('chromeos/autotest/dut_pingable_no_ssh').increment(
3217                    fields={'host': self.hostname})
3218
3219    def try_set_device_needs_manual_repair(self):
3220        """Check if device require manual attention to be fixed.
3221
3222        The state 'needs_manual_repair' can be set when auto repair cannot
3223        fix the device due hardware or cable issues.
3224        """
3225        # ignore the logic if state present
3226        # state can be set by any cros repair actions
3227        if self.get_device_repair_state():
3228            return
3229        if self._dut_is_accessible_by_verifier():
3230            # DUT is accessible and we still have many options to repair it.
3231            return
3232        needs_manual_repair = False
3233        dhp = self.health_profile
3234        if dhp and dhp.get_repair_fail_count() > 49:
3235            # 42 = 6 times during 7 days. (every 4 hour repair)
3236            # round up to 50 in case somebody will run some attempt on it.
3237            logging.info(
3238                    'DUT is not sshable and fail %s times.'
3239                    ' Limit to try repair is 50 times',
3240                    dhp.get_repair_fail_count())
3241            needs_manual_repair = True
3242
3243        if not needs_manual_repair:
3244            # We cannot ssh to the DUT and we have hardware or set-up issues
3245            # with servo then we need request manual repair for the DUT.
3246            servo_state_required_manual_fix = [
3247                    servo_constants.SERVO_STATE_DUT_NOT_CONNECTED,
3248                    servo_constants.SERVO_STATE_NEED_REPLACEMENT,
3249            ]
3250            if self.get_servo_state() in servo_state_required_manual_fix:
3251                logging.info(
3252                        'DUT required manual repair because it is not sshable'
3253                        ' and possible have setup issue with Servo. Please'
3254                        ' verify all connections and present of devices.')
3255                needs_manual_repair = True
3256
3257        if needs_manual_repair:
3258            self.set_device_repair_state(
3259                    cros_constants.DEVICE_STATE_NEEDS_MANUAL_REPAIR)
3260
3261    def _reboot_labstation_if_needed(self):
3262        """Place request to reboot the labstation if DUT is not sshable.
3263
3264        @returns: None
3265        """
3266        message_prefix = "Don't need to request servo-host reboot"
3267        if self._dut_is_accessible_by_verifier():
3268            return
3269        if not self._servo_host:
3270            logging.debug('%s as it not initialized', message_prefix)
3271            return
3272        if not self._servo_host.is_up_fast():
3273            logging.debug('%s as servo-host is not sshable', message_prefix)
3274            return
3275        if not self._servo_host.is_labstation():
3276            logging.debug('Servo_v3 is not requested to reboot for the DUT')
3277            return
3278        usb_path = self._servo_host.get_main_servo_usb_path()
3279        if usb_path:
3280            connected_port = os.path.basename(os.path.normpath(usb_path))
3281            # Directly connected servo to the labstation looks like '1-5.3'
3282            # and when connected by hub - '1-5.2.3' or '1-5.2.1.3'. Where:
3283            # - '1-5' - port on labstation
3284            # - '2' or '2.1'   - port on the hub or smart-hub
3285            # - '3'   - port on servo hub
3286            if len(connected_port.split('.')) > 2:
3287                logging.debug('%s as servo connected by hub', message_prefix)
3288                return
3289        self._servo_host.request_reboot()
3290        logging.info('Requested labstation reboot because DUT is not sshable')
3291
3292    def is_file_system_writable(self, testdirs=None):
3293        """Check is the file systems are writable.
3294
3295        The standard linux response to certain unexpected file system errors
3296        (including hardware errors in block devices) is to change the file
3297        system status to read-only. This checks that that hasn't happened.
3298
3299        @param testdirs: List of directories to check. If no data provided
3300                         then '/mnt/stateful_partition' and '/var/tmp'
3301                         directories will be checked.
3302
3303        @returns boolean whether file-system writable.
3304        """
3305        def _check_dir(testdir):
3306            # check if we can create a file
3307            filename = os.path.join(testdir, 'writable_my_test_file')
3308            command = 'touch %s && rm %s' % (filename, filename)
3309            rv = self.run(command=command,
3310                          timeout=30,
3311                          ignore_status=True)
3312            is_writable = rv.exit_status == 0
3313            if not is_writable:
3314                logging.info('Cannot create a file in "%s"!'
3315                             ' Probably the FS is read-only', testdir)
3316                logging.info("FileSystem is not writable!")
3317                return False
3318            return True
3319
3320        if not testdirs or len(testdirs) == 0:
3321            # N.B. Order matters here:  Encrypted stateful is loop-mounted
3322            # from a file in unencrypted stateful, so we don't test for
3323            # errors in encrypted stateful if unencrypted fails.
3324            testdirs = ['/mnt/stateful_partition', '/var/tmp']
3325
3326        for dir in testdirs:
3327            # loop will be stopped if any directory fill fail the check
3328            try:
3329                if not _check_dir(dir):
3330                    return False
3331            except Exception as e:
3332                # here expected only timeout error, all other will
3333                # be catch by 'ignore_status=True'
3334                logging.debug('Fail to check %s to write in it', dir)
3335                return False
3336        return True
3337
3338    def blocking_sync(self, freeze_for_reset=False):
3339        """Sync root device and internal device, via script.
3340
3341        The actual calls end up logged by the run() call, since they're printed
3342        to stdout/stderr in the script.
3343
3344        @param freeze_for_reset: if True, prepare for reset by blocking writes
3345                                 (only if enable_fs_sync_fsfreeze=True)
3346        """
3347
3348        if freeze_for_reset and self.USE_FSFREEZE:
3349            logging.info('Blocking sync and freeze')
3350        elif freeze_for_reset:
3351            logging.info('Blocking sync for reset')
3352        else:
3353            logging.info('Blocking sync')
3354
3355        # client/bin is installed on the DUT as /usr/local/autotest/bin
3356        sync_cmd = '/usr/local/autotest/bin/fs_sync.py'
3357        if freeze_for_reset and self.USE_FSFREEZE:
3358            sync_cmd += ' --freeze'
3359        return self.run(sync_cmd)
3360
3361    def set_health_profile_dut_state(self, state):
3362        if not self.health_profile:
3363            logging.debug('Device health profile is not initialized, skip'
3364                          ' set dut state.')
3365            return
3366        reset_counters = state in profile_constants.STATES_NEED_RESET_COUNTER
3367        self.health_profile.update_dut_state(state, reset_counters)
3368
3369    def require_snk_mode_in_recovery(self):
3370        """Check whether we need to switch servo_v4 role to snk when
3371        booting into recovery mode. (See crbug.com/1129165)
3372        """
3373        has_battery = True
3374        # Determine if the host has battery based on host_info first.
3375        power_info = self.host_info_store.get().get_label_value('power')
3376        if power_info:
3377            has_battery = power_info == 'battery'
3378        elif self.is_up_fast():
3379            # when running local tests host_info is not available, so we
3380            # need to determine whether the host has battery by checking
3381            # from host side.
3382            logging.debug('Label `power` is not found in host_info, checking'
3383                          ' if the host has battery from host side.')
3384            has_battery = self.has_battery()
3385
3386        if not has_battery:
3387            logging.info(
3388                    '%s does not has battery, snk mode is not needed'
3389                    ' for recovery.', self.hostname)
3390            return False
3391
3392        if not self.servo.supports_built_in_pd_control():
3393            logging.info('Power delivery is not supported on this servo, snk'
3394                         ' mode is not needed for recovery.')
3395            return False
3396        try:
3397            battery_percent = self.servo.get('battery_charge_percent')
3398            if battery_percent < cros_constants.MIN_BATTERY_LEVEL:
3399                logging.info(
3400                        'Current battery level %s%% below %s%% threshold, we'
3401                        ' will attempt to boot host in recovery mode without'
3402                        ' changing servo to snk mode. Please note the host may'
3403                        ' not able to see usb drive in recovery mode later due'
3404                        ' to servo not in snk mode.', battery_percent,
3405                        cros_constants.MIN_BATTERY_LEVEL)
3406                return False
3407        except Exception as e:
3408            logging.info(
3409                    'Unexpected error occurred when getting'
3410                    ' battery_charge_percent from servo; %s', str(e))
3411            return False
3412        return True
3413
3414    def _set_servo_topology(self):
3415        """Set servo-topology info to the host-info."""
3416        logging.debug('Try to save servo topology to host-info.')
3417        if not self._servo_host:
3418            logging.debug('Servo host is not initialized.')
3419            return
3420        if not self.is_servo_in_working_state():
3421            logging.debug('Is servo is not in working state then'
3422                          ' update topology is not allowed.')
3423            return
3424        if not self._servo_host.is_servo_topology_supported():
3425            logging.debug('Servo-topology is not supported.')
3426            return
3427        servo_topology = self._servo_host.get_topology()
3428        if not servo_topology or servo_topology.is_empty():
3429            logging.debug('Servo topology is empty')
3430            return
3431        servo_topology.save(self.host_info_store)
3432