blob: 723c30ed2c1ca58ab5fa85165d5f3a0ef94220dd [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
Federico Ressi0e04f8f2018-10-24 12:19:05 +020019
20from oslo_log import log
21from tempest.lib import exceptions as lib_exc
22
23from neutron_tempest_plugin.common import ssh
24from neutron_tempest_plugin import config
25from neutron_tempest_plugin import exceptions
26
27
28LOG = log.getLogger(__name__)
29
30CONF = config.CONF
31
32if ssh.Client.proxy_jump_host:
33 # Perform all SSH connections passing through configured SSH server
34 SSH_PROXY_CLIENT = ssh.Client.create_proxy_client()
35else:
36 SSH_PROXY_CLIENT = None
37
38
39def execute(command, ssh_client=None, timeout=None, check=True):
40 """Execute command inside a remote or local shell
41
42 :param command: command string to be executed
43
44 :param ssh_client: SSH client instance used for remote shell execution
45
46 :param timeout: command execution timeout in seconds
47
Slawek Kaplonski86620da2020-02-06 10:41:36 +010048 :param check: when False it doesn't raises ShellCommandFailed when
Elod Illesf2e985e2023-11-06 19:30:29 +010049 exit status is not zero. True by default
Federico Ressi0e04f8f2018-10-24 12:19:05 +020050
51 :returns: STDOUT text when command execution terminates with zero exit
Elod Illesf2e985e2023-11-06 19:30:29 +010052 status.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020053
54 :raises ShellTimeoutExpired: when timeout expires before command execution
Elod Illesf2e985e2023-11-06 19:30:29 +010055 terminates. In such case it kills the process, then it eventually would
56 try to read STDOUT and STDERR buffers (not fully implemented) before
57 raising the exception.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020058
Slawek Kaplonski86620da2020-02-06 10:41:36 +010059 :raises ShellCommandFailed: when command execution terminates with non-zero
Elod Illesf2e985e2023-11-06 19:30:29 +010060 exit status.
Federico Ressi0e04f8f2018-10-24 12:19:05 +020061 """
62 ssh_client = ssh_client or SSH_PROXY_CLIENT
63 if timeout:
64 timeout = float(timeout)
65
66 if ssh_client:
67 result = execute_remote_command(command=command, timeout=timeout,
68 ssh_client=ssh_client)
69 else:
70 result = execute_local_command(command=command, timeout=timeout)
71
72 if result.exit_status == 0:
73 LOG.debug("Command %r succeeded:\n"
74 "stderr:\n%s\n"
75 "stdout:\n%s\n",
76 command, result.stderr, result.stdout)
77 elif result.exit_status is None:
78 LOG.debug("Command %r timeout expired (timeout=%s):\n"
79 "stderr:\n%s\n"
80 "stdout:\n%s\n",
81 command, timeout, result.stderr, result.stdout)
82 else:
83 LOG.debug("Command %r failed (exit_status=%s):\n"
84 "stderr:\n%s\n"
85 "stdout:\n%s\n",
86 command, result.exit_status, result.stderr, result.stdout)
87 if check:
88 result.check()
89
90 return result
91
92
93def execute_remote_command(command, ssh_client, timeout=None):
94 """Execute command on a remote host using SSH client"""
95 LOG.debug("Executing command %r on remote host %r (timeout=%r)...",
96 command, ssh_client.host, timeout)
97
98 stdout = stderr = exit_status = None
99
100 try:
101 # TODO(fressi): re-implement to capture stderr
102 stdout = ssh_client.exec_command(command, timeout=timeout)
103 exit_status = 0
104
105 except lib_exc.TimeoutException:
106 # TODO(fressi): re-implement to capture STDOUT and STDERR and make
107 # sure process is killed
108 pass
109
110 except lib_exc.SSHExecCommandFailed as ex:
111 # Please note class SSHExecCommandFailed has been re-based on
Slawek Kaplonski86620da2020-02-06 10:41:36 +0100112 # top of ShellCommandFailed
Federico Ressi0e04f8f2018-10-24 12:19:05 +0200113 stdout = ex.stdout
114 stderr = ex.stderr
115 exit_status = ex.exit_status
116
117 return ShellExecuteResult(command=command, timeout=timeout,
118 exit_status=exit_status,
119 stdout=stdout, stderr=stderr)
120
121
122def execute_local_command(command, timeout=None):
123 """Execute command on local host using local shell"""
124
125 LOG.debug("Executing command %r on local host (timeout=%r)...",
126 command, timeout)
127
128 process = subprocess.Popen(command, shell=True,
129 universal_newlines=True,
130 stdout=subprocess.PIPE,
131 stderr=subprocess.PIPE)
132
Federico Ressi0e04f8f2018-10-24 12:19:05 +0200133 # Wait for process execution while reading STDERR and STDOUT streams
134 if timeout:
135 try:
136 stdout, stderr = process.communicate(timeout=timeout)
137 except subprocess.TimeoutExpired:
138 # At this state I expect the process to be still running
139 # therefore it has to be kill later after calling poll()
140 LOG.exception("Command %r timeout expired.", command)
141 stdout = stderr = None
142 else:
143 stdout, stderr = process.communicate()
144
145 # Check process termination status
146 exit_status = process.poll()
147 if exit_status is None:
148 # The process is still running after calling communicate():
149 # let kill it and then read buffers again
150 process.kill()
151 stdout, stderr = process.communicate()
152
153 return ShellExecuteResult(command=command, timeout=timeout,
154 stdout=stdout, stderr=stderr,
155 exit_status=exit_status)
156
157
158class ShellExecuteResult(collections.namedtuple(
159 'ShellExecuteResult', ['command', 'timeout', 'exit_status', 'stdout',
160 'stderr'])):
161
162 def check(self):
163 if self.exit_status is None:
164 raise exceptions.ShellTimeoutExpired(command=self.command,
165 timeout=self.timeout,
166 stderr=self.stderr,
167 stdout=self.stdout)
168
169 elif self.exit_status != 0:
Slawek Kaplonski86620da2020-02-06 10:41:36 +0100170 raise exceptions.ShellCommandFailed(command=self.command,
171 exit_status=self.exit_status,
172 stderr=self.stderr,
173 stdout=self.stdout)