blob: 60107d7761681aebf1ee54772d81281de015ae0a [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2012 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16
songwenping0fa20692021-01-05 06:30:18 +000017import io
Matthew Treinish9e26ca82016-02-23 11:43:20 -050018import select
19import socket
20import time
21import warnings
22
23from oslo_log import log as logging
24import six
25
26from tempest.lib import exceptions
27
28
29with warnings.catch_warnings():
30 warnings.simplefilter("ignore")
31 import paramiko
32
33
34LOG = logging.getLogger(__name__)
35
36
37class Client(object):
38
39 def __init__(self, host, username, password=None, timeout=300, pkey=None,
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090040 channel_timeout=10, look_for_keys=False, key_filename=None,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090041 port=22, proxy_client=None):
42 """SSH client.
43
44 Many of parameters are just passed to the underlying implementation
45 as it is. See the paramiko documentation for more details.
46 http://docs.paramiko.org/en/2.1/api/client.html#paramiko.client.SSHClient.connect
47
48 :param host: Host to login.
49 :param username: SSH username.
50 :param password: SSH password, or a password to unlock private key.
51 :param timeout: Timeout in seconds, including retries.
52 Default is 300 seconds.
53 :param pkey: Private key.
54 :param channel_timeout: Channel timeout in seconds, passed to the
55 paramiko. Default is 10 seconds.
56 :param look_for_keys: Whether or not to search for private keys
57 in ``~/.ssh``. Default is False.
58 :param key_filename: Filename for private key to use.
59 :param port: SSH port number.
60 :param proxy_client: Another SSH client to provide a transport
61 for ssh-over-ssh. The default is None, which means
62 not to use ssh-over-ssh.
63 :type proxy_client: ``tempest.lib.common.ssh.Client`` object
64 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050065 self.host = host
66 self.username = username
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090067 self.port = port
Matthew Treinish9e26ca82016-02-23 11:43:20 -050068 self.password = password
69 if isinstance(pkey, six.string_types):
70 pkey = paramiko.RSAKey.from_private_key(
songwenping0fa20692021-01-05 06:30:18 +000071 io.StringIO(str(pkey)))
Matthew Treinish9e26ca82016-02-23 11:43:20 -050072 self.pkey = pkey
73 self.look_for_keys = look_for_keys
74 self.key_filename = key_filename
75 self.timeout = int(timeout)
76 self.channel_timeout = float(channel_timeout)
77 self.buf_size = 1024
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090078 self.proxy_client = proxy_client
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +000079 if (self.proxy_client and self.proxy_client.host == self.host and
80 self.proxy_client.port == self.port and
81 self.proxy_client.username == self.username):
82 raise exceptions.SSHClientProxyClientLoop(
83 host=self.host, port=self.port, username=self.username)
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090084 self._proxy_conn = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -050085
86 def _get_ssh_connection(self, sleep=1.5, backoff=1):
87 """Returns an ssh connection to the specified host."""
88 bsleep = sleep
89 ssh = paramiko.SSHClient()
90 ssh.set_missing_host_key_policy(
91 paramiko.AutoAddPolicy())
92 _start_time = time.time()
93 if self.pkey is not None:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090094 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050095 " with public key authentication",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090096 self.host, self.port, self.username)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050097 else:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090098 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050099 " with password %s",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900100 self.host, self.port, self.username, str(self.password))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500101 attempts = 0
102 while True:
YAMAMOTO Takashi2c0ae152017-05-30 20:53:50 +0900103 if self.proxy_client is not None:
104 proxy_chan = self._get_proxy_channel()
105 else:
106 proxy_chan = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500107 try:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900108 ssh.connect(self.host, port=self.port, username=self.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500109 password=self.password,
110 look_for_keys=self.look_for_keys,
111 key_filename=self.key_filename,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900112 timeout=self.channel_timeout, pkey=self.pkey,
113 sock=proxy_chan)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500114 LOG.info("ssh connection to %s@%s successfully created",
115 self.username, self.host)
116 return ssh
117 except (EOFError,
Eugene Bagdasaryane56dd322016-06-03 14:47:04 +0300118 socket.error, socket.timeout,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500119 paramiko.SSHException) as e:
Matthew Treinishd4e041d2017-03-01 09:58:57 -0500120 ssh.close()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500121 if self._is_timed_out(_start_time):
122 LOG.exception("Failed to establish authenticated ssh"
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000123 " connection to %s@%s after %d attempts. "
124 "Proxy client: %s",
125 self.username, self.host, attempts,
126 self._get_proxy_client_info())
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500127 raise exceptions.SSHTimeout(host=self.host,
128 user=self.username,
129 password=self.password)
130 bsleep += backoff
131 attempts += 1
132 LOG.warning("Failed to establish authenticated ssh"
133 " connection to %s@%s (%s). Number attempts: %s."
134 " Retry after %d seconds.",
135 self.username, self.host, e, attempts, bsleep)
136 time.sleep(bsleep)
137
138 def _is_timed_out(self, start_time):
139 return (time.time() - self.timeout) > start_time
140
141 @staticmethod
142 def _can_system_poll():
143 return hasattr(select, 'poll')
144
145 def exec_command(self, cmd, encoding="utf-8"):
146 """Execute the specified command on the server
147
148 Note that this method is reading whole command outputs to memory, thus
149 shouldn't be used for large outputs.
150
151 :param str cmd: Command to run at remote server.
152 :param str encoding: Encoding for result from paramiko.
153 Result will not be decoded if None.
154 :returns: data read from standard output of the command.
155 :raises: SSHExecCommandFailed if command returns nonzero
156 status. The exception contains command status stderr content.
157 :raises: TimeoutException if cmd doesn't end when timeout expires.
158 """
159 ssh = self._get_ssh_connection()
160 transport = ssh.get_transport()
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100161 with transport.open_session() as channel:
162 channel.fileno() # Register event pipe
163 channel.exec_command(cmd)
164 channel.shutdown_write()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500165
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100166 # If the executing host is linux-based, poll the channel
167 if self._can_system_poll():
168 out_data_chunks = []
169 err_data_chunks = []
170 poll = select.poll()
171 poll.register(channel, select.POLLIN)
172 start_time = time.time()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500173
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100174 while True:
175 ready = poll.poll(self.channel_timeout)
176 if not any(ready):
177 if not self._is_timed_out(start_time):
178 continue
179 raise exceptions.TimeoutException(
180 "Command: '{0}' executed on host '{1}'.".format(
181 cmd, self.host))
182 if not ready[0]: # If there is nothing to read.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500183 continue
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100184 out_chunk = err_chunk = None
185 if channel.recv_ready():
186 out_chunk = channel.recv(self.buf_size)
187 out_data_chunks += out_chunk,
188 if channel.recv_stderr_ready():
189 err_chunk = channel.recv_stderr(self.buf_size)
190 err_data_chunks += err_chunk,
191 if not err_chunk and not out_chunk:
192 break
193 out_data = b''.join(out_data_chunks)
194 err_data = b''.join(err_data_chunks)
195 # Just read from the channels
196 else:
197 out_file = channel.makefile('rb', self.buf_size)
198 err_file = channel.makefile_stderr('rb', self.buf_size)
199 out_data = out_file.read()
200 err_data = err_file.read()
201 if encoding:
202 out_data = out_data.decode(encoding)
203 err_data = err_data.decode(encoding)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500204
Georgy Dyuldinbce51c52016-08-22 15:28:46 +0300205 exit_status = channel.recv_exit_status()
206
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100207 ssh.close()
208
209 if 0 != exit_status:
210 raise exceptions.SSHExecCommandFailed(
211 command=cmd, exit_status=exit_status,
212 stderr=err_data, stdout=out_data)
213 return out_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500214
215 def test_connection_auth(self):
216 """Raises an exception when we can not connect to server via ssh."""
217 connection = self._get_ssh_connection()
218 connection.close()
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900219
220 def _get_proxy_channel(self):
221 conn = self.proxy_client._get_ssh_connection()
222 # Keep a reference to avoid g/c
223 # https://github.com/paramiko/paramiko/issues/440
224 self._proxy_conn = conn
225 transport = conn.get_transport()
226 chan = transport.open_session()
227 cmd = 'nc %s %s' % (self.host, self.port)
228 chan.exec_command(cmd)
229 return chan
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000230
231 def _get_proxy_client_info(self):
232 if not self.proxy_client:
233 return 'no proxy client'
234 nested_pclient = self.proxy_client._get_proxy_client_info()
235 return ('%(username)s@%(host)s:%(port)s, nested proxy client: '
236 '%(nested_pclient)s' % {'username': self.proxy_client.username,
237 'host': self.proxy_client.host,
238 'port': self.proxy_client.port,
239 'nested_pclient': nested_pclient})