blob: 073bf55d1241beb6d0d6568bb06e584cb74e3cd5 [file] [log] [blame]
Federico Ressi0e04f8f2018-10-24 12:19:05 +02001# Copyright (c) 2018 Red Hat, Inc.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import collections
18import subprocess
19import sys
20
21from oslo_log import log
22from tempest.lib import exceptions as lib_exc
23
24from neutron_tempest_plugin.common import ssh
25from neutron_tempest_plugin import config
26from neutron_tempest_plugin import exceptions
27
28
29LOG = log.getLogger(__name__)
30
31CONF = config.CONF
32
33if ssh.Client.proxy_jump_host:
34 # Perform all SSH connections passing through configured SSH server
35 SSH_PROXY_CLIENT = ssh.Client.create_proxy_client()
36else:
37 SSH_PROXY_CLIENT = None
38
39
40def execute(command, ssh_client=None, timeout=None, check=True):
41 """Execute command inside a remote or local shell
42
43 :param command: command string to be executed
44
45 :param ssh_client: SSH client instance used for remote shell execution
46
47 :param timeout: command execution timeout in seconds
48
Slawek Kaplonski86620da2020-02-06 10:41:36 +010049 :param check: when False it doesn't raises ShellCommandFailed when
Elod Illesf2e985e2023-11-06 19:30:29 +010050 exit status is not zero. True by default
Federico Ressi0e04f8f2018-10-24 12:19:05 +020051
52 :returns: STDOUT text when command execution terminates with zero exit
Elod Illesf2e985e2023-11-06 19:30:29 +010053 status.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020054
55 :raises ShellTimeoutExpired: when timeout expires before command execution
Elod Illesf2e985e2023-11-06 19:30:29 +010056 terminates. In such case it kills the process, then it eventually would
57 try to read STDOUT and STDERR buffers (not fully implemented) before
58 raising the exception.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020059
Slawek Kaplonski86620da2020-02-06 10:41:36 +010060 :raises ShellCommandFailed: when command execution terminates with non-zero
Elod Illesf2e985e2023-11-06 19:30:29 +010061 exit status.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020062 """
63 ssh_client = ssh_client or SSH_PROXY_CLIENT
64 if timeout:
65 timeout = float(timeout)
66
67 if ssh_client:
68 result = execute_remote_command(command=command, timeout=timeout,
69 ssh_client=ssh_client)
70 else:
71 result = execute_local_command(command=command, timeout=timeout)
72
73 if result.exit_status == 0:
74 LOG.debug("Command %r succeeded:\n"
75 "stderr:\n%s\n"
76 "stdout:\n%s\n",
77 command, result.stderr, result.stdout)
78 elif result.exit_status is None:
79 LOG.debug("Command %r timeout expired (timeout=%s):\n"
80 "stderr:\n%s\n"
81 "stdout:\n%s\n",
82 command, timeout, result.stderr, result.stdout)
83 else:
84 LOG.debug("Command %r failed (exit_status=%s):\n"
85 "stderr:\n%s\n"
86 "stdout:\n%s\n",
87 command, result.exit_status, result.stderr, result.stdout)
88 if check:
89 result.check()
90
91 return result
92
93
94def execute_remote_command(command, ssh_client, timeout=None):
95 """Execute command on a remote host using SSH client"""
96 LOG.debug("Executing command %r on remote host %r (timeout=%r)...",
97 command, ssh_client.host, timeout)
98
99 stdout = stderr = exit_status = None
100
101 try:
102 # TODO(fressi): re-implement to capture stderr
103 stdout = ssh_client.exec_command(command, timeout=timeout)
104 exit_status = 0
105
106 except lib_exc.TimeoutException:
107 # TODO(fressi): re-implement to capture STDOUT and STDERR and make
108 # sure process is killed
109 pass
110
111 except lib_exc.SSHExecCommandFailed as ex:
112 # Please note class SSHExecCommandFailed has been re-based on
Slawek Kaplonski86620da2020-02-06 10:41:36 +0100113 # top of ShellCommandFailed
Federico Ressi0e04f8f2018-10-24 12:19:05 +0200114 stdout = ex.stdout
115 stderr = ex.stderr
116 exit_status = ex.exit_status
117
118 return ShellExecuteResult(command=command, timeout=timeout,
119 exit_status=exit_status,
120 stdout=stdout, stderr=stderr)
121
122
123def execute_local_command(command, timeout=None):
124 """Execute command on local host using local shell"""
125
126 LOG.debug("Executing command %r on local host (timeout=%r)...",
127 command, timeout)
128
129 process = subprocess.Popen(command, shell=True,
130 universal_newlines=True,
131 stdout=subprocess.PIPE,
132 stderr=subprocess.PIPE)
133
134 if timeout and sys.version_info < (3, 3):
135 # TODO(fressi): re-implement to timeout support on older Pythons
136 LOG.warning("Popen.communicate method doens't support for timeout "
137 "on Python %r", sys.version)
138 timeout = None
139
140 # Wait for process execution while reading STDERR and STDOUT streams
141 if timeout:
142 try:
143 stdout, stderr = process.communicate(timeout=timeout)
144 except subprocess.TimeoutExpired:
145 # At this state I expect the process to be still running
146 # therefore it has to be kill later after calling poll()
147 LOG.exception("Command %r timeout expired.", command)
148 stdout = stderr = None
149 else:
150 stdout, stderr = process.communicate()
151
152 # Check process termination status
153 exit_status = process.poll()
154 if exit_status is None:
155 # The process is still running after calling communicate():
156 # let kill it and then read buffers again
157 process.kill()
158 stdout, stderr = process.communicate()
159
160 return ShellExecuteResult(command=command, timeout=timeout,
161 stdout=stdout, stderr=stderr,
162 exit_status=exit_status)
163
164
165class ShellExecuteResult(collections.namedtuple(
166 'ShellExecuteResult', ['command', 'timeout', 'exit_status', 'stdout',
167 'stderr'])):
168
169 def check(self):
170 if self.exit_status is None:
171 raise exceptions.ShellTimeoutExpired(command=self.command,
172 timeout=self.timeout,
173 stderr=self.stderr,
174 stdout=self.stdout)
175
176 elif self.exit_status != 0:
Slawek Kaplonski86620da2020-02-06 10:41:36 +0100177 raise exceptions.ShellCommandFailed(command=self.command,
178 exit_status=self.exit_status,
179 stderr=self.stderr,
180 stdout=self.stdout)