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