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