xref: /aosp_15_r20/external/autotest/server/hosts/ssh_host.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li#
3*9c5db199SXin Li# Copyright 2007 Google Inc. Released under the GPL v2
4*9c5db199SXin Li
5*9c5db199SXin Li"""
6*9c5db199SXin LiThis module defines the SSHHost class.
7*9c5db199SXin Li
8*9c5db199SXin LiImplementation details:
9*9c5db199SXin LiYou should import the "hosts" package instead of importing each type of host.
10*9c5db199SXin Li
11*9c5db199SXin Li        SSHHost: a remote machine with a ssh access
12*9c5db199SXin Li"""
13*9c5db199SXin Li
14*9c5db199SXin Lifrom __future__ import absolute_import
15*9c5db199SXin Lifrom __future__ import division
16*9c5db199SXin Lifrom __future__ import print_function
17*9c5db199SXin Li
18*9c5db199SXin Liimport inspect
19*9c5db199SXin Liimport logging
20*9c5db199SXin Liimport re
21*9c5db199SXin Liimport time
22*9c5db199SXin Li
23*9c5db199SXin Liimport common
24*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
25*9c5db199SXin Lifrom autotest_lib.client.common_lib import pxssh
26*9c5db199SXin Lifrom autotest_lib.server import utils
27*9c5db199SXin Lifrom autotest_lib.server.hosts import abstract_ssh
28*9c5db199SXin Liimport six
29*9c5db199SXin Li
30*9c5db199SXin Li# In case cros_host is being ran via SSP on an older Moblab version with an
31*9c5db199SXin Li# older chromite version.
32*9c5db199SXin Litry:
33*9c5db199SXin Li    from autotest_lib.utils.frozen_chromite.lib import metrics
34*9c5db199SXin Liexcept ImportError:
35*9c5db199SXin Li    metrics = utils.metrics_mock
36*9c5db199SXin Li
37*9c5db199SXin Li
38*9c5db199SXin Lidef THIS_IS_SLOW(func):
39*9c5db199SXin Li    """Mark the given function as slow, when looking at calls to it"""
40*9c5db199SXin Li    func.__name__ = '%s__SLOW__' % func.__name__
41*9c5db199SXin Li    return func
42*9c5db199SXin Li
43*9c5db199SXin Li
44*9c5db199SXin Liclass SSHHost(abstract_ssh.AbstractSSHHost):
45*9c5db199SXin Li    """
46*9c5db199SXin Li    This class represents a remote machine controlled through an ssh
47*9c5db199SXin Li    session on which you can run programs.
48*9c5db199SXin Li
49*9c5db199SXin Li    It is not the machine autoserv is running on. The machine must be
50*9c5db199SXin Li    configured for password-less login, for example through public key
51*9c5db199SXin Li    authentication.
52*9c5db199SXin Li
53*9c5db199SXin Li    It includes support for controlling the machine through a serial
54*9c5db199SXin Li    console on which you can run programs. If such a serial console is
55*9c5db199SXin Li    set up on the machine then capabilities such as hard reset and
56*9c5db199SXin Li    boot strap monitoring are available. If the machine does not have a
57*9c5db199SXin Li    serial console available then ordinary SSH-based commands will
58*9c5db199SXin Li    still be available, but attempts to use extensions such as
59*9c5db199SXin Li    console logging or hard reset will fail silently.
60*9c5db199SXin Li
61*9c5db199SXin Li    Implementation details:
62*9c5db199SXin Li    This is a leaf class in an abstract class hierarchy, it must
63*9c5db199SXin Li    implement the unimplemented methods in parent classes.
64*9c5db199SXin Li    """
65*9c5db199SXin Li    RUN_TIMEOUT = 3600
66*9c5db199SXin Li
67*9c5db199SXin Li    def _initialize(self, hostname, *args, **dargs):
68*9c5db199SXin Li        """
69*9c5db199SXin Li        Construct a SSHHost object
70*9c5db199SXin Li
71*9c5db199SXin Li        Args:
72*9c5db199SXin Li                hostname: network hostname or address of remote machine
73*9c5db199SXin Li        """
74*9c5db199SXin Li        super(SSHHost, self)._initialize(hostname=hostname, *args, **dargs)
75*9c5db199SXin Li        self._default_run_timeout = self.RUN_TIMEOUT
76*9c5db199SXin Li        self.setup_ssh()
77*9c5db199SXin Li
78*9c5db199SXin Li
79*9c5db199SXin Li    def ssh_command(self, connect_timeout=30, options='', alive_interval=300,
80*9c5db199SXin Li                    alive_count_max=3, connection_attempts=1):
81*9c5db199SXin Li        """
82*9c5db199SXin Li        Construct an ssh command with proper args for this host.
83*9c5db199SXin Li
84*9c5db199SXin Li        @param connect_timeout: connection timeout (in seconds)
85*9c5db199SXin Li        @param options: SSH options
86*9c5db199SXin Li        @param alive_interval: SSH Alive interval.
87*9c5db199SXin Li        @param alive_count_max: SSH AliveCountMax.
88*9c5db199SXin Li        @param connection_attempts: SSH ConnectionAttempts
89*9c5db199SXin Li        """
90*9c5db199SXin Li        options = " ".join([options, self._main_ssh.ssh_option])
91*9c5db199SXin Li        base_cmd = self.make_ssh_command(user=self.user, port=self.port,
92*9c5db199SXin Li                                         opts=options,
93*9c5db199SXin Li                                         hosts_file=self.known_hosts_file,
94*9c5db199SXin Li                                         connect_timeout=connect_timeout,
95*9c5db199SXin Li                                         alive_interval=alive_interval,
96*9c5db199SXin Li                                         alive_count_max=alive_count_max,
97*9c5db199SXin Li                                         connection_attempts=connection_attempts)
98*9c5db199SXin Li        return "%s %s" % (base_cmd, self.hostname)
99*9c5db199SXin Li
100*9c5db199SXin Li    def _get_server_stack_state(self, lowest_frames=0, highest_frames=None):
101*9c5db199SXin Li        """ Get the server stack frame status.
102*9c5db199SXin Li        @param lowest_frames: the lowest frames to start printing.
103*9c5db199SXin Li        @param highest_frames: the highest frames to print.
104*9c5db199SXin Li                        (None means no restriction)
105*9c5db199SXin Li        """
106*9c5db199SXin Li        stack_frames = inspect.stack()
107*9c5db199SXin Li        stack = ''
108*9c5db199SXin Li        for frame in stack_frames[lowest_frames:highest_frames]:
109*9c5db199SXin Li            function_name = inspect.getframeinfo(frame[0]).function
110*9c5db199SXin Li            stack = '%s|%s' % (function_name, stack)
111*9c5db199SXin Li        del stack_frames
112*9c5db199SXin Li        return stack[:-1] # Delete the last '|' character
113*9c5db199SXin Li
114*9c5db199SXin Li    def _verbose_logger_command(self, command):
115*9c5db199SXin Li        """
116*9c5db199SXin Li        Prepend the command for the client with information about the ssh
117*9c5db199SXin Li        command to be executed and the server stack state.
118*9c5db199SXin Li
119*9c5db199SXin Li        @param command: the ssh command to be executed.
120*9c5db199SXin Li        """
121*9c5db199SXin Li        # The last few frames on the stack are not useful, so skip them.
122*9c5db199SXin Li        stack = self._get_server_stack_state(lowest_frames=3, highest_frames=6)
123*9c5db199SXin Li        # If logger executable exists on the DUT, use it to report the command.
124*9c5db199SXin Li        # Then regardless of logger, run the command as usual.
125*9c5db199SXin Li        command = ('test -x /usr/bin/logger && /usr/bin/logger'
126*9c5db199SXin Li                   ' -t autotest "from [%s] ssh_run: %s"; %s'
127*9c5db199SXin Li                   % (stack, utils.sh_escape(command), command))
128*9c5db199SXin Li        return command
129*9c5db199SXin Li
130*9c5db199SXin Li    def _tls_run(self, original_cmd, timeout, ignore_status, stdout, stderr,
131*9c5db199SXin Li                 args, ignore_timeout):
132*9c5db199SXin Li        """Helper function for run(), uses the tls client."""
133*9c5db199SXin Li        if not self.tls_connection.alive:
134*9c5db199SXin Li            raise error.TLSConnectionError("TLS not connected.")
135*9c5db199SXin Li        original_cmd = ' '.join([original_cmd] +
136*9c5db199SXin Li                                [utils.sh_quote_word(arg) for arg in args])
137*9c5db199SXin Li
138*9c5db199SXin Li        try:
139*9c5db199SXin Li            result = self.tls_exec_dut_command_client.run_cmd(original_cmd, timeout,
140*9c5db199SXin Li                                                       stdout, stderr,
141*9c5db199SXin Li                                                       ignore_timeout)
142*9c5db199SXin Li        except Exception as e:
143*9c5db199SXin Li            logging.warning("TLS Client run err %s", e)
144*9c5db199SXin Li            raise e
145*9c5db199SXin Li
146*9c5db199SXin Li        if not ignore_status and result.exit_status > 0:
147*9c5db199SXin Li            msg = result.stderr.strip()
148*9c5db199SXin Li            if not msg:
149*9c5db199SXin Li                msg = result.stdout.strip()
150*9c5db199SXin Li                if msg:
151*9c5db199SXin Li                    msg = msg.splitlines()[-1]
152*9c5db199SXin Li            raise error.AutoservRunError(
153*9c5db199SXin Li                    "command execution error using TLS (%d): %s" %
154*9c5db199SXin Li                    (result.exit_status, msg), result)
155*9c5db199SXin Li
156*9c5db199SXin Li        return result
157*9c5db199SXin Li
158*9c5db199SXin Li    def _run(self, command, timeout, ignore_status, stdout, stderr,
159*9c5db199SXin Li             connect_timeout, env, options, stdin, args, ignore_timeout,
160*9c5db199SXin Li             ssh_failure_retry_ok, verbose):
161*9c5db199SXin Li        """Helper function for run()."""
162*9c5db199SXin Li        if connect_timeout > timeout:
163*9c5db199SXin Li            # timeout passed from run() may be smaller than 1, because we
164*9c5db199SXin Li            # subtract the elapsed time from the original timeout supplied.
165*9c5db199SXin Li            connect_timeout = max(int(timeout), 1)
166*9c5db199SXin Li        original_cmd = command
167*9c5db199SXin Li
168*9c5db199SXin Li        # If TLS client has been built, and not marked as unstable, use it.
169*9c5db199SXin Li        # NOTE: if the tls_enabled setting in the config is not True, the
170*9c5db199SXin Li        # client will not have been built.
171*9c5db199SXin Li        use_tls = self.tls_exec_dut_command_client and not self.tls_unstable
172*9c5db199SXin Li
173*9c5db199SXin Li        if verbose:
174*9c5db199SXin Li            stack = self._get_server_stack_state(lowest_frames=2,
175*9c5db199SXin Li                                                 highest_frames=8)
176*9c5db199SXin Li
177*9c5db199SXin Li            logging.debug("Running (via %s) '%s' from '%s'",
178*9c5db199SXin Li                          'TLS' if use_tls else 'SSH', command, stack)
179*9c5db199SXin Li            command = self._verbose_logger_command(command)
180*9c5db199SXin Li
181*9c5db199SXin Li        if use_tls:
182*9c5db199SXin Li            try:
183*9c5db199SXin Li                return self._tls_run(command, timeout, ignore_status, stdout,
184*9c5db199SXin Li                                     stderr, args, ignore_timeout)
185*9c5db199SXin Li            except (error.AutoservRunError, error.CmdTimeoutError) as e:
186*9c5db199SXin Li                raise e
187*9c5db199SXin Li            except Exception as e:
188*9c5db199SXin Li                # If TLS fails for unknown reason, we will revert to normal ssh.
189*9c5db199SXin Li                logging.warning(
190*9c5db199SXin Li                        "Unexpected TLS cmd failed. Reverting to SSH.\n %s", e)
191*9c5db199SXin Li
192*9c5db199SXin Li                # Note the TLS as unstable so we do not attempt to re-start it.
193*9c5db199SXin Li                self.tls_unstable = True
194*9c5db199SXin Li
195*9c5db199SXin Li        ssh_cmd = self.ssh_command(connect_timeout, options)
196*9c5db199SXin Li        if not env.strip():
197*9c5db199SXin Li            env = ""
198*9c5db199SXin Li        else:
199*9c5db199SXin Li            env = "export %s;" % env
200*9c5db199SXin Li        for arg in args:
201*9c5db199SXin Li            command += ' "%s"' % utils.sh_escape(arg)
202*9c5db199SXin Li        full_cmd = '%s "%s %s"' % (ssh_cmd, env, utils.sh_escape(command))
203*9c5db199SXin Li
204*9c5db199SXin Li        def counters_inc(counter_name, failure_name):
205*9c5db199SXin Li            """Helper function to increment metrics counters.
206*9c5db199SXin Li            @param counter_name: string indicating which counter to use
207*9c5db199SXin Li            @param failure_name: string indentifying an error, or 'success'
208*9c5db199SXin Li            """
209*9c5db199SXin Li            if counter_name == 'call':
210*9c5db199SXin Li                # ssh_counter records the outcome of each ssh invocation
211*9c5db199SXin Li                # inside _run(), including exceptions.
212*9c5db199SXin Li                ssh_counter = metrics.Counter('chromeos/autotest/ssh/calls')
213*9c5db199SXin Li                fields = {'error' : failure_name or 'success',
214*9c5db199SXin Li                          'attempt' : ssh_call_count}
215*9c5db199SXin Li                ssh_counter.increment(fields=fields)
216*9c5db199SXin Li
217*9c5db199SXin Li            if counter_name == 'run':
218*9c5db199SXin Li                # run_counter records each call to _run() with its result
219*9c5db199SXin Li                # and how many tries were made.  Calls are recorded when
220*9c5db199SXin Li                # _run() exits (including exiting with an exception)
221*9c5db199SXin Li                run_counter = metrics.Counter('chromeos/autotest/ssh/runs')
222*9c5db199SXin Li                fields = {'error' : failure_name or 'success',
223*9c5db199SXin Li                          'attempt' : ssh_call_count}
224*9c5db199SXin Li                run_counter.increment(fields=fields)
225*9c5db199SXin Li
226*9c5db199SXin Li        # If ssh_failure_retry_ok is True, retry twice on timeouts and generic
227*9c5db199SXin Li        # error 255: if a simple retry doesn't work, kill the ssh main
228*9c5db199SXin Li        # connection and try again.  (Note that either error could come from
229*9c5db199SXin Li        # the command running in the DUT, in which case the retry may be
230*9c5db199SXin Li        # useless but, in theory, also harmless.)
231*9c5db199SXin Li        if ssh_failure_retry_ok:
232*9c5db199SXin Li            # Ignore ssh command timeout, even though it could be a timeout due
233*9c5db199SXin Li            # to the command executing in the remote host.  Note that passing
234*9c5db199SXin Li            # ignore_timeout = True makes utils.run() return None on timeouts
235*9c5db199SXin Li            # (and only on timeouts).
236*9c5db199SXin Li            original_ignore_timeout = ignore_timeout
237*9c5db199SXin Li            ignore_timeout = True
238*9c5db199SXin Li            ssh_failure_retry_count = 2
239*9c5db199SXin Li        else:
240*9c5db199SXin Li            ssh_failure_retry_count = 0
241*9c5db199SXin Li
242*9c5db199SXin Li        ssh_call_count = 0
243*9c5db199SXin Li
244*9c5db199SXin Li        while True:
245*9c5db199SXin Li            try:
246*9c5db199SXin Li                # Increment call count first, in case utils.run() throws an
247*9c5db199SXin Li                # exception.
248*9c5db199SXin Li                ssh_call_count += 1
249*9c5db199SXin Li                result = utils.run(full_cmd, timeout, True, stdout, stderr,
250*9c5db199SXin Li                                   verbose=False, stdin=stdin,
251*9c5db199SXin Li                                   stderr_is_expected=ignore_status,
252*9c5db199SXin Li                                   ignore_timeout=ignore_timeout)
253*9c5db199SXin Li            except Exception as e:
254*9c5db199SXin Li                # No retries on exception.
255*9c5db199SXin Li                counters_inc('call', 'exception')
256*9c5db199SXin Li                counters_inc('run', 'exception')
257*9c5db199SXin Li                raise e
258*9c5db199SXin Li
259*9c5db199SXin Li            failure_name = None
260*9c5db199SXin Li
261*9c5db199SXin Li            if result:
262*9c5db199SXin Li                if result.exit_status == 255:
263*9c5db199SXin Li                    if re.search(r'^ssh: .*: Name or service not known',
264*9c5db199SXin Li                                 result.stderr):
265*9c5db199SXin Li                        failure_name = 'dns_failure'
266*9c5db199SXin Li                    else:
267*9c5db199SXin Li                        failure_name = 'error_255'
268*9c5db199SXin Li                elif result.exit_status > 0:
269*9c5db199SXin Li                    failure_name = 'nonzero_status'
270*9c5db199SXin Li            else:
271*9c5db199SXin Li                # result == None
272*9c5db199SXin Li                failure_name = 'timeout'
273*9c5db199SXin Li
274*9c5db199SXin Li            # Record the outcome of the ssh invocation.
275*9c5db199SXin Li            counters_inc('call', failure_name)
276*9c5db199SXin Li
277*9c5db199SXin Li            if failure_name:
278*9c5db199SXin Li                # There was a failure: decide whether to retry.
279*9c5db199SXin Li                if failure_name == 'dns_failure':
280*9c5db199SXin Li                    raise error.AutoservSshDnsError("DNS Failure: ", result)
281*9c5db199SXin Li                else:
282*9c5db199SXin Li                    if ssh_failure_retry_count == 2:
283*9c5db199SXin Li                        logging.debug('retrying ssh command after %s',
284*9c5db199SXin Li                                       failure_name)
285*9c5db199SXin Li                        ssh_failure_retry_count -= 1
286*9c5db199SXin Li                        continue
287*9c5db199SXin Li                    elif ssh_failure_retry_count == 1:
288*9c5db199SXin Li                        # After two failures, restart the main connection
289*9c5db199SXin Li                        # before the final try.
290*9c5db199SXin Li                        stack = self._get_server_stack_state(lowest_frames=1,
291*9c5db199SXin Li                                                             highest_frames=7)
292*9c5db199SXin Li                        logging.debug(
293*9c5db199SXin Li                                'retry 2: restarting main connection from \'%s\'',
294*9c5db199SXin Li                                stack)
295*9c5db199SXin Li                        self.restart_main_ssh()
296*9c5db199SXin Li                        # Last retry: reinstate timeout behavior.
297*9c5db199SXin Li                        ignore_timeout = original_ignore_timeout
298*9c5db199SXin Li                        ssh_failure_retry_count -= 1
299*9c5db199SXin Li                        continue
300*9c5db199SXin Li
301*9c5db199SXin Li            # No retry conditions occurred.  Exit the loop.
302*9c5db199SXin Li            break
303*9c5db199SXin Li
304*9c5db199SXin Li        # The outcomes of ssh invocations have been recorded.  Now record
305*9c5db199SXin Li        # the outcome of this function.
306*9c5db199SXin Li
307*9c5db199SXin Li        if ignore_timeout and not result:
308*9c5db199SXin Li            counters_inc('run', 'ignored_timeout')
309*9c5db199SXin Li            return None
310*9c5db199SXin Li
311*9c5db199SXin Li        # The error messages will show up in band (indistinguishable
312*9c5db199SXin Li        # from stuff sent through the SSH connection), so we have the
313*9c5db199SXin Li        # remote computer echo the message "Connected." before running
314*9c5db199SXin Li        # any command.  Since the following 2 errors have to do with
315*9c5db199SXin Li        # connecting, it's safe to do these checks.
316*9c5db199SXin Li        if result.exit_status == 255:
317*9c5db199SXin Li            if re.search(r'^ssh: connect to host .* port .*: '
318*9c5db199SXin Li                         r'Connection timed out\r$', result.stderr):
319*9c5db199SXin Li                counters_inc('run', 'final_timeout')
320*9c5db199SXin Li                raise error.AutoservSSHTimeout(
321*9c5db199SXin Li                        "ssh timed out: %r" % original_cmd.strip(), result)
322*9c5db199SXin Li            if "Permission denied." in result.stderr:
323*9c5db199SXin Li                msg = "ssh permission denied"
324*9c5db199SXin Li                counters_inc('run', 'final_eperm')
325*9c5db199SXin Li                raise error.AutoservSshPermissionDeniedError(msg, result)
326*9c5db199SXin Li
327*9c5db199SXin Li        if not ignore_status and result.exit_status > 0:
328*9c5db199SXin Li            counters_inc('run', 'final_run_error')
329*9c5db199SXin Li            msg = result.stderr.strip()
330*9c5db199SXin Li            if not msg:
331*9c5db199SXin Li                msg = result.stdout.strip()
332*9c5db199SXin Li                if msg:
333*9c5db199SXin Li                    msg = msg.splitlines()[-1]
334*9c5db199SXin Li            raise error.AutoservRunError("command execution error (%d): %r" %
335*9c5db199SXin Li                                         (result.exit_status, msg), result)
336*9c5db199SXin Li
337*9c5db199SXin Li        counters_inc('run', failure_name)
338*9c5db199SXin Li        return result
339*9c5db199SXin Li
340*9c5db199SXin Li    def set_default_run_timeout(self, timeout):
341*9c5db199SXin Li        """Set the default timeout for run."""
342*9c5db199SXin Li        if timeout < 0:
343*9c5db199SXin Li            raise error.TestError('Invalid timeout %d', timeout)
344*9c5db199SXin Li        self._default_run_timeout = timeout
345*9c5db199SXin Li
346*9c5db199SXin Li    @THIS_IS_SLOW
347*9c5db199SXin Li    def run(self, command, timeout=None, ignore_status=False,
348*9c5db199SXin Li            stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
349*9c5db199SXin Li            connect_timeout=30, options='', stdin=None, verbose=True, args=(),
350*9c5db199SXin Li            ignore_timeout=False, ssh_failure_retry_ok=False):
351*9c5db199SXin Li        """
352*9c5db199SXin Li        Run a command on the remote host.
353*9c5db199SXin Li        @note: This RPC call has an overhead of minimum 40ms and up to 400ms on
354*9c5db199SXin Li               servers (crbug.com/734887). Each time a call is added for
355*9c5db199SXin Li               every job, a server core dies in the lab.
356*9c5db199SXin Li        @see: common_lib.hosts.host.run()
357*9c5db199SXin Li
358*9c5db199SXin Li        @param timeout: command execution timeout in seconds. Default is
359*9c5db199SXin Li                        _default_run_timeout (1 hour).
360*9c5db199SXin Li        @param connect_timeout: ssh connection timeout (in seconds)
361*9c5db199SXin Li        @param options: string with additional ssh command options
362*9c5db199SXin Li        @param verbose: log the commands
363*9c5db199SXin Li        @param ignore_timeout: bool True if SSH command timeouts should be
364*9c5db199SXin Li                ignored.  Will return None on command timeout.
365*9c5db199SXin Li        @param ssh_failure_retry_ok: True if the command may be retried on
366*9c5db199SXin Li                probable ssh failure (error 255 or timeout).  When true,
367*9c5db199SXin Li                the command may be executed up to three times, the second
368*9c5db199SXin Li                time after restarting the ssh main connection.  Use only for
369*9c5db199SXin Li                commands that are idempotent, because when a "probable
370*9c5db199SXin Li                ssh failure" occurs, we cannot tell if the command executed
371*9c5db199SXin Li                or not.
372*9c5db199SXin Li
373*9c5db199SXin Li        @raises AutoservRunError: if the command failed
374*9c5db199SXin Li        @raises AutoservSSHTimeout: ssh connection has timed out
375*9c5db199SXin Li        """
376*9c5db199SXin Li        # For example if the command is a list, we need to convert it to a
377*9c5db199SXin Li        # string first.
378*9c5db199SXin Li        if not isinstance(command, six.string_types):
379*9c5db199SXin Li            command = ' '.join(command)
380*9c5db199SXin Li
381*9c5db199SXin Li        if timeout is None:
382*9c5db199SXin Li            timeout = self._default_run_timeout
383*9c5db199SXin Li        start_time = time.time()
384*9c5db199SXin Li        with metrics.SecondsTimer('chromeos/autotest/ssh/main_ssh_time',
385*9c5db199SXin Li                                  scale=0.001):
386*9c5db199SXin Li
387*9c5db199SXin Li            self.start_main_ssh(min(
388*9c5db199SXin Li                    timeout,
389*9c5db199SXin Li                    self.DEFAULT_START_MAIN_SSH_TIMEOUT_S,
390*9c5db199SXin Li            ))
391*9c5db199SXin Li
392*9c5db199SXin Li            env = " ".join("=".join(pair) for pair in six.iteritems(self.env))
393*9c5db199SXin Li            elapsed = time.time() - start_time
394*9c5db199SXin Li            try:
395*9c5db199SXin Li                return self._run(command, timeout - elapsed, ignore_status,
396*9c5db199SXin Li                                 stdout_tee, stderr_tee, connect_timeout, env,
397*9c5db199SXin Li                                 options, stdin, args, ignore_timeout,
398*9c5db199SXin Li                                 ssh_failure_retry_ok, verbose)
399*9c5db199SXin Li            except error.CmdError as cmderr:
400*9c5db199SXin Li                # We get a CmdError here only if there is timeout of that
401*9c5db199SXin Li                # command. Catch that and stuff it into AutoservRunError and
402*9c5db199SXin Li                # raise it.
403*9c5db199SXin Li                timeout_message = str('Timeout encountered: %s' %
404*9c5db199SXin Li                                      cmderr.args[0])
405*9c5db199SXin Li                raise error.AutoservRunError(timeout_message, cmderr.args[1])
406*9c5db199SXin Li
407*9c5db199SXin Li
408*9c5db199SXin Li    def run_background(self, command, verbose=True):
409*9c5db199SXin Li        """Start a command on the host in the background.
410*9c5db199SXin Li
411*9c5db199SXin Li        The command is started on the host in the background, and
412*9c5db199SXin Li        this method call returns immediately without waiting for the
413*9c5db199SXin Li        command's completion.  The PID of the process on the host is
414*9c5db199SXin Li        returned as a string.
415*9c5db199SXin Li
416*9c5db199SXin Li        The command may redirect its stdin, stdout, or stderr as
417*9c5db199SXin Li        necessary.  Without redirection, all input and output will
418*9c5db199SXin Li        use /dev/null.
419*9c5db199SXin Li
420*9c5db199SXin Li        @param command The command to run in the background
421*9c5db199SXin Li        @param verbose As for `self.run()`
422*9c5db199SXin Li
423*9c5db199SXin Li        @return Returns the PID of the remote background process
424*9c5db199SXin Li                as a string.
425*9c5db199SXin Li        """
426*9c5db199SXin Li        # Redirection here isn't merely hygienic; it's a functional
427*9c5db199SXin Li        # requirement.  sshd won't terminate until stdin, stdout,
428*9c5db199SXin Li        # and stderr are all closed.
429*9c5db199SXin Li        #
430*9c5db199SXin Li        # The subshell is needed to do the right thing in case the
431*9c5db199SXin Li        # passed in command has its own I/O redirections.
432*9c5db199SXin Li        cmd_fmt = '( %s ) </dev/null >/dev/null 2>&1 & echo -n $!'
433*9c5db199SXin Li        return self.run(cmd_fmt % command, verbose=verbose).stdout
434*9c5db199SXin Li
435*9c5db199SXin Li
436*9c5db199SXin Li    def run_short(self, command, **kwargs):
437*9c5db199SXin Li        """
438*9c5db199SXin Li        Calls the run() command with a short default timeout.
439*9c5db199SXin Li
440*9c5db199SXin Li        Takes the same arguments as does run(),
441*9c5db199SXin Li        with the exception of the timeout argument which
442*9c5db199SXin Li        here is fixed at 60 seconds.
443*9c5db199SXin Li        It returns the result of run.
444*9c5db199SXin Li
445*9c5db199SXin Li        @param command: the command line string
446*9c5db199SXin Li
447*9c5db199SXin Li        """
448*9c5db199SXin Li        return self.run(command, timeout=60, **kwargs)
449*9c5db199SXin Li
450*9c5db199SXin Li
451*9c5db199SXin Li    def run_grep(self, command, timeout=30, ignore_status=False,
452*9c5db199SXin Li                 stdout_ok_regexp=None, stdout_err_regexp=None,
453*9c5db199SXin Li                 stderr_ok_regexp=None, stderr_err_regexp=None,
454*9c5db199SXin Li                 connect_timeout=30):
455*9c5db199SXin Li        """
456*9c5db199SXin Li        Run a command on the remote host and look for regexp
457*9c5db199SXin Li        in stdout or stderr to determine if the command was
458*9c5db199SXin Li        successul or not.
459*9c5db199SXin Li
460*9c5db199SXin Li
461*9c5db199SXin Li        @param command: the command line string
462*9c5db199SXin Li        @param timeout: time limit in seconds before attempting to
463*9c5db199SXin Li                        kill the running process. The run() function
464*9c5db199SXin Li                        will take a few seconds longer than 'timeout'
465*9c5db199SXin Li                        to complete if it has to kill the process.
466*9c5db199SXin Li        @param ignore_status: do not raise an exception, no matter
467*9c5db199SXin Li                              what the exit code of the command is.
468*9c5db199SXin Li        @param stdout_ok_regexp: regexp that should be in stdout
469*9c5db199SXin Li                                 if the command was successul.
470*9c5db199SXin Li        @param stdout_err_regexp: regexp that should be in stdout
471*9c5db199SXin Li                                  if the command failed.
472*9c5db199SXin Li        @param stderr_ok_regexp: regexp that should be in stderr
473*9c5db199SXin Li                                 if the command was successul.
474*9c5db199SXin Li        @param stderr_err_regexp: regexp that should be in stderr
475*9c5db199SXin Li                                 if the command failed.
476*9c5db199SXin Li        @param connect_timeout: connection timeout (in seconds)
477*9c5db199SXin Li
478*9c5db199SXin Li        Returns:
479*9c5db199SXin Li                if the command was successul, raises an exception
480*9c5db199SXin Li                otherwise.
481*9c5db199SXin Li
482*9c5db199SXin Li        Raises:
483*9c5db199SXin Li                AutoservRunError:
484*9c5db199SXin Li                - the exit code of the command execution was not 0.
485*9c5db199SXin Li                - If stderr_err_regexp is found in stderr,
486*9c5db199SXin Li                - If stdout_err_regexp is found in stdout,
487*9c5db199SXin Li                - If stderr_ok_regexp is not found in stderr.
488*9c5db199SXin Li                - If stdout_ok_regexp is not found in stdout,
489*9c5db199SXin Li        """
490*9c5db199SXin Li
491*9c5db199SXin Li        # We ignore the status, because we will handle it at the end.
492*9c5db199SXin Li        result = self.run(command, timeout, ignore_status=True,
493*9c5db199SXin Li                          connect_timeout=connect_timeout)
494*9c5db199SXin Li
495*9c5db199SXin Li        # Look for the patterns, in order
496*9c5db199SXin Li        for (regexp, stream) in ((stderr_err_regexp, result.stderr),
497*9c5db199SXin Li                                 (stdout_err_regexp, result.stdout)):
498*9c5db199SXin Li            if regexp and stream:
499*9c5db199SXin Li                err_re = re.compile (regexp)
500*9c5db199SXin Li                if err_re.search(stream):
501*9c5db199SXin Li                    raise error.AutoservRunError(
502*9c5db199SXin Li                        '%r failed, found error pattern: %r' % (command,
503*9c5db199SXin Li                                                                regexp), result)
504*9c5db199SXin Li
505*9c5db199SXin Li        for (regexp, stream) in ((stderr_ok_regexp, result.stderr),
506*9c5db199SXin Li                                 (stdout_ok_regexp, result.stdout)):
507*9c5db199SXin Li            if regexp and stream:
508*9c5db199SXin Li                ok_re = re.compile (regexp)
509*9c5db199SXin Li                if ok_re.search(stream):
510*9c5db199SXin Li                    if ok_re.search(stream):
511*9c5db199SXin Li                        return
512*9c5db199SXin Li
513*9c5db199SXin Li        if not ignore_status and result.exit_status > 0:
514*9c5db199SXin Li            msg = result.stderr.strip()
515*9c5db199SXin Li            if not msg:
516*9c5db199SXin Li                msg = result.stdout.strip()
517*9c5db199SXin Li                if msg:
518*9c5db199SXin Li                    msg = msg.splitlines()[-1]
519*9c5db199SXin Li            raise error.AutoservRunError("command execution error (%d): %r" %
520*9c5db199SXin Li                                         (result.exit_status, msg), result)
521*9c5db199SXin Li
522*9c5db199SXin Li
523*9c5db199SXin Li    def setup_ssh_key(self):
524*9c5db199SXin Li        """Setup SSH Key"""
525*9c5db199SXin Li        logging.debug('Performing SSH key setup on %s as %s.',
526*9c5db199SXin Li                      self.host_port, self.user)
527*9c5db199SXin Li
528*9c5db199SXin Li        try:
529*9c5db199SXin Li            host = pxssh.pxssh()
530*9c5db199SXin Li            host.login(self.hostname, self.user, self.password,
531*9c5db199SXin Li                        port=self.port)
532*9c5db199SXin Li            public_key = utils.get_public_key()
533*9c5db199SXin Li
534*9c5db199SXin Li            host.sendline('mkdir -p ~/.ssh')
535*9c5db199SXin Li            host.prompt()
536*9c5db199SXin Li            host.sendline('chmod 700 ~/.ssh')
537*9c5db199SXin Li            host.prompt()
538*9c5db199SXin Li            host.sendline("echo '%s' >> ~/.ssh/authorized_keys; " %
539*9c5db199SXin Li                            public_key)
540*9c5db199SXin Li            host.prompt()
541*9c5db199SXin Li            host.sendline('chmod 600 ~/.ssh/authorized_keys')
542*9c5db199SXin Li            host.prompt()
543*9c5db199SXin Li            host.logout()
544*9c5db199SXin Li
545*9c5db199SXin Li            logging.debug('SSH key setup complete.')
546*9c5db199SXin Li
547*9c5db199SXin Li        except:
548*9c5db199SXin Li            logging.debug('SSH key setup has failed.')
549*9c5db199SXin Li            try:
550*9c5db199SXin Li                host.logout()
551*9c5db199SXin Li            except:
552*9c5db199SXin Li                pass
553*9c5db199SXin Li
554*9c5db199SXin Li
555*9c5db199SXin Li    def setup_ssh(self):
556*9c5db199SXin Li        """Setup SSH"""
557*9c5db199SXin Li        if self.password:
558*9c5db199SXin Li            try:
559*9c5db199SXin Li                self.ssh_ping()
560*9c5db199SXin Li            except error.AutoservSshPingHostError:
561*9c5db199SXin Li                self.setup_ssh_key()
562