xref: /aosp_15_r20/external/autotest/server/hosts/abstract_ssh.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2008 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 Li
10*9c5db199SXin Liimport os, time, socket, shutil, glob, logging, tempfile, re
11*9c5db199SXin Liimport shlex
12*9c5db199SXin Liimport subprocess
13*9c5db199SXin Li
14*9c5db199SXin Lifrom autotest_lib.client.bin.result_tools import runner as result_tools_runner
15*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
16*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils
17*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros.network import ping_runner
18*9c5db199SXin Lifrom autotest_lib.client.common_lib.global_config import global_config
19*9c5db199SXin Lifrom autotest_lib.server import autoserv_parser
20*9c5db199SXin Lifrom autotest_lib.server import utils, autotest
21*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info
22*9c5db199SXin Lifrom autotest_lib.server.hosts import remote
23*9c5db199SXin Lifrom autotest_lib.server.hosts import rpc_server_tracker
24*9c5db199SXin Lifrom autotest_lib.server.hosts import ssh_multiplex
25*9c5db199SXin Lifrom autotest_lib.server.hosts.tls_client import exec_dut_command
26*9c5db199SXin Li
27*9c5db199SXin Liimport six
28*9c5db199SXin Lifrom six.moves import filter
29*9c5db199SXin Li
30*9c5db199SXin Litry:
31*9c5db199SXin Li    from autotest_lib.utils.frozen_chromite.lib import metrics
32*9c5db199SXin Liexcept ImportError:
33*9c5db199SXin Li    metrics = utils.metrics_mock
34*9c5db199SXin Li
35*9c5db199SXin Li# pylint: disable=C0111
36*9c5db199SXin Li
37*9c5db199SXin Liget_value = global_config.get_config_value
38*9c5db199SXin Lienable_main_ssh = get_value('AUTOSERV',
39*9c5db199SXin Li                            'enable_main_ssh',
40*9c5db199SXin Li                            type=bool,
41*9c5db199SXin Li                            default=False)
42*9c5db199SXin Li
43*9c5db199SXin LiENABLE_EXEC_DUT_COMMAND = get_value('AUTOSERV',
44*9c5db199SXin Li                                    'enable_tls',
45*9c5db199SXin Li                                    type=bool,
46*9c5db199SXin Li                                    default=False)
47*9c5db199SXin Li
48*9c5db199SXin Li# Number of seconds to use the cached up status.
49*9c5db199SXin Li_DEFAULT_UP_STATUS_EXPIRATION_SECONDS = 300
50*9c5db199SXin Li_DEFAULT_SSH_PORT = None
51*9c5db199SXin Li
52*9c5db199SXin Li# Number of seconds to wait for the host to shut down in wait_down().
53*9c5db199SXin Li_DEFAULT_WAIT_DOWN_TIME_SECONDS = 120
54*9c5db199SXin Li
55*9c5db199SXin Li# Number of seconds to wait for the host to boot up in wait_up().
56*9c5db199SXin Li_DEFAULT_WAIT_UP_TIME_SECONDS = 120
57*9c5db199SXin Li
58*9c5db199SXin Li# Timeout in seconds for a single call of get_boot_id() in wait_down()
59*9c5db199SXin Li# and a single ssh ping in wait_up().
60*9c5db199SXin Li_DEFAULT_MAX_PING_TIMEOUT = 10
61*9c5db199SXin Li
62*9c5db199SXin Li# The client symlink directory.
63*9c5db199SXin LiAUTOTEST_CLIENT_SYMLINK_END = 'client/autotest_lib'
64*9c5db199SXin Li
65*9c5db199SXin Li
66*9c5db199SXin Liclass AbstractSSHHost(remote.RemoteHost):
67*9c5db199SXin Li    """
68*9c5db199SXin Li    This class represents a generic implementation of most of the
69*9c5db199SXin Li    framework necessary for controlling a host via ssh. It implements
70*9c5db199SXin Li    almost all of the abstract Host methods, except for the core
71*9c5db199SXin Li    Host.run method.
72*9c5db199SXin Li    """
73*9c5db199SXin Li    VERSION_PREFIX = ''
74*9c5db199SXin Li    # Timeout for main ssh connection setup, in seconds.
75*9c5db199SXin Li    DEFAULT_START_MAIN_SSH_TIMEOUT_S = 5
76*9c5db199SXin Li
77*9c5db199SXin Li    def _initialize(self,
78*9c5db199SXin Li                    hostname,
79*9c5db199SXin Li                    user="root",
80*9c5db199SXin Li                    port=_DEFAULT_SSH_PORT,
81*9c5db199SXin Li                    password="",
82*9c5db199SXin Li                    is_client_install_supported=True,
83*9c5db199SXin Li                    afe_host=None,
84*9c5db199SXin Li                    host_info_store=None,
85*9c5db199SXin Li                    connection_pool=None,
86*9c5db199SXin Li                    *args,
87*9c5db199SXin Li                    **dargs):
88*9c5db199SXin Li        super(AbstractSSHHost, self)._initialize(hostname=hostname,
89*9c5db199SXin Li                                                 *args, **dargs)
90*9c5db199SXin Li        """
91*9c5db199SXin Li        @param hostname: The hostname of the host.
92*9c5db199SXin Li        @param user: The username to use when ssh'ing into the host.
93*9c5db199SXin Li        @param password: The password to use when ssh'ing into the host.
94*9c5db199SXin Li        @param port: The port to use for ssh.
95*9c5db199SXin Li        @param is_client_install_supported: Boolean to indicate if we can
96*9c5db199SXin Li                install autotest on the host.
97*9c5db199SXin Li        @param afe_host: The host object attained from the AFE (get_hosts).
98*9c5db199SXin Li        @param host_info_store: Optional host_info.CachingHostInfoStore object
99*9c5db199SXin Li                to obtain / update host information.
100*9c5db199SXin Li        @param connection_pool: ssh_multiplex.ConnectionPool instance to share
101*9c5db199SXin Li                the main ssh connection across control scripts.
102*9c5db199SXin Li        """
103*9c5db199SXin Li        self._track_class_usage()
104*9c5db199SXin Li        # IP address is retrieved only on demand. Otherwise the host
105*9c5db199SXin Li        # initialization will fail for host is not online.
106*9c5db199SXin Li        self._ip = None
107*9c5db199SXin Li        self.user = user
108*9c5db199SXin Li        self.port = port
109*9c5db199SXin Li        self.password = password
110*9c5db199SXin Li        self._is_client_install_supported = is_client_install_supported
111*9c5db199SXin Li        self._use_rsync = None
112*9c5db199SXin Li        self.known_hosts_file = tempfile.mkstemp()[1]
113*9c5db199SXin Li        self._rpc_server_tracker = rpc_server_tracker.RpcServerTracker(self);
114*9c5db199SXin Li        self._tls_exec_dut_command_client = None
115*9c5db199SXin Li        self._tls_unstable = False
116*9c5db199SXin Li
117*9c5db199SXin Li        # Read the value of the use_icmp flag, setting to true if missing.
118*9c5db199SXin Li        args_string = autoserv_parser.autoserv_parser.options.args
119*9c5db199SXin Li        args_dict = utils.args_to_dict(
120*9c5db199SXin Li                args_string.split() if args_string is not None else '')
121*9c5db199SXin Li        value = args_dict.get('use_icmp', 'true').lower()
122*9c5db199SXin Li        if value == 'true':
123*9c5db199SXin Li            self._use_icmp = True
124*9c5db199SXin Li        elif value == 'false':
125*9c5db199SXin Li            self._use_icmp = False
126*9c5db199SXin Li        else:
127*9c5db199SXin Li            raise ValueError(
128*9c5db199SXin Li                    'use_icmp must be true or false: {}'.format(value))
129*9c5db199SXin Li        """
130*9c5db199SXin Li        Main SSH connection background job, socket temp directory and socket
131*9c5db199SXin Li        control path option. If main-SSH is enabled, these fields will be
132*9c5db199SXin Li        initialized by start_main_ssh when a new SSH connection is initiated.
133*9c5db199SXin Li        """
134*9c5db199SXin Li        self._connection_pool = connection_pool
135*9c5db199SXin Li        if connection_pool:
136*9c5db199SXin Li            self._main_ssh = connection_pool.get(hostname, user, port)
137*9c5db199SXin Li        else:
138*9c5db199SXin Li            self._main_ssh = ssh_multiplex.MainSsh(hostname, user, port)
139*9c5db199SXin Li
140*9c5db199SXin Li        self._afe_host = afe_host or utils.EmptyAFEHost()
141*9c5db199SXin Li        self.host_info_store = (host_info_store or
142*9c5db199SXin Li                                host_info.InMemoryHostInfoStore())
143*9c5db199SXin Li
144*9c5db199SXin Li        # The cached status of whether the DUT responded to ping.
145*9c5db199SXin Li        self._cached_up_status = None
146*9c5db199SXin Li        # The timestamp when the value of _cached_up_status is set.
147*9c5db199SXin Li        self._cached_up_status_updated = None
148*9c5db199SXin Li
149*9c5db199SXin Li
150*9c5db199SXin Li    @property
151*9c5db199SXin Li    def ip(self):
152*9c5db199SXin Li        """@return IP address of the host.
153*9c5db199SXin Li        """
154*9c5db199SXin Li        if not self._ip:
155*9c5db199SXin Li            self._ip = socket.getaddrinfo(self.hostname, None)[0][4][0]
156*9c5db199SXin Li        return self._ip
157*9c5db199SXin Li
158*9c5db199SXin Li
159*9c5db199SXin Li    @property
160*9c5db199SXin Li    def is_client_install_supported(self):
161*9c5db199SXin Li        """"
162*9c5db199SXin Li        Returns True if the host supports autotest client installs, False
163*9c5db199SXin Li        otherwise.
164*9c5db199SXin Li        """
165*9c5db199SXin Li        return self._is_client_install_supported
166*9c5db199SXin Li
167*9c5db199SXin Li    def is_satlab(self):
168*9c5db199SXin Li        """Determine if the host is part of satlab
169*9c5db199SXin Li
170*9c5db199SXin Li        TODO(otabek@): Remove or update to better logic to determime Satlab.
171*9c5db199SXin Li
172*9c5db199SXin Li        @returns True if ths host is running under satlab otherwise False.
173*9c5db199SXin Li        """
174*9c5db199SXin Li        if not hasattr(self, '_is_satlab'):
175*9c5db199SXin Li            self._is_satlab = self.hostname.startswith('satlab-')
176*9c5db199SXin Li        return self._is_satlab
177*9c5db199SXin Li
178*9c5db199SXin Li    @property
179*9c5db199SXin Li    def rpc_server_tracker(self):
180*9c5db199SXin Li        """"
181*9c5db199SXin Li        @return The RPC server tracker associated with this host.
182*9c5db199SXin Li        """
183*9c5db199SXin Li        return self._rpc_server_tracker
184*9c5db199SXin Li
185*9c5db199SXin Li
186*9c5db199SXin Li    @property
187*9c5db199SXin Li    def is_default_port(self):
188*9c5db199SXin Li        """Returns True if its port is default SSH port."""
189*9c5db199SXin Li        return self.port == _DEFAULT_SSH_PORT or self.port is None
190*9c5db199SXin Li
191*9c5db199SXin Li    @property
192*9c5db199SXin Li    def host_port(self):
193*9c5db199SXin Li        """Returns hostname if port is default. Otherwise, hostname:port.
194*9c5db199SXin Li        """
195*9c5db199SXin Li        if self.is_default_port:
196*9c5db199SXin Li            return self.hostname
197*9c5db199SXin Li        else:
198*9c5db199SXin Li            return '%s:%d' % (self.hostname, self.port)
199*9c5db199SXin Li
200*9c5db199SXin Li    @property
201*9c5db199SXin Li    def use_icmp(self):
202*9c5db199SXin Li        """Returns True if icmp pings are allowed."""
203*9c5db199SXin Li        return self._use_icmp
204*9c5db199SXin Li
205*9c5db199SXin Li
206*9c5db199SXin Li    # Though it doesn't use self here, it is not declared as staticmethod
207*9c5db199SXin Li    # because its subclass may use self to access member variables.
208*9c5db199SXin Li    def make_ssh_command(self, user="root", port=_DEFAULT_SSH_PORT, opts='',
209*9c5db199SXin Li                         hosts_file='/dev/null', connect_timeout=30,
210*9c5db199SXin Li                         alive_interval=300, alive_count_max=3,
211*9c5db199SXin Li                         connection_attempts=1):
212*9c5db199SXin Li        ssh_options = " ".join([
213*9c5db199SXin Li            opts,
214*9c5db199SXin Li            self.make_ssh_options(
215*9c5db199SXin Li                hosts_file=hosts_file, connect_timeout=connect_timeout,
216*9c5db199SXin Li                alive_interval=alive_interval, alive_count_max=alive_count_max,
217*9c5db199SXin Li                connection_attempts=connection_attempts)])
218*9c5db199SXin Li        return ("/usr/bin/ssh -a -x %s -l %s %s" %
219*9c5db199SXin Li                (ssh_options, user, "-p %d " % port if port else ""))
220*9c5db199SXin Li
221*9c5db199SXin Li
222*9c5db199SXin Li    @staticmethod
223*9c5db199SXin Li    def make_ssh_options(hosts_file='/dev/null', connect_timeout=30,
224*9c5db199SXin Li                         alive_interval=300, alive_count_max=3,
225*9c5db199SXin Li                         connection_attempts=1):
226*9c5db199SXin Li        """Composes SSH -o options."""
227*9c5db199SXin Li        assert isinstance(connect_timeout, six.integer_types)
228*9c5db199SXin Li        assert connect_timeout > 0 # can't disable the timeout
229*9c5db199SXin Li
230*9c5db199SXin Li        options = [("StrictHostKeyChecking", "no"),
231*9c5db199SXin Li                   ("UserKnownHostsFile", hosts_file),
232*9c5db199SXin Li                   ("BatchMode", "yes"),
233*9c5db199SXin Li                   ("ConnectTimeout", str(connect_timeout)),
234*9c5db199SXin Li                   ("ServerAliveInterval", str(alive_interval)),
235*9c5db199SXin Li                   ("ServerAliveCountMax", str(alive_count_max)),
236*9c5db199SXin Li                   ("ConnectionAttempts", str(connection_attempts))]
237*9c5db199SXin Li        return " ".join("-o %s=%s" % kv for kv in options)
238*9c5db199SXin Li
239*9c5db199SXin Li
240*9c5db199SXin Li    def use_rsync(self):
241*9c5db199SXin Li        if self._use_rsync is not None:
242*9c5db199SXin Li            return self._use_rsync
243*9c5db199SXin Li
244*9c5db199SXin Li        # Check if rsync is available on the remote host. If it's not,
245*9c5db199SXin Li        # don't try to use it for any future file transfers.
246*9c5db199SXin Li        self._use_rsync = self.check_rsync()
247*9c5db199SXin Li        if not self._use_rsync:
248*9c5db199SXin Li            logging.warning("rsync not available on remote host %s -- disabled",
249*9c5db199SXin Li                            self.host_port)
250*9c5db199SXin Li        return self._use_rsync
251*9c5db199SXin Li
252*9c5db199SXin Li
253*9c5db199SXin Li    def check_rsync(self):
254*9c5db199SXin Li        """
255*9c5db199SXin Li        Check if rsync is available on the remote host.
256*9c5db199SXin Li        """
257*9c5db199SXin Li        try:
258*9c5db199SXin Li            self.run("rsync --version", stdout_tee=None, stderr_tee=None)
259*9c5db199SXin Li        except error.AutoservRunError:
260*9c5db199SXin Li            return False
261*9c5db199SXin Li        return True
262*9c5db199SXin Li
263*9c5db199SXin Li
264*9c5db199SXin Li    def _encode_remote_paths(self, paths, escape=True, use_scp=False):
265*9c5db199SXin Li        """
266*9c5db199SXin Li        Given a list of file paths, encodes it as a single remote path, in
267*9c5db199SXin Li        the style used by rsync and scp.
268*9c5db199SXin Li        escape: add \\ to protect special characters.
269*9c5db199SXin Li        use_scp: encode for scp if true, rsync if false.
270*9c5db199SXin Li        """
271*9c5db199SXin Li        if escape:
272*9c5db199SXin Li            paths = [utils.scp_remote_escape(path) for path in paths]
273*9c5db199SXin Li
274*9c5db199SXin Li        remote = self.hostname
275*9c5db199SXin Li
276*9c5db199SXin Li        # rsync and scp require IPv6 brackets, even when there isn't any
277*9c5db199SXin Li        # trailing port number (ssh doesn't support IPv6 brackets).
278*9c5db199SXin Li        # In the Python >= 3.3 future, 'import ipaddress' will parse addresses.
279*9c5db199SXin Li        if re.search(r':.*:', remote):
280*9c5db199SXin Li            remote = '[%s]' % remote
281*9c5db199SXin Li
282*9c5db199SXin Li        if use_scp:
283*9c5db199SXin Li            return '%s@%s:"%s"' % (self.user, remote, " ".join(paths))
284*9c5db199SXin Li        else:
285*9c5db199SXin Li            return '%s@%s:%s' % (
286*9c5db199SXin Li                    self.user, remote,
287*9c5db199SXin Li                    " :".join('"%s"' % p for p in paths))
288*9c5db199SXin Li
289*9c5db199SXin Li    def _encode_local_paths(self, paths, escape=True):
290*9c5db199SXin Li        """
291*9c5db199SXin Li        Given a list of file paths, encodes it as a single local path.
292*9c5db199SXin Li        escape: add \\ to protect special characters.
293*9c5db199SXin Li        """
294*9c5db199SXin Li        if escape:
295*9c5db199SXin Li            paths = [utils.sh_escape(path) for path in paths]
296*9c5db199SXin Li
297*9c5db199SXin Li        return " ".join('"%s"' % p for p in paths)
298*9c5db199SXin Li
299*9c5db199SXin Li
300*9c5db199SXin Li    def rsync_options(self, delete_dest=False, preserve_symlinks=False,
301*9c5db199SXin Li                      safe_symlinks=False, excludes=None):
302*9c5db199SXin Li        """Obtains rsync options for the remote."""
303*9c5db199SXin Li        ssh_cmd = self.make_ssh_command(user=self.user, port=self.port,
304*9c5db199SXin Li                                        opts=self._main_ssh.ssh_option,
305*9c5db199SXin Li                                        hosts_file=self.known_hosts_file)
306*9c5db199SXin Li        if delete_dest:
307*9c5db199SXin Li            delete_flag = "--delete"
308*9c5db199SXin Li        else:
309*9c5db199SXin Li            delete_flag = ""
310*9c5db199SXin Li        if safe_symlinks:
311*9c5db199SXin Li            symlink_flag = "-l --safe-links"
312*9c5db199SXin Li        elif preserve_symlinks:
313*9c5db199SXin Li            symlink_flag = "-l"
314*9c5db199SXin Li        else:
315*9c5db199SXin Li            symlink_flag = "-L"
316*9c5db199SXin Li        exclude_args = ''
317*9c5db199SXin Li        if excludes:
318*9c5db199SXin Li            exclude_args = ' '.join(
319*9c5db199SXin Li                    ["--exclude '%s'" % exclude for exclude in excludes])
320*9c5db199SXin Li        return "%s %s --timeout=1800 --rsh='%s' -az --no-o --no-g %s" % (
321*9c5db199SXin Li            symlink_flag, delete_flag, ssh_cmd, exclude_args)
322*9c5db199SXin Li
323*9c5db199SXin Li
324*9c5db199SXin Li    def _make_rsync_cmd(self, sources, dest, delete_dest,
325*9c5db199SXin Li                        preserve_symlinks, safe_symlinks, excludes=None):
326*9c5db199SXin Li        """
327*9c5db199SXin Li        Given a string of source paths and a destination path, produces the
328*9c5db199SXin Li        appropriate rsync command for copying them. Remote paths must be
329*9c5db199SXin Li        pre-encoded.
330*9c5db199SXin Li        """
331*9c5db199SXin Li        rsync_options = self.rsync_options(
332*9c5db199SXin Li            delete_dest=delete_dest, preserve_symlinks=preserve_symlinks,
333*9c5db199SXin Li            safe_symlinks=safe_symlinks, excludes=excludes)
334*9c5db199SXin Li        return 'rsync %s %s "%s"' % (rsync_options, sources, dest)
335*9c5db199SXin Li
336*9c5db199SXin Li
337*9c5db199SXin Li    def _make_ssh_cmd(self, cmd):
338*9c5db199SXin Li        """
339*9c5db199SXin Li        Create a base ssh command string for the host which can be used
340*9c5db199SXin Li        to run commands directly on the machine
341*9c5db199SXin Li        """
342*9c5db199SXin Li        base_cmd = self.make_ssh_command(user=self.user, port=self.port,
343*9c5db199SXin Li                                         opts=self._main_ssh.ssh_option,
344*9c5db199SXin Li                                         hosts_file=self.known_hosts_file)
345*9c5db199SXin Li
346*9c5db199SXin Li        return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd))
347*9c5db199SXin Li
348*9c5db199SXin Li    def _make_scp_cmd(self, sources, dest):
349*9c5db199SXin Li        """
350*9c5db199SXin Li        Given a string of source paths and a destination path, produces the
351*9c5db199SXin Li        appropriate scp command for encoding it. Remote paths must be
352*9c5db199SXin Li        pre-encoded.
353*9c5db199SXin Li        """
354*9c5db199SXin Li        command = ("scp -rq %s -o StrictHostKeyChecking=no "
355*9c5db199SXin Li                   "-o UserKnownHostsFile=%s %s%s '%s'")
356*9c5db199SXin Li        return command % (self._main_ssh.ssh_option, self.known_hosts_file,
357*9c5db199SXin Li                          "-P %d " % self.port if self.port else '', sources,
358*9c5db199SXin Li                          dest)
359*9c5db199SXin Li
360*9c5db199SXin Li
361*9c5db199SXin Li    def _make_rsync_compatible_globs(self, path, is_local):
362*9c5db199SXin Li        """
363*9c5db199SXin Li        Given an rsync-style path, returns a list of globbed paths
364*9c5db199SXin Li        that will hopefully provide equivalent behaviour for scp. Does not
365*9c5db199SXin Li        support the full range of rsync pattern matching behaviour, only that
366*9c5db199SXin Li        exposed in the get/send_file interface (trailing slashes).
367*9c5db199SXin Li
368*9c5db199SXin Li        The is_local param is flag indicating if the paths should be
369*9c5db199SXin Li        interpreted as local or remote paths.
370*9c5db199SXin Li        """
371*9c5db199SXin Li
372*9c5db199SXin Li        # non-trailing slash paths should just work
373*9c5db199SXin Li        if len(path) == 0 or path[-1] != "/":
374*9c5db199SXin Li            return [path]
375*9c5db199SXin Li
376*9c5db199SXin Li        # make a function to test if a pattern matches any files
377*9c5db199SXin Li        if is_local:
378*9c5db199SXin Li            def glob_matches_files(path, pattern):
379*9c5db199SXin Li                return len(glob.glob(path + pattern)) > 0
380*9c5db199SXin Li        else:
381*9c5db199SXin Li            def glob_matches_files(path, pattern):
382*9c5db199SXin Li                result = self.run("ls \"%s\"%s" % (utils.sh_escape(path),
383*9c5db199SXin Li                                                   pattern),
384*9c5db199SXin Li                                  stdout_tee=None, ignore_status=True)
385*9c5db199SXin Li                return result.exit_status == 0
386*9c5db199SXin Li
387*9c5db199SXin Li        # take a set of globs that cover all files, and see which are needed
388*9c5db199SXin Li        patterns = ["*", ".[!.]*"]
389*9c5db199SXin Li        patterns = [p for p in patterns if glob_matches_files(path, p)]
390*9c5db199SXin Li
391*9c5db199SXin Li        # convert them into a set of paths suitable for the commandline
392*9c5db199SXin Li        if is_local:
393*9c5db199SXin Li            return ["\"%s\"%s" % (utils.sh_escape(path), pattern)
394*9c5db199SXin Li                    for pattern in patterns]
395*9c5db199SXin Li        else:
396*9c5db199SXin Li            return [utils.scp_remote_escape(path) + pattern
397*9c5db199SXin Li                    for pattern in patterns]
398*9c5db199SXin Li
399*9c5db199SXin Li
400*9c5db199SXin Li    def _make_rsync_compatible_source(self, source, is_local):
401*9c5db199SXin Li        """
402*9c5db199SXin Li        Applies the same logic as _make_rsync_compatible_globs, but
403*9c5db199SXin Li        applies it to an entire list of sources, producing a new list of
404*9c5db199SXin Li        sources, properly quoted.
405*9c5db199SXin Li        """
406*9c5db199SXin Li        return sum((self._make_rsync_compatible_globs(path, is_local)
407*9c5db199SXin Li                    for path in source), [])
408*9c5db199SXin Li
409*9c5db199SXin Li
410*9c5db199SXin Li    def _set_umask_perms(self, dest):
411*9c5db199SXin Li        """
412*9c5db199SXin Li        Given a destination file/dir (recursively) set the permissions on
413*9c5db199SXin Li        all the files and directories to the max allowed by running umask.
414*9c5db199SXin Li        """
415*9c5db199SXin Li
416*9c5db199SXin Li        # now this looks strange but I haven't found a way in Python to _just_
417*9c5db199SXin Li        # get the umask, apparently the only option is to try to set it
418*9c5db199SXin Li        umask = os.umask(0)
419*9c5db199SXin Li        os.umask(umask)
420*9c5db199SXin Li
421*9c5db199SXin Li        max_privs = 0o777 & ~umask
422*9c5db199SXin Li
423*9c5db199SXin Li        def set_file_privs(filename):
424*9c5db199SXin Li            """Sets mode of |filename|.  Assumes |filename| exists."""
425*9c5db199SXin Li            file_stat = os.stat(filename)
426*9c5db199SXin Li
427*9c5db199SXin Li            file_privs = max_privs
428*9c5db199SXin Li            # if the original file permissions do not have at least one
429*9c5db199SXin Li            # executable bit then do not set it anywhere
430*9c5db199SXin Li            if not file_stat.st_mode & 0o111:
431*9c5db199SXin Li                file_privs &= ~0o111
432*9c5db199SXin Li
433*9c5db199SXin Li            os.chmod(filename, file_privs)
434*9c5db199SXin Li
435*9c5db199SXin Li        # try a bottom-up walk so changes on directory permissions won't cut
436*9c5db199SXin Li        # our access to the files/directories inside it
437*9c5db199SXin Li        for root, dirs, files in os.walk(dest, topdown=False):
438*9c5db199SXin Li            # when setting the privileges we emulate the chmod "X" behaviour
439*9c5db199SXin Li            # that sets to execute only if it is a directory or any of the
440*9c5db199SXin Li            # owner/group/other already has execute right
441*9c5db199SXin Li            for dirname in dirs:
442*9c5db199SXin Li                os.chmod(os.path.join(root, dirname), max_privs)
443*9c5db199SXin Li
444*9c5db199SXin Li            # Filter out broken symlinks as we go.
445*9c5db199SXin Li            for filename in filter(os.path.exists, files):
446*9c5db199SXin Li                set_file_privs(os.path.join(root, filename))
447*9c5db199SXin Li
448*9c5db199SXin Li
449*9c5db199SXin Li        # now set privs for the dest itself
450*9c5db199SXin Li        if os.path.isdir(dest):
451*9c5db199SXin Li            os.chmod(dest, max_privs)
452*9c5db199SXin Li        else:
453*9c5db199SXin Li            set_file_privs(dest)
454*9c5db199SXin Li
455*9c5db199SXin Li
456*9c5db199SXin Li    def get_file(self, source, dest, delete_dest=False, preserve_perm=True,
457*9c5db199SXin Li                 preserve_symlinks=False, retry=True, safe_symlinks=False,
458*9c5db199SXin Li                 try_rsync=True):
459*9c5db199SXin Li        """
460*9c5db199SXin Li        Copy files from the remote host to a local path.
461*9c5db199SXin Li
462*9c5db199SXin Li        Directories will be copied recursively.
463*9c5db199SXin Li        If a source component is a directory with a trailing slash,
464*9c5db199SXin Li        the content of the directory will be copied, otherwise, the
465*9c5db199SXin Li        directory itself and its content will be copied. This
466*9c5db199SXin Li        behavior is similar to that of the program 'rsync'.
467*9c5db199SXin Li
468*9c5db199SXin Li        Args:
469*9c5db199SXin Li                source: either
470*9c5db199SXin Li                        1) a single file or directory, as a string
471*9c5db199SXin Li                        2) a list of one or more (possibly mixed)
472*9c5db199SXin Li                                files or directories
473*9c5db199SXin Li                dest: a file or a directory (if source contains a
474*9c5db199SXin Li                        directory or more than one element, you must
475*9c5db199SXin Li                        supply a directory dest)
476*9c5db199SXin Li                delete_dest: if this is true, the command will also clear
477*9c5db199SXin Li                             out any old files at dest that are not in the
478*9c5db199SXin Li                             source
479*9c5db199SXin Li                preserve_perm: tells get_file() to try to preserve the sources
480*9c5db199SXin Li                               permissions on files and dirs
481*9c5db199SXin Li                preserve_symlinks: try to preserve symlinks instead of
482*9c5db199SXin Li                                   transforming them into files/dirs on copy
483*9c5db199SXin Li                safe_symlinks: same as preserve_symlinks, but discard links
484*9c5db199SXin Li                               that may point outside the copied tree
485*9c5db199SXin Li                try_rsync: set to False to skip directly to using scp
486*9c5db199SXin Li        Raises:
487*9c5db199SXin Li                AutoservRunError: the scp command failed
488*9c5db199SXin Li        """
489*9c5db199SXin Li        logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,'
490*9c5db199SXin Li                      'preserve_perm: %s, preserve_symlinks:%s', source, dest,
491*9c5db199SXin Li                      delete_dest, preserve_perm, preserve_symlinks)
492*9c5db199SXin Li
493*9c5db199SXin Li        # Start a main SSH connection if necessary.
494*9c5db199SXin Li        self.start_main_ssh()
495*9c5db199SXin Li
496*9c5db199SXin Li        if isinstance(source, six.string_types):
497*9c5db199SXin Li            source = [source]
498*9c5db199SXin Li        dest = os.path.abspath(dest)
499*9c5db199SXin Li
500*9c5db199SXin Li        # If rsync is disabled or fails, try scp.
501*9c5db199SXin Li        try_scp = True
502*9c5db199SXin Li        if try_rsync and self.use_rsync():
503*9c5db199SXin Li            logging.debug('Using Rsync.')
504*9c5db199SXin Li            try:
505*9c5db199SXin Li                remote_source = self._encode_remote_paths(source)
506*9c5db199SXin Li                local_dest = utils.sh_escape(dest)
507*9c5db199SXin Li                rsync = self._make_rsync_cmd(remote_source, local_dest,
508*9c5db199SXin Li                                             delete_dest, preserve_symlinks,
509*9c5db199SXin Li                                             safe_symlinks)
510*9c5db199SXin Li                utils.run(rsync)
511*9c5db199SXin Li                try_scp = False
512*9c5db199SXin Li            except error.CmdError as e:
513*9c5db199SXin Li                # retry on rsync exit values which may be caused by transient
514*9c5db199SXin Li                # network problems:
515*9c5db199SXin Li                #
516*9c5db199SXin Li                # rc 10: Error in socket I/O
517*9c5db199SXin Li                # rc 12: Error in rsync protocol data stream
518*9c5db199SXin Li                # rc 23: Partial transfer due to error
519*9c5db199SXin Li                # rc 255: Ssh error
520*9c5db199SXin Li                #
521*9c5db199SXin Li                # Note that rc 23 includes dangling symlinks.  In this case
522*9c5db199SXin Li                # retrying is useless, but not very damaging since rsync checks
523*9c5db199SXin Li                # for those before starting the transfer (scp does not).
524*9c5db199SXin Li                status = e.result_obj.exit_status
525*9c5db199SXin Li                if status in [10, 12, 23, 255] and retry:
526*9c5db199SXin Li                    logging.warning('rsync status %d, retrying', status)
527*9c5db199SXin Li                    self.get_file(source, dest, delete_dest, preserve_perm,
528*9c5db199SXin Li                                  preserve_symlinks, retry=False)
529*9c5db199SXin Li                    # The nested get_file() does all that's needed.
530*9c5db199SXin Li                    return
531*9c5db199SXin Li                else:
532*9c5db199SXin Li                    logging.warning("trying scp, rsync failed: %s (%d)",
533*9c5db199SXin Li                                     e, status)
534*9c5db199SXin Li
535*9c5db199SXin Li        if try_scp:
536*9c5db199SXin Li            logging.debug('Trying scp.')
537*9c5db199SXin Li            # scp has no equivalent to --delete, just drop the entire dest dir
538*9c5db199SXin Li            if delete_dest and os.path.isdir(dest):
539*9c5db199SXin Li                shutil.rmtree(dest)
540*9c5db199SXin Li                os.mkdir(dest)
541*9c5db199SXin Li
542*9c5db199SXin Li            remote_source = self._make_rsync_compatible_source(source, False)
543*9c5db199SXin Li            if remote_source:
544*9c5db199SXin Li                # _make_rsync_compatible_source() already did the escaping
545*9c5db199SXin Li                remote_source = self._encode_remote_paths(
546*9c5db199SXin Li                        remote_source, escape=False, use_scp=True)
547*9c5db199SXin Li                local_dest = utils.sh_escape(dest)
548*9c5db199SXin Li                scp = self._make_scp_cmd(remote_source, local_dest)
549*9c5db199SXin Li                try:
550*9c5db199SXin Li                    utils.run(scp)
551*9c5db199SXin Li                except error.CmdError as e:
552*9c5db199SXin Li                    logging.debug('scp failed: %s', e)
553*9c5db199SXin Li                    raise error.AutoservRunError(e.args[0], e.args[1])
554*9c5db199SXin Li
555*9c5db199SXin Li        if not preserve_perm:
556*9c5db199SXin Li            # we have no way to tell scp to not try to preserve the
557*9c5db199SXin Li            # permissions so set them after copy instead.
558*9c5db199SXin Li            # for rsync we could use "--no-p --chmod=ugo=rwX" but those
559*9c5db199SXin Li            # options are only in very recent rsync versions
560*9c5db199SXin Li            self._set_umask_perms(dest)
561*9c5db199SXin Li
562*9c5db199SXin Li
563*9c5db199SXin Li    def send_file(self, source, dest, delete_dest=False,
564*9c5db199SXin Li                  preserve_symlinks=False, excludes=None):
565*9c5db199SXin Li        """
566*9c5db199SXin Li        Copy files from a local path to the remote host.
567*9c5db199SXin Li
568*9c5db199SXin Li        Directories will be copied recursively.
569*9c5db199SXin Li        If a source component is a directory with a trailing slash,
570*9c5db199SXin Li        the content of the directory will be copied, otherwise, the
571*9c5db199SXin Li        directory itself and its content will be copied. This
572*9c5db199SXin Li        behavior is similar to that of the program 'rsync'.
573*9c5db199SXin Li
574*9c5db199SXin Li        Args:
575*9c5db199SXin Li                source: either
576*9c5db199SXin Li                        1) a single file or directory, as a string
577*9c5db199SXin Li                        2) a list of one or more (possibly mixed)
578*9c5db199SXin Li                                files or directories
579*9c5db199SXin Li                dest: a file or a directory (if source contains a
580*9c5db199SXin Li                        directory or more than one element, you must
581*9c5db199SXin Li                        supply a directory dest)
582*9c5db199SXin Li                delete_dest: if this is true, the command will also clear
583*9c5db199SXin Li                             out any old files at dest that are not in the
584*9c5db199SXin Li                             source
585*9c5db199SXin Li                preserve_symlinks: controls if symlinks on the source will be
586*9c5db199SXin Li                    copied as such on the destination or transformed into the
587*9c5db199SXin Li                    referenced file/directory
588*9c5db199SXin Li                excludes: A list of file pattern that matches files not to be
589*9c5db199SXin Li                          sent. `send_file` will fail if exclude is set, since
590*9c5db199SXin Li                          local copy does not support --exclude, e.g., when
591*9c5db199SXin Li                          using scp to copy file.
592*9c5db199SXin Li
593*9c5db199SXin Li        Raises:
594*9c5db199SXin Li                AutoservRunError: the scp command failed
595*9c5db199SXin Li        """
596*9c5db199SXin Li        logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,'
597*9c5db199SXin Li                      'preserve_symlinks:%s', source, dest,
598*9c5db199SXin Li                      delete_dest, preserve_symlinks)
599*9c5db199SXin Li        # Start a main SSH connection if necessary.
600*9c5db199SXin Li        self.start_main_ssh()
601*9c5db199SXin Li
602*9c5db199SXin Li        if isinstance(source, six.string_types):
603*9c5db199SXin Li            source = [source]
604*9c5db199SXin Li
605*9c5db199SXin Li        client_symlink = _client_symlink(source)
606*9c5db199SXin Li        # The client symlink *must* be preserved, and should not be sent with
607*9c5db199SXin Li        # the main send_file in case scp is used, which does not support symlink
608*9c5db199SXin Li        if client_symlink:
609*9c5db199SXin Li            source.remove(client_symlink)
610*9c5db199SXin Li
611*9c5db199SXin Li        local_sources = self._encode_local_paths(source)
612*9c5db199SXin Li        if not local_sources:
613*9c5db199SXin Li            raise error.TestError('source |%s| yielded an empty string' % (
614*9c5db199SXin Li                source))
615*9c5db199SXin Li        if local_sources.find('\x00') != -1:
616*9c5db199SXin Li            raise error.TestError('one or more sources include NUL char')
617*9c5db199SXin Li
618*9c5db199SXin Li        self._send_file(
619*9c5db199SXin Li                dest=dest,
620*9c5db199SXin Li                source=source,
621*9c5db199SXin Li                local_sources=local_sources,
622*9c5db199SXin Li                delete_dest=delete_dest,
623*9c5db199SXin Li                excludes=excludes,
624*9c5db199SXin Li                preserve_symlinks=preserve_symlinks)
625*9c5db199SXin Li
626*9c5db199SXin Li        # Send the client symlink after the rest of the autotest repo has been
627*9c5db199SXin Li        # sent.
628*9c5db199SXin Li        if client_symlink:
629*9c5db199SXin Li            self._send_client_symlink(dest=dest,
630*9c5db199SXin Li                                      source=[client_symlink],
631*9c5db199SXin Li                                      local_sources=client_symlink,
632*9c5db199SXin Li                                      delete_dest=delete_dest,
633*9c5db199SXin Li                                      excludes=excludes,
634*9c5db199SXin Li                                      preserve_symlinks=True)
635*9c5db199SXin Li
636*9c5db199SXin Li    def _send_client_symlink(self, dest, source, local_sources, delete_dest,
637*9c5db199SXin Li                             excludes, preserve_symlinks):
638*9c5db199SXin Li        if self.use_rsync():
639*9c5db199SXin Li            if self._send_using_rsync(dest=dest,
640*9c5db199SXin Li                                      local_sources=local_sources,
641*9c5db199SXin Li                                      delete_dest=delete_dest,
642*9c5db199SXin Li                                      preserve_symlinks=preserve_symlinks,
643*9c5db199SXin Li                                      excludes=excludes):
644*9c5db199SXin Li                return
645*9c5db199SXin Li        # Manually create the symlink if rsync is not available, or fails.
646*9c5db199SXin Li        try:
647*9c5db199SXin Li            self.run('mkdir {f} && touch {f}/__init__.py && cd {f} && '
648*9c5db199SXin Li                     'ln -s ../ client'.format(
649*9c5db199SXin Li                             f=os.path.join(dest, 'autotest_lib')))
650*9c5db199SXin Li        except Exception as e:
651*9c5db199SXin Li            raise error.AutotestHostRunError(
652*9c5db199SXin Li                    "Could not create client symlink on host: %s" % e)
653*9c5db199SXin Li
654*9c5db199SXin Li    def _send_file(self, dest, source, local_sources, delete_dest, excludes,
655*9c5db199SXin Li                   preserve_symlinks):
656*9c5db199SXin Li        """Send file(s), trying rsync first, then scp."""
657*9c5db199SXin Li        if self.use_rsync():
658*9c5db199SXin Li            rsync_success = self._send_using_rsync(
659*9c5db199SXin Li                    dest=dest,
660*9c5db199SXin Li                    local_sources=local_sources,
661*9c5db199SXin Li                    delete_dest=delete_dest,
662*9c5db199SXin Li                    preserve_symlinks=preserve_symlinks,
663*9c5db199SXin Li                    excludes=excludes)
664*9c5db199SXin Li            if rsync_success:
665*9c5db199SXin Li                return
666*9c5db199SXin Li
667*9c5db199SXin Li        # Send using scp if you cannot via rsync, or rsync fails.
668*9c5db199SXin Li        self._send_using_scp(dest=dest,
669*9c5db199SXin Li                             source=source,
670*9c5db199SXin Li                             delete_dest=delete_dest,
671*9c5db199SXin Li                             excludes=excludes)
672*9c5db199SXin Li
673*9c5db199SXin Li    def _send_using_rsync(self, dest, local_sources, delete_dest,
674*9c5db199SXin Li                          preserve_symlinks, excludes):
675*9c5db199SXin Li        """Send using rsync.
676*9c5db199SXin Li
677*9c5db199SXin Li        Args:
678*9c5db199SXin Li            dest: a file or a directory (if source contains a
679*9c5db199SXin Li                    directory or more than one element, you must
680*9c5db199SXin Li                    supply a directory dest)
681*9c5db199SXin Li            local_sources: a string of files/dirs to send separated with spaces
682*9c5db199SXin Li            delete_dest: if this is true, the command will also clear
683*9c5db199SXin Li                         out any old files at dest that are not in the
684*9c5db199SXin Li                         source
685*9c5db199SXin Li            preserve_symlinks: controls if symlinks on the source will be
686*9c5db199SXin Li                copied as such on the destination or transformed into the
687*9c5db199SXin Li                referenced file/directory
688*9c5db199SXin Li            excludes: A list of file pattern that matches files not to be
689*9c5db199SXin Li                      sent. `send_file` will fail if exclude is set, since
690*9c5db199SXin Li                      local copy does not support --exclude, e.g., when
691*9c5db199SXin Li                      using scp to copy file.
692*9c5db199SXin Li        Returns:
693*9c5db199SXin Li            bool: True if the cmd succeeded, else False
694*9c5db199SXin Li
695*9c5db199SXin Li        """
696*9c5db199SXin Li        logging.debug('Using Rsync.')
697*9c5db199SXin Li        remote_dest = self._encode_remote_paths([dest])
698*9c5db199SXin Li        try:
699*9c5db199SXin Li            rsync = self._make_rsync_cmd(local_sources,
700*9c5db199SXin Li                                         remote_dest,
701*9c5db199SXin Li                                         delete_dest,
702*9c5db199SXin Li                                         preserve_symlinks,
703*9c5db199SXin Li                                         False,
704*9c5db199SXin Li                                         excludes=excludes)
705*9c5db199SXin Li            utils.run(rsync)
706*9c5db199SXin Li            return True
707*9c5db199SXin Li        except error.CmdError as e:
708*9c5db199SXin Li            logging.warning("trying scp, rsync failed: %s", e)
709*9c5db199SXin Li        return False
710*9c5db199SXin Li
711*9c5db199SXin Li    def _send_using_scp(self, dest, source, delete_dest, excludes):
712*9c5db199SXin Li        """Send using scp.
713*9c5db199SXin Li
714*9c5db199SXin Li        Args:
715*9c5db199SXin Li                source: either
716*9c5db199SXin Li                        1) a single file or directory, as a string
717*9c5db199SXin Li                        2) a list of one or more (possibly mixed)
718*9c5db199SXin Li                                files or directories
719*9c5db199SXin Li                dest: a file or a directory (if source contains a
720*9c5db199SXin Li                        directory or more than one element, you must
721*9c5db199SXin Li                        supply a directory dest)
722*9c5db199SXin Li                delete_dest: if this is true, the command will also clear
723*9c5db199SXin Li                             out any old files at dest that are not in the
724*9c5db199SXin Li                             source
725*9c5db199SXin Li                excludes: A list of file pattern that matches files not to be
726*9c5db199SXin Li                          sent. `send_file` will fail if exclude is set, since
727*9c5db199SXin Li                          local copy does not support --exclude, e.g., when
728*9c5db199SXin Li                          using scp to copy file.
729*9c5db199SXin Li
730*9c5db199SXin Li        Raises:
731*9c5db199SXin Li                AutoservRunError: the scp command failed
732*9c5db199SXin Li        """
733*9c5db199SXin Li        logging.debug('Trying scp.')
734*9c5db199SXin Li        if excludes:
735*9c5db199SXin Li            raise error.AutotestHostRunError(
736*9c5db199SXin Li                    '--exclude is not supported in scp, try to use rsync. '
737*9c5db199SXin Li                    'excludes: %s' % ','.join(excludes), None)
738*9c5db199SXin Li
739*9c5db199SXin Li        # scp has no equivalent to --delete, just drop the entire dest dir
740*9c5db199SXin Li        if delete_dest:
741*9c5db199SXin Li            is_dir = self.run("ls -d %s/" % dest,
742*9c5db199SXin Li                              ignore_status=True).exit_status == 0
743*9c5db199SXin Li            if is_dir:
744*9c5db199SXin Li                cmd = "rm -rf %s && mkdir %s"
745*9c5db199SXin Li                cmd %= (dest, dest)
746*9c5db199SXin Li                self.run(cmd)
747*9c5db199SXin Li
748*9c5db199SXin Li        remote_dest = self._encode_remote_paths([dest], use_scp=True)
749*9c5db199SXin Li        local_sources = self._make_rsync_compatible_source(source, True)
750*9c5db199SXin Li        if local_sources:
751*9c5db199SXin Li            sources = self._encode_local_paths(local_sources, escape=False)
752*9c5db199SXin Li            scp = self._make_scp_cmd(sources, remote_dest)
753*9c5db199SXin Li            try:
754*9c5db199SXin Li                utils.run(scp)
755*9c5db199SXin Li            except error.CmdError as e:
756*9c5db199SXin Li                logging.debug('scp failed: %s', e)
757*9c5db199SXin Li                raise error.AutoservRunError(e.args[0], e.args[1])
758*9c5db199SXin Li        else:
759*9c5db199SXin Li            logging.debug('skipping scp for empty source list')
760*9c5db199SXin Li
761*9c5db199SXin Li    def verify_ssh_user_access(self):
762*9c5db199SXin Li        """Verify ssh access to this host.
763*9c5db199SXin Li
764*9c5db199SXin Li        @returns False if ssh_ping fails due to Permissions error, True
765*9c5db199SXin Li                 otherwise.
766*9c5db199SXin Li        """
767*9c5db199SXin Li        try:
768*9c5db199SXin Li            self.ssh_ping()
769*9c5db199SXin Li        except (error.AutoservSshPermissionDeniedError,
770*9c5db199SXin Li                error.AutoservSshPingHostError):
771*9c5db199SXin Li            return False
772*9c5db199SXin Li        return True
773*9c5db199SXin Li
774*9c5db199SXin Li
775*9c5db199SXin Li    def ssh_ping(self, timeout=60, connect_timeout=None, base_cmd='true'):
776*9c5db199SXin Li        """
777*9c5db199SXin Li        Pings remote host via ssh.
778*9c5db199SXin Li
779*9c5db199SXin Li        @param timeout: Command execution timeout in seconds.
780*9c5db199SXin Li                        Defaults to 60 seconds.
781*9c5db199SXin Li        @param connect_timeout: ssh connection timeout in seconds.
782*9c5db199SXin Li        @param base_cmd: The base command to run with the ssh ping.
783*9c5db199SXin Li                         Defaults to true.
784*9c5db199SXin Li        @raise AutoservSSHTimeout: If the ssh ping times out.
785*9c5db199SXin Li        @raise AutoservSshPermissionDeniedError: If ssh ping fails due to
786*9c5db199SXin Li                                                 permissions.
787*9c5db199SXin Li        @raise AutoservSshPingHostError: For other AutoservRunErrors.
788*9c5db199SXin Li        """
789*9c5db199SXin Li        ctimeout = min(timeout, connect_timeout or timeout)
790*9c5db199SXin Li        try:
791*9c5db199SXin Li            self.run(base_cmd, timeout=timeout, connect_timeout=ctimeout,
792*9c5db199SXin Li                     ssh_failure_retry_ok=True)
793*9c5db199SXin Li        except error.AutoservSSHTimeout:
794*9c5db199SXin Li            msg = "Host (ssh) verify timed out (timeout = %d)" % timeout
795*9c5db199SXin Li            raise error.AutoservSSHTimeout(msg)
796*9c5db199SXin Li        except error.AutoservSshPermissionDeniedError:
797*9c5db199SXin Li            #let AutoservSshPermissionDeniedError be visible to the callers
798*9c5db199SXin Li            raise
799*9c5db199SXin Li        except error.AutoservRunError as e:
800*9c5db199SXin Li            # convert the generic AutoservRunError into something more
801*9c5db199SXin Li            # specific for this context
802*9c5db199SXin Li            raise error.AutoservSshPingHostError(e.description + '\n' +
803*9c5db199SXin Li                                                 repr(e.result_obj))
804*9c5db199SXin Li
805*9c5db199SXin Li
806*9c5db199SXin Li    def is_up(self, timeout=60, connect_timeout=None, base_cmd='true'):
807*9c5db199SXin Li        """
808*9c5db199SXin Li        Check if the remote host is up by ssh-ing and running a base command.
809*9c5db199SXin Li
810*9c5db199SXin Li        @param timeout: command execution timeout in seconds.
811*9c5db199SXin Li        @param connect_timeout: ssh connection timeout in seconds.
812*9c5db199SXin Li        @param base_cmd: a base command to run with ssh. The default is 'true'.
813*9c5db199SXin Li        @returns True if the remote host is up before the timeout expires,
814*9c5db199SXin Li                 False otherwise.
815*9c5db199SXin Li        """
816*9c5db199SXin Li        try:
817*9c5db199SXin Li            self.ssh_ping(timeout=timeout,
818*9c5db199SXin Li                          connect_timeout=connect_timeout,
819*9c5db199SXin Li                          base_cmd=base_cmd)
820*9c5db199SXin Li        except error.AutoservError:
821*9c5db199SXin Li            return False
822*9c5db199SXin Li        else:
823*9c5db199SXin Li            return True
824*9c5db199SXin Li
825*9c5db199SXin Li
826*9c5db199SXin Li    def is_up_fast(self, count=1):
827*9c5db199SXin Li        """Return True if the host can be pinged.
828*9c5db199SXin Li
829*9c5db199SXin Li        @param count How many time try to ping before decide that host is not
830*9c5db199SXin Li                    reachable by ping.
831*9c5db199SXin Li        """
832*9c5db199SXin Li        if not self._use_icmp:
833*9c5db199SXin Li            stack = self._get_server_stack_state(lowest_frames=1,
834*9c5db199SXin Li                                                 highest_frames=7)
835*9c5db199SXin Li            logging.warning("is_up_fast called with icmp disabled from %s!",
836*9c5db199SXin Li                            stack)
837*9c5db199SXin Li            return True
838*9c5db199SXin Li        ping_config = ping_runner.PingConfig(self.hostname,
839*9c5db199SXin Li                                             count=1,
840*9c5db199SXin Li                                             ignore_result=True,
841*9c5db199SXin Li                                             ignore_status=True)
842*9c5db199SXin Li
843*9c5db199SXin Li        # Run up to the amount specified, but also exit as soon as the first
844*9c5db199SXin Li        # reply is found.
845*9c5db199SXin Li        loops_remaining = count
846*9c5db199SXin Li        while loops_remaining > 0:
847*9c5db199SXin Li            loops_remaining -= 1
848*9c5db199SXin Li            if ping_runner.PingRunner().ping(ping_config).received > 0:
849*9c5db199SXin Li                return True
850*9c5db199SXin Li        return False
851*9c5db199SXin Li
852*9c5db199SXin Li
853*9c5db199SXin Li    def wait_up(self,
854*9c5db199SXin Li                timeout=_DEFAULT_WAIT_UP_TIME_SECONDS,
855*9c5db199SXin Li                host_is_down=False):
856*9c5db199SXin Li        """
857*9c5db199SXin Li        Wait until the remote host is up or the timeout expires.
858*9c5db199SXin Li
859*9c5db199SXin Li        In fact, it will wait until an ssh connection to the remote
860*9c5db199SXin Li        host can be established, and getty is running.
861*9c5db199SXin Li
862*9c5db199SXin Li        @param timeout time limit in seconds before returning even
863*9c5db199SXin Li            if the host is not up.
864*9c5db199SXin Li        @param host_is_down set to True if the host is known to be down before
865*9c5db199SXin Li            wait_up.
866*9c5db199SXin Li
867*9c5db199SXin Li        @returns True if the host was found to be up before the timeout expires,
868*9c5db199SXin Li                 False otherwise
869*9c5db199SXin Li        """
870*9c5db199SXin Li        if host_is_down:
871*9c5db199SXin Li            # Since we expect the host to be down when this is called, if there is
872*9c5db199SXin Li            # an existing ssh main connection close it.
873*9c5db199SXin Li            self.close_main_ssh()
874*9c5db199SXin Li        current_time = int(time.time())
875*9c5db199SXin Li        end_time = current_time + timeout
876*9c5db199SXin Li
877*9c5db199SXin Li        ssh_success_logged = False
878*9c5db199SXin Li        autoserv_error_logged = False
879*9c5db199SXin Li        while current_time < end_time:
880*9c5db199SXin Li            ping_timeout = min(_DEFAULT_MAX_PING_TIMEOUT,
881*9c5db199SXin Li                               end_time - current_time)
882*9c5db199SXin Li            if self.is_up(timeout=ping_timeout, connect_timeout=ping_timeout):
883*9c5db199SXin Li                if not ssh_success_logged:
884*9c5db199SXin Li                    logging.debug('Successfully pinged host %s',
885*9c5db199SXin Li                                  self.host_port)
886*9c5db199SXin Li                    wait_procs = self.get_wait_up_processes()
887*9c5db199SXin Li                    if wait_procs:
888*9c5db199SXin Li                        logging.debug('Waiting for processes: %s', wait_procs)
889*9c5db199SXin Li                    else:
890*9c5db199SXin Li                        logging.debug('No wait_up processes to wait for')
891*9c5db199SXin Li                    ssh_success_logged = True
892*9c5db199SXin Li                try:
893*9c5db199SXin Li                    if self.are_wait_up_processes_up():
894*9c5db199SXin Li                        logging.debug('Host %s is now up', self.host_port)
895*9c5db199SXin Li                        return True
896*9c5db199SXin Li                except error.AutoservError as e:
897*9c5db199SXin Li                    if not autoserv_error_logged:
898*9c5db199SXin Li                        logging.debug('Ignoring failure to reach %s: %s %s',
899*9c5db199SXin Li                                      self.host_port, e,
900*9c5db199SXin Li                                      '(and further similar failures)')
901*9c5db199SXin Li                        autoserv_error_logged = True
902*9c5db199SXin Li            time.sleep(1)
903*9c5db199SXin Li            current_time = int(time.time())
904*9c5db199SXin Li
905*9c5db199SXin Li        logging.debug('Host %s is still down after waiting %d seconds',
906*9c5db199SXin Li                      self.host_port, int(timeout + time.time() - end_time))
907*9c5db199SXin Li        return False
908*9c5db199SXin Li
909*9c5db199SXin Li
910*9c5db199SXin Li    def wait_down(self, timeout=_DEFAULT_WAIT_DOWN_TIME_SECONDS,
911*9c5db199SXin Li                  warning_timer=None, old_boot_id=None,
912*9c5db199SXin Li                  max_ping_timeout=_DEFAULT_MAX_PING_TIMEOUT):
913*9c5db199SXin Li        """
914*9c5db199SXin Li        Wait until the remote host is down or the timeout expires.
915*9c5db199SXin Li
916*9c5db199SXin Li        If old_boot_id is provided, waits until either the machine is
917*9c5db199SXin Li        unpingable or self.get_boot_id() returns a value different from
918*9c5db199SXin Li        old_boot_id. If the boot_id value has changed then the function
919*9c5db199SXin Li        returns True under the assumption that the machine has shut down
920*9c5db199SXin Li        and has now already come back up.
921*9c5db199SXin Li
922*9c5db199SXin Li        If old_boot_id is None then until the machine becomes unreachable the
923*9c5db199SXin Li        method assumes the machine has not yet shut down.
924*9c5db199SXin Li
925*9c5db199SXin Li        @param timeout Time limit in seconds before returning even if the host
926*9c5db199SXin Li            is still up.
927*9c5db199SXin Li        @param warning_timer Time limit in seconds that will generate a warning
928*9c5db199SXin Li            if the host is not down yet. Can be None for no warning.
929*9c5db199SXin Li        @param old_boot_id A string containing the result of self.get_boot_id()
930*9c5db199SXin Li            prior to the host being told to shut down. Can be None if this is
931*9c5db199SXin Li            not available.
932*9c5db199SXin Li        @param max_ping_timeout Maximum timeout in seconds for each
933*9c5db199SXin Li            self.get_boot_id() call. If this timeout is hit, it is assumed that
934*9c5db199SXin Li            the host went down and became unreachable.
935*9c5db199SXin Li
936*9c5db199SXin Li        @returns True if the host was found to be down (max_ping_timeout timeout
937*9c5db199SXin Li            expired or boot_id changed if provided) and False if timeout
938*9c5db199SXin Li            expired.
939*9c5db199SXin Li        """
940*9c5db199SXin Li        #TODO: there is currently no way to distinguish between knowing
941*9c5db199SXin Li        #TODO: boot_id was unsupported and not knowing the boot_id.
942*9c5db199SXin Li        current_time = int(time.time())
943*9c5db199SXin Li        end_time = current_time + timeout
944*9c5db199SXin Li
945*9c5db199SXin Li        if warning_timer:
946*9c5db199SXin Li            warn_time = current_time + warning_timer
947*9c5db199SXin Li
948*9c5db199SXin Li        if old_boot_id is not None:
949*9c5db199SXin Li            logging.debug('Host %s pre-shutdown boot_id is %s',
950*9c5db199SXin Li                          self.host_port, old_boot_id)
951*9c5db199SXin Li
952*9c5db199SXin Li        # Impose semi real-time deadline constraints, since some clients
953*9c5db199SXin Li        # (eg: watchdog timer tests) expect strict checking of time elapsed.
954*9c5db199SXin Li        # Each iteration of this loop is treated as though it atomically
955*9c5db199SXin Li        # completes within current_time, this is needed because if we used
956*9c5db199SXin Li        # inline time.time() calls instead then the following could happen:
957*9c5db199SXin Li        #
958*9c5db199SXin Li        # while time.time() < end_time:                     [23 < 30]
959*9c5db199SXin Li        #    some code.                                     [takes 10 secs]
960*9c5db199SXin Li        #    try:
961*9c5db199SXin Li        #        new_boot_id = self.get_boot_id(timeout=end_time - time.time())
962*9c5db199SXin Li        #                                                   [30 - 33]
963*9c5db199SXin Li        # The last step will lead to a return True, when in fact the machine
964*9c5db199SXin Li        # went down at 32 seconds (>30). Hence we need to pass get_boot_id
965*9c5db199SXin Li        # the same time that allowed us into that iteration of the loop.
966*9c5db199SXin Li        while current_time < end_time:
967*9c5db199SXin Li            ping_timeout = min(end_time - current_time, max_ping_timeout)
968*9c5db199SXin Li            try:
969*9c5db199SXin Li                new_boot_id = self.get_boot_id(timeout=ping_timeout)
970*9c5db199SXin Li            except error.AutoservError:
971*9c5db199SXin Li                logging.debug('Host %s is now unreachable over ssh, is down',
972*9c5db199SXin Li                              self.host_port)
973*9c5db199SXin Li                return True
974*9c5db199SXin Li            else:
975*9c5db199SXin Li                # if the machine is up but the boot_id value has changed from
976*9c5db199SXin Li                # old boot id, then we can assume the machine has gone down
977*9c5db199SXin Li                # and then already come back up
978*9c5db199SXin Li                if old_boot_id is not None and old_boot_id != new_boot_id:
979*9c5db199SXin Li                    logging.debug('Host %s now has boot_id %s and so must '
980*9c5db199SXin Li                                  'have rebooted', self.host_port, new_boot_id)
981*9c5db199SXin Li                    return True
982*9c5db199SXin Li
983*9c5db199SXin Li            if warning_timer and current_time > warn_time:
984*9c5db199SXin Li                self.record("INFO", None, "shutdown",
985*9c5db199SXin Li                            "Shutdown took longer than %ds" % warning_timer)
986*9c5db199SXin Li                # Print the warning only once.
987*9c5db199SXin Li                warning_timer = None
988*9c5db199SXin Li                # If a machine is stuck switching runlevels
989*9c5db199SXin Li                # This may cause the machine to reboot.
990*9c5db199SXin Li                self.run('kill -HUP 1', ignore_status=True)
991*9c5db199SXin Li
992*9c5db199SXin Li            time.sleep(1)
993*9c5db199SXin Li            current_time = int(time.time())
994*9c5db199SXin Li
995*9c5db199SXin Li        return False
996*9c5db199SXin Li
997*9c5db199SXin Li
998*9c5db199SXin Li    # tunable constants for the verify & repair code
999*9c5db199SXin Li    AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER",
1000*9c5db199SXin Li                                               "gb_diskspace_required",
1001*9c5db199SXin Li                                               type=float,
1002*9c5db199SXin Li                                               default=20.0)
1003*9c5db199SXin Li
1004*9c5db199SXin Li
1005*9c5db199SXin Li    def verify_connectivity(self):
1006*9c5db199SXin Li        super(AbstractSSHHost, self).verify_connectivity()
1007*9c5db199SXin Li
1008*9c5db199SXin Li        logging.info('Pinging host %s', self.host_port)
1009*9c5db199SXin Li        self.ssh_ping()
1010*9c5db199SXin Li        logging.info("Host (ssh) %s is alive", self.host_port)
1011*9c5db199SXin Li
1012*9c5db199SXin Li        if self.is_shutting_down():
1013*9c5db199SXin Li            raise error.AutoservHostIsShuttingDownError("Host is shutting down")
1014*9c5db199SXin Li
1015*9c5db199SXin Li
1016*9c5db199SXin Li    def verify_software(self):
1017*9c5db199SXin Li        super(AbstractSSHHost, self).verify_software()
1018*9c5db199SXin Li        try:
1019*9c5db199SXin Li            self.check_diskspace(autotest.Autotest.get_install_dir(self),
1020*9c5db199SXin Li                                 self.AUTOTEST_GB_DISKSPACE_REQUIRED)
1021*9c5db199SXin Li        except error.AutoservDiskFullHostError:
1022*9c5db199SXin Li            # only want to raise if it's a space issue
1023*9c5db199SXin Li            raise
1024*9c5db199SXin Li        except (error.AutoservHostError, autotest.AutodirNotFoundError):
1025*9c5db199SXin Li            logging.exception('autodir space check exception, this is probably '
1026*9c5db199SXin Li                             'safe to ignore\n')
1027*9c5db199SXin Li
1028*9c5db199SXin Li    def close(self):
1029*9c5db199SXin Li        super(AbstractSSHHost, self).close()
1030*9c5db199SXin Li        self.rpc_server_tracker.disconnect_all()
1031*9c5db199SXin Li        if not self._connection_pool:
1032*9c5db199SXin Li            self._main_ssh.close()
1033*9c5db199SXin Li        if os.path.exists(self.known_hosts_file):
1034*9c5db199SXin Li            os.remove(self.known_hosts_file)
1035*9c5db199SXin Li        self.tls_exec_dut_command = None
1036*9c5db199SXin Li
1037*9c5db199SXin Li    def close_main_ssh(self):
1038*9c5db199SXin Li        """Stop the ssh main connection.
1039*9c5db199SXin Li
1040*9c5db199SXin Li        Intended for situations when the host is known to be down and we don't
1041*9c5db199SXin Li        need a ssh timeout to tell us it is down. For example, if you just
1042*9c5db199SXin Li        instructed the host to shutdown or hibernate.
1043*9c5db199SXin Li        """
1044*9c5db199SXin Li        logging.debug("Stopping main ssh connection")
1045*9c5db199SXin Li        self._main_ssh.close()
1046*9c5db199SXin Li
1047*9c5db199SXin Li    def restart_main_ssh(self):
1048*9c5db199SXin Li        """
1049*9c5db199SXin Li        Stop and restart the ssh main connection.  This is meant as a last
1050*9c5db199SXin Li        resort when ssh commands fail and we don't understand why.
1051*9c5db199SXin Li        """
1052*9c5db199SXin Li        logging.debug("Restarting main ssh connection")
1053*9c5db199SXin Li        self._main_ssh.close()
1054*9c5db199SXin Li        self._main_ssh.maybe_start(timeout=30)
1055*9c5db199SXin Li
1056*9c5db199SXin Li    def start_main_ssh(self, timeout=DEFAULT_START_MAIN_SSH_TIMEOUT_S):
1057*9c5db199SXin Li        """
1058*9c5db199SXin Li        Called whenever a non-main SSH connection needs to be initiated (e.g.,
1059*9c5db199SXin Li        by run, rsync, scp). If main SSH support is enabled and a main SSH
1060*9c5db199SXin Li        connection is not active already, start a new one in the background.
1061*9c5db199SXin Li        Also, cleanup any zombie main SSH connections (e.g., dead due to
1062*9c5db199SXin Li        reboot).
1063*9c5db199SXin Li
1064*9c5db199SXin Li        timeout: timeout in seconds (default 5) to wait for main ssh
1065*9c5db199SXin Li                 connection to be established. If timeout is reached, a
1066*9c5db199SXin Li                 warning message is logged, but no other action is taken.
1067*9c5db199SXin Li        """
1068*9c5db199SXin Li        if not enable_main_ssh:
1069*9c5db199SXin Li            return
1070*9c5db199SXin Li        self._main_ssh.maybe_start(timeout=timeout)
1071*9c5db199SXin Li
1072*9c5db199SXin Li    @property
1073*9c5db199SXin Li    def tls_unstable(self):
1074*9c5db199SXin Li        # A single test will rebuild remote many times. Its safe to assume if
1075*9c5db199SXin Li        # TLS unstable for one try, it will be for others. If we check each,
1076*9c5db199SXin Li        # it adds ~60 seconds per test (if its dead).
1077*9c5db199SXin Li        if os.getenv('TLS_UNSTABLE'):
1078*9c5db199SXin Li            return bool(os.getenv('TLS_UNSTABLE'))
1079*9c5db199SXin Li        if self._tls_unstable is not None:
1080*9c5db199SXin Li            return self._tls_unstable
1081*9c5db199SXin Li
1082*9c5db199SXin Li    @tls_unstable.setter
1083*9c5db199SXin Li    def tls_unstable(self, v):
1084*9c5db199SXin Li        if not isinstance(v, bool):
1085*9c5db199SXin Li            raise error.AutoservError('tls_stable setting must be bool, got %s'
1086*9c5db199SXin Li                                      % (type(v)))
1087*9c5db199SXin Li        os.environ['TLS_UNSTABLE'] = str(v)
1088*9c5db199SXin Li        self._tls_unstable = v
1089*9c5db199SXin Li
1090*9c5db199SXin Li    @property
1091*9c5db199SXin Li    def tls_exec_dut_command_client(self):
1092*9c5db199SXin Li        # If client is already initialized, return that.
1093*9c5db199SXin Li        if not ENABLE_EXEC_DUT_COMMAND:
1094*9c5db199SXin Li            return None
1095*9c5db199SXin Li        if self.tls_unstable:
1096*9c5db199SXin Li            return None
1097*9c5db199SXin Li        if self._tls_exec_dut_command_client is not None:
1098*9c5db199SXin Li            return self._tls_exec_dut_command_client
1099*9c5db199SXin Li        # If the TLS connection is alive, create a new client.
1100*9c5db199SXin Li        if self.tls_connection is None:
1101*9c5db199SXin Li            return None
1102*9c5db199SXin Li        return exec_dut_command.TLSExecDutCommandClient(
1103*9c5db199SXin Li            tlsconnection=self.tls_connection,
1104*9c5db199SXin Li            hostname=self.hostname)
1105*9c5db199SXin Li
1106*9c5db199SXin Li    def clear_known_hosts(self):
1107*9c5db199SXin Li        """Clears out the temporary ssh known_hosts file.
1108*9c5db199SXin Li
1109*9c5db199SXin Li        This is useful if the test SSHes to the machine, then reinstalls it,
1110*9c5db199SXin Li        then SSHes to it again.  It can be called after the reinstall to
1111*9c5db199SXin Li        reduce the spam in the logs.
1112*9c5db199SXin Li        """
1113*9c5db199SXin Li        logging.info("Clearing known hosts for host '%s', file '%s'.",
1114*9c5db199SXin Li                     self.host_port, self.known_hosts_file)
1115*9c5db199SXin Li        # Clear out the file by opening it for writing and then closing.
1116*9c5db199SXin Li        fh = open(self.known_hosts_file, "w")
1117*9c5db199SXin Li        fh.close()
1118*9c5db199SXin Li
1119*9c5db199SXin Li
1120*9c5db199SXin Li    def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True):
1121*9c5db199SXin Li        """Copy log directories from a host to a local directory.
1122*9c5db199SXin Li
1123*9c5db199SXin Li        @param remote_src_dir: A destination directory on the host.
1124*9c5db199SXin Li        @param local_dest_dir: A path to a local destination directory.
1125*9c5db199SXin Li            If it doesn't exist it will be created.
1126*9c5db199SXin Li        @param ignore_errors: If True, ignore exceptions.
1127*9c5db199SXin Li
1128*9c5db199SXin Li        @raises OSError: If there were problems creating the local_dest_dir and
1129*9c5db199SXin Li            ignore_errors is False.
1130*9c5db199SXin Li        @raises AutoservRunError, AutotestRunError: If something goes wrong
1131*9c5db199SXin Li            while copying the directories and ignore_errors is False.
1132*9c5db199SXin Li        """
1133*9c5db199SXin Li        if not self.check_cached_up_status():
1134*9c5db199SXin Li            logging.warning('Host %s did not answer to ping, skip collecting '
1135*9c5db199SXin Li                            'logs.', self.host_port)
1136*9c5db199SXin Li            return
1137*9c5db199SXin Li
1138*9c5db199SXin Li        locally_created_dest = False
1139*9c5db199SXin Li        if (not os.path.exists(local_dest_dir)
1140*9c5db199SXin Li                or not os.path.isdir(local_dest_dir)):
1141*9c5db199SXin Li            try:
1142*9c5db199SXin Li                os.makedirs(local_dest_dir)
1143*9c5db199SXin Li                locally_created_dest = True
1144*9c5db199SXin Li            except OSError as e:
1145*9c5db199SXin Li                logging.warning('Unable to collect logs from host '
1146*9c5db199SXin Li                                '%s: %s', self.host_port, e)
1147*9c5db199SXin Li                if not ignore_errors:
1148*9c5db199SXin Li                    raise
1149*9c5db199SXin Li                return
1150*9c5db199SXin Li
1151*9c5db199SXin Li        # Build test result directory summary
1152*9c5db199SXin Li        try:
1153*9c5db199SXin Li            result_tools_runner.run_on_client(self, remote_src_dir)
1154*9c5db199SXin Li        except (error.AutotestRunError, error.AutoservRunError,
1155*9c5db199SXin Li                error.AutoservSSHTimeout) as e:
1156*9c5db199SXin Li            logging.exception(
1157*9c5db199SXin Li                    'Non-critical failure: Failed to collect and throttle '
1158*9c5db199SXin Li                    'results at %s from host %s', remote_src_dir,
1159*9c5db199SXin Li                    self.host_port)
1160*9c5db199SXin Li
1161*9c5db199SXin Li        try:
1162*9c5db199SXin Li            self.get_file(remote_src_dir, local_dest_dir, safe_symlinks=True)
1163*9c5db199SXin Li        except (error.AutotestRunError, error.AutoservRunError,
1164*9c5db199SXin Li                error.AutoservSSHTimeout) as e:
1165*9c5db199SXin Li            logging.warning('Collection of %s to local dir %s from host %s '
1166*9c5db199SXin Li                            'failed: %s', remote_src_dir, local_dest_dir,
1167*9c5db199SXin Li                            self.host_port, e)
1168*9c5db199SXin Li            if locally_created_dest:
1169*9c5db199SXin Li                shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors)
1170*9c5db199SXin Li            if not ignore_errors:
1171*9c5db199SXin Li                raise
1172*9c5db199SXin Li
1173*9c5db199SXin Li        # Clean up directory summary file on the client side.
1174*9c5db199SXin Li        try:
1175*9c5db199SXin Li            result_tools_runner.run_on_client(self, remote_src_dir,
1176*9c5db199SXin Li                                              cleanup_only=True)
1177*9c5db199SXin Li        except (error.AutotestRunError, error.AutoservRunError,
1178*9c5db199SXin Li                error.AutoservSSHTimeout) as e:
1179*9c5db199SXin Li            logging.exception(
1180*9c5db199SXin Li                    'Non-critical failure: Failed to cleanup result summary '
1181*9c5db199SXin Li                    'files at %s in host %s', remote_src_dir, self.hostname)
1182*9c5db199SXin Li
1183*9c5db199SXin Li
1184*9c5db199SXin Li    def create_ssh_tunnel(self, port, local_port):
1185*9c5db199SXin Li        """Create an ssh tunnel from local_port to port.
1186*9c5db199SXin Li
1187*9c5db199SXin Li        This is used to forward a port securely through a tunnel process from
1188*9c5db199SXin Li        the server to the DUT for RPC server connection.
1189*9c5db199SXin Li
1190*9c5db199SXin Li        @param port: remote port on the host.
1191*9c5db199SXin Li        @param local_port: local forwarding port.
1192*9c5db199SXin Li
1193*9c5db199SXin Li        @return: the tunnel process.
1194*9c5db199SXin Li        """
1195*9c5db199SXin Li        tunnel_options = '-n -N -q -L %d:localhost:%d' % (local_port, port)
1196*9c5db199SXin Li        ssh_cmd = self.make_ssh_command(opts=tunnel_options, port=self.port)
1197*9c5db199SXin Li        tunnel_cmd = '%s %s' % (ssh_cmd, self.hostname)
1198*9c5db199SXin Li        logging.debug('Full tunnel command: %s', tunnel_cmd)
1199*9c5db199SXin Li        # Exec the ssh process directly here rather than using a shell.
1200*9c5db199SXin Li        # Using a shell leaves a dangling ssh process, because we deliver
1201*9c5db199SXin Li        # signals to the shell wrapping ssh, not the ssh process itself.
1202*9c5db199SXin Li        args = shlex.split(tunnel_cmd)
1203*9c5db199SXin Li        with open('/dev/null', 'w') as devnull:
1204*9c5db199SXin Li            tunnel_proc = subprocess.Popen(args, stdout=devnull, stderr=devnull,
1205*9c5db199SXin Li                                           close_fds=True)
1206*9c5db199SXin Li        logging.debug('Started ssh tunnel, local = %d'
1207*9c5db199SXin Li                      ' remote = %d, pid = %d',
1208*9c5db199SXin Li                      local_port, port, tunnel_proc.pid)
1209*9c5db199SXin Li        return tunnel_proc
1210*9c5db199SXin Li
1211*9c5db199SXin Li
1212*9c5db199SXin Li    def disconnect_ssh_tunnel(self, tunnel_proc):
1213*9c5db199SXin Li        """
1214*9c5db199SXin Li        Disconnects a previously forwarded port from the server to the DUT for
1215*9c5db199SXin Li        RPC server connection.
1216*9c5db199SXin Li
1217*9c5db199SXin Li        @param tunnel_proc: a tunnel process returned from |create_ssh_tunnel|.
1218*9c5db199SXin Li        """
1219*9c5db199SXin Li        if tunnel_proc.poll() is None:
1220*9c5db199SXin Li            tunnel_proc.terminate()
1221*9c5db199SXin Li            logging.debug('Terminated tunnel, pid %d', tunnel_proc.pid)
1222*9c5db199SXin Li        else:
1223*9c5db199SXin Li            logging.debug('Tunnel pid %d terminated early, status %d',
1224*9c5db199SXin Li                          tunnel_proc.pid, tunnel_proc.returncode)
1225*9c5db199SXin Li
1226*9c5db199SXin Li
1227*9c5db199SXin Li    def get_os_type(self):
1228*9c5db199SXin Li        """Returns the host OS descriptor (to be implemented in subclasses).
1229*9c5db199SXin Li
1230*9c5db199SXin Li        @return A string describing the OS type.
1231*9c5db199SXin Li        """
1232*9c5db199SXin Li        raise NotImplementedError
1233*9c5db199SXin Li
1234*9c5db199SXin Li
1235*9c5db199SXin Li    def check_cached_up_status(
1236*9c5db199SXin Li            self, expiration_seconds=_DEFAULT_UP_STATUS_EXPIRATION_SECONDS):
1237*9c5db199SXin Li        """Check if the DUT responded to ping in the past `expiration_seconds`.
1238*9c5db199SXin Li
1239*9c5db199SXin Li        @param expiration_seconds: The number of seconds to keep the cached
1240*9c5db199SXin Li                status of whether the DUT responded to ping.
1241*9c5db199SXin Li        @return: True if the DUT has responded to ping during the past
1242*9c5db199SXin Li                 `expiration_seconds`.
1243*9c5db199SXin Li        """
1244*9c5db199SXin Li        # Refresh the up status if any of following conditions is true:
1245*9c5db199SXin Li        # * cached status is never set
1246*9c5db199SXin Li        # * cached status is False, so the method can check if the host is up
1247*9c5db199SXin Li        #   again.
1248*9c5db199SXin Li        # * If the cached status is older than `expiration_seconds`
1249*9c5db199SXin Li        # If we have icmp disabled, treat that as a cached ping.
1250*9c5db199SXin Li        if not self._use_icmp:
1251*9c5db199SXin Li            return True
1252*9c5db199SXin Li        expire_time = time.time() - expiration_seconds
1253*9c5db199SXin Li        if (self._cached_up_status_updated is None or
1254*9c5db199SXin Li                not self._cached_up_status or
1255*9c5db199SXin Li                self._cached_up_status_updated < expire_time):
1256*9c5db199SXin Li            self._cached_up_status = self.is_up_fast()
1257*9c5db199SXin Li            self._cached_up_status_updated = time.time()
1258*9c5db199SXin Li        return self._cached_up_status
1259*9c5db199SXin Li
1260*9c5db199SXin Li
1261*9c5db199SXin Li    def _track_class_usage(self):
1262*9c5db199SXin Li        """Tracking which class was used.
1263*9c5db199SXin Li
1264*9c5db199SXin Li        The idea to identify unused classes to be able clean them up.
1265*9c5db199SXin Li        We skip names with dynamic created classes where the name is
1266*9c5db199SXin Li        hostname of the device.
1267*9c5db199SXin Li        """
1268*9c5db199SXin Li        class_name = None
1269*9c5db199SXin Li        if 'chrome' not in self.__class__.__name__:
1270*9c5db199SXin Li            class_name = self.__class__.__name__
1271*9c5db199SXin Li        else:
1272*9c5db199SXin Li            for base in self.__class__.__bases__:
1273*9c5db199SXin Li                if 'chrome' not in base.__name__:
1274*9c5db199SXin Li                    class_name = base.__name__
1275*9c5db199SXin Li                    break
1276*9c5db199SXin Li        if class_name:
1277*9c5db199SXin Li            data = {'host_class': class_name}
1278*9c5db199SXin Li            metrics.Counter(
1279*9c5db199SXin Li                'chromeos/autotest/used_hosts').increment(fields=data)
1280*9c5db199SXin Li
1281*9c5db199SXin Li    def is_file_exists(self, file_path):
1282*9c5db199SXin Li        """Check whether a given file is exist on the host.
1283*9c5db199SXin Li        """
1284*9c5db199SXin Li        result = self.run('test -f ' + file_path,
1285*9c5db199SXin Li                          timeout=30,
1286*9c5db199SXin Li                          ignore_status=True)
1287*9c5db199SXin Li        return result.exit_status == 0
1288*9c5db199SXin Li
1289*9c5db199SXin Li
1290*9c5db199SXin Lidef _client_symlink(sources):
1291*9c5db199SXin Li    """Return the client symlink if in sources."""
1292*9c5db199SXin Li    for source in sources:
1293*9c5db199SXin Li        if source.endswith(AUTOTEST_CLIENT_SYMLINK_END):
1294*9c5db199SXin Li            return source
1295*9c5db199SXin Li    return None
1296