blob: eb03faa369ac9a76359b514a67a626b27048409b [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
Matthew Treinish9e26ca82016-02-23 11:43:20 -050024
25from tempest.lib import exceptions
26
27
28with warnings.catch_warnings():
29 warnings.simplefilter("ignore")
30 import paramiko
31
32
33LOG = logging.getLogger(__name__)
34
35
36class Client(object):
37
38 def __init__(self, host, username, password=None, timeout=300, pkey=None,
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090039 channel_timeout=10, look_for_keys=False, key_filename=None,
Ade Lee6ded0702021-09-04 15:56:34 -040040 port=22, proxy_client=None, ssh_key_type='rsa'):
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090041 """SSH client.
42
43 Many of parameters are just passed to the underlying implementation
44 as it is. See the paramiko documentation for more details.
45 http://docs.paramiko.org/en/2.1/api/client.html#paramiko.client.SSHClient.connect
46
47 :param host: Host to login.
48 :param username: SSH username.
49 :param password: SSH password, or a password to unlock private key.
50 :param timeout: Timeout in seconds, including retries.
51 Default is 300 seconds.
52 :param pkey: Private key.
53 :param channel_timeout: Channel timeout in seconds, passed to the
54 paramiko. Default is 10 seconds.
55 :param look_for_keys: Whether or not to search for private keys
56 in ``~/.ssh``. Default is False.
57 :param key_filename: Filename for private key to use.
58 :param port: SSH port number.
59 :param proxy_client: Another SSH client to provide a transport
60 for ssh-over-ssh. The default is None, which means
61 not to use ssh-over-ssh.
Ade Lee6ded0702021-09-04 15:56:34 -040062 :param ssh_key_type: ssh key type (rsa, ecdsa)
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090063 :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
songwenpinga6ee2d12021-02-22 10:24:16 +080069 if isinstance(pkey, str):
Ade Lee6ded0702021-09-04 15:56:34 -040070 if ssh_key_type == 'rsa':
71 pkey = paramiko.RSAKey.from_private_key(
72 io.StringIO(str(pkey)))
73 elif ssh_key_type == 'ecdsa':
74 pkey = paramiko.ECDSAKey.from_private_key(
75 io.StringIO(str(pkey)))
76 else:
77 raise exceptions.SSHClientUnsupportedKeyType(
78 key_type=ssh_key_type)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050079 self.pkey = pkey
80 self.look_for_keys = look_for_keys
81 self.key_filename = key_filename
82 self.timeout = int(timeout)
83 self.channel_timeout = float(channel_timeout)
84 self.buf_size = 1024
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090085 self.proxy_client = proxy_client
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +000086 if (self.proxy_client and self.proxy_client.host == self.host and
87 self.proxy_client.port == self.port and
88 self.proxy_client.username == self.username):
89 raise exceptions.SSHClientProxyClientLoop(
90 host=self.host, port=self.port, username=self.username)
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090091 self._proxy_conn = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -050092
93 def _get_ssh_connection(self, sleep=1.5, backoff=1):
94 """Returns an ssh connection to the specified host."""
95 bsleep = sleep
96 ssh = paramiko.SSHClient()
97 ssh.set_missing_host_key_policy(
98 paramiko.AutoAddPolicy())
99 _start_time = time.time()
100 if self.pkey is not None:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900101 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500102 " with public key authentication",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900103 self.host, self.port, self.username)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500104 else:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900105 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500106 " with password %s",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900107 self.host, self.port, self.username, str(self.password))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500108 attempts = 0
109 while True:
YAMAMOTO Takashi2c0ae152017-05-30 20:53:50 +0900110 if self.proxy_client is not None:
111 proxy_chan = self._get_proxy_channel()
112 else:
113 proxy_chan = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500114 try:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900115 ssh.connect(self.host, port=self.port, username=self.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500116 password=self.password,
117 look_for_keys=self.look_for_keys,
118 key_filename=self.key_filename,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900119 timeout=self.channel_timeout, pkey=self.pkey,
120 sock=proxy_chan)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500121 LOG.info("ssh connection to %s@%s successfully created",
122 self.username, self.host)
123 return ssh
124 except (EOFError,
Eugene Bagdasaryane56dd322016-06-03 14:47:04 +0300125 socket.error, socket.timeout,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500126 paramiko.SSHException) as e:
Matthew Treinishd4e041d2017-03-01 09:58:57 -0500127 ssh.close()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500128 if self._is_timed_out(_start_time):
129 LOG.exception("Failed to establish authenticated ssh"
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000130 " connection to %s@%s after %d attempts. "
131 "Proxy client: %s",
132 self.username, self.host, attempts,
133 self._get_proxy_client_info())
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500134 raise exceptions.SSHTimeout(host=self.host,
135 user=self.username,
136 password=self.password)
137 bsleep += backoff
138 attempts += 1
139 LOG.warning("Failed to establish authenticated ssh"
140 " connection to %s@%s (%s). Number attempts: %s."
141 " Retry after %d seconds.",
142 self.username, self.host, e, attempts, bsleep)
143 time.sleep(bsleep)
144
145 def _is_timed_out(self, start_time):
146 return (time.time() - self.timeout) > start_time
147
148 @staticmethod
149 def _can_system_poll():
150 return hasattr(select, 'poll')
151
152 def exec_command(self, cmd, encoding="utf-8"):
153 """Execute the specified command on the server
154
155 Note that this method is reading whole command outputs to memory, thus
156 shouldn't be used for large outputs.
157
158 :param str cmd: Command to run at remote server.
159 :param str encoding: Encoding for result from paramiko.
160 Result will not be decoded if None.
161 :returns: data read from standard output of the command.
162 :raises: SSHExecCommandFailed if command returns nonzero
163 status. The exception contains command status stderr content.
164 :raises: TimeoutException if cmd doesn't end when timeout expires.
165 """
166 ssh = self._get_ssh_connection()
167 transport = ssh.get_transport()
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100168 with transport.open_session() as channel:
169 channel.fileno() # Register event pipe
170 channel.exec_command(cmd)
171 channel.shutdown_write()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500172
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100173 # If the executing host is linux-based, poll the channel
174 if self._can_system_poll():
175 out_data_chunks = []
176 err_data_chunks = []
177 poll = select.poll()
178 poll.register(channel, select.POLLIN)
179 start_time = time.time()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500180
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100181 while True:
182 ready = poll.poll(self.channel_timeout)
183 if not any(ready):
184 if not self._is_timed_out(start_time):
185 continue
186 raise exceptions.TimeoutException(
187 "Command: '{0}' executed on host '{1}'.".format(
188 cmd, self.host))
189 if not ready[0]: # If there is nothing to read.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500190 continue
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100191 out_chunk = err_chunk = None
192 if channel.recv_ready():
193 out_chunk = channel.recv(self.buf_size)
194 out_data_chunks += out_chunk,
195 if channel.recv_stderr_ready():
196 err_chunk = channel.recv_stderr(self.buf_size)
197 err_data_chunks += err_chunk,
198 if not err_chunk and not out_chunk:
199 break
200 out_data = b''.join(out_data_chunks)
201 err_data = b''.join(err_data_chunks)
202 # Just read from the channels
203 else:
204 out_file = channel.makefile('rb', self.buf_size)
205 err_file = channel.makefile_stderr('rb', self.buf_size)
206 out_data = out_file.read()
207 err_data = err_file.read()
208 if encoding:
209 out_data = out_data.decode(encoding)
210 err_data = err_data.decode(encoding)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500211
Georgy Dyuldinbce51c52016-08-22 15:28:46 +0300212 exit_status = channel.recv_exit_status()
213
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100214 ssh.close()
215
216 if 0 != exit_status:
217 raise exceptions.SSHExecCommandFailed(
218 command=cmd, exit_status=exit_status,
219 stderr=err_data, stdout=out_data)
220 return out_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500221
222 def test_connection_auth(self):
223 """Raises an exception when we can not connect to server via ssh."""
224 connection = self._get_ssh_connection()
225 connection.close()
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900226
227 def _get_proxy_channel(self):
228 conn = self.proxy_client._get_ssh_connection()
229 # Keep a reference to avoid g/c
230 # https://github.com/paramiko/paramiko/issues/440
231 self._proxy_conn = conn
232 transport = conn.get_transport()
233 chan = transport.open_session()
234 cmd = 'nc %s %s' % (self.host, self.port)
235 chan.exec_command(cmd)
236 return chan
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000237
238 def _get_proxy_client_info(self):
239 if not self.proxy_client:
240 return 'no proxy client'
241 nested_pclient = self.proxy_client._get_proxy_client_info()
242 return ('%(username)s@%(host)s:%(port)s, nested proxy client: '
243 '%(nested_pclient)s' % {'username': self.proxy_client.username,
244 'host': self.proxy_client.host,
245 'port': self.proxy_client.port,
246 'nested_pclient': nested_pclient})