blob: ee1537542bddd659fe00aca816229d026b451e7b [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,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090040 port=22, proxy_client=None):
41 """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.
62 :type proxy_client: ``tempest.lib.common.ssh.Client`` object
63 """
Matthew Treinish9e26ca82016-02-23 11:43:20 -050064 self.host = host
65 self.username = username
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090066 self.port = port
Matthew Treinish9e26ca82016-02-23 11:43:20 -050067 self.password = password
songwenpinga6ee2d12021-02-22 10:24:16 +080068 if isinstance(pkey, str):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050069 pkey = paramiko.RSAKey.from_private_key(
songwenping0fa20692021-01-05 06:30:18 +000070 io.StringIO(str(pkey)))
Matthew Treinish9e26ca82016-02-23 11:43:20 -050071 self.pkey = pkey
72 self.look_for_keys = look_for_keys
73 self.key_filename = key_filename
74 self.timeout = int(timeout)
75 self.channel_timeout = float(channel_timeout)
76 self.buf_size = 1024
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090077 self.proxy_client = proxy_client
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +000078 if (self.proxy_client and self.proxy_client.host == self.host and
79 self.proxy_client.port == self.port and
80 self.proxy_client.username == self.username):
81 raise exceptions.SSHClientProxyClientLoop(
82 host=self.host, port=self.port, username=self.username)
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090083 self._proxy_conn = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -050084
85 def _get_ssh_connection(self, sleep=1.5, backoff=1):
86 """Returns an ssh connection to the specified host."""
87 bsleep = sleep
88 ssh = paramiko.SSHClient()
89 ssh.set_missing_host_key_policy(
90 paramiko.AutoAddPolicy())
91 _start_time = time.time()
92 if self.pkey is not None:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090093 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050094 " with public key authentication",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090095 self.host, self.port, self.username)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050096 else:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090097 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050098 " with password %s",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090099 self.host, self.port, self.username, str(self.password))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500100 attempts = 0
101 while True:
YAMAMOTO Takashi2c0ae152017-05-30 20:53:50 +0900102 if self.proxy_client is not None:
103 proxy_chan = self._get_proxy_channel()
104 else:
105 proxy_chan = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500106 try:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900107 ssh.connect(self.host, port=self.port, username=self.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500108 password=self.password,
109 look_for_keys=self.look_for_keys,
110 key_filename=self.key_filename,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900111 timeout=self.channel_timeout, pkey=self.pkey,
112 sock=proxy_chan)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500113 LOG.info("ssh connection to %s@%s successfully created",
114 self.username, self.host)
115 return ssh
116 except (EOFError,
Eugene Bagdasaryane56dd322016-06-03 14:47:04 +0300117 socket.error, socket.timeout,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500118 paramiko.SSHException) as e:
Matthew Treinishd4e041d2017-03-01 09:58:57 -0500119 ssh.close()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500120 if self._is_timed_out(_start_time):
121 LOG.exception("Failed to establish authenticated ssh"
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000122 " connection to %s@%s after %d attempts. "
123 "Proxy client: %s",
124 self.username, self.host, attempts,
125 self._get_proxy_client_info())
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500126 raise exceptions.SSHTimeout(host=self.host,
127 user=self.username,
128 password=self.password)
129 bsleep += backoff
130 attempts += 1
131 LOG.warning("Failed to establish authenticated ssh"
132 " connection to %s@%s (%s). Number attempts: %s."
133 " Retry after %d seconds.",
134 self.username, self.host, e, attempts, bsleep)
135 time.sleep(bsleep)
136
137 def _is_timed_out(self, start_time):
138 return (time.time() - self.timeout) > start_time
139
140 @staticmethod
141 def _can_system_poll():
142 return hasattr(select, 'poll')
143
144 def exec_command(self, cmd, encoding="utf-8"):
145 """Execute the specified command on the server
146
147 Note that this method is reading whole command outputs to memory, thus
148 shouldn't be used for large outputs.
149
150 :param str cmd: Command to run at remote server.
151 :param str encoding: Encoding for result from paramiko.
152 Result will not be decoded if None.
153 :returns: data read from standard output of the command.
154 :raises: SSHExecCommandFailed if command returns nonzero
155 status. The exception contains command status stderr content.
156 :raises: TimeoutException if cmd doesn't end when timeout expires.
157 """
158 ssh = self._get_ssh_connection()
159 transport = ssh.get_transport()
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100160 with transport.open_session() as channel:
161 channel.fileno() # Register event pipe
162 channel.exec_command(cmd)
163 channel.shutdown_write()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500164
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100165 # If the executing host is linux-based, poll the channel
166 if self._can_system_poll():
167 out_data_chunks = []
168 err_data_chunks = []
169 poll = select.poll()
170 poll.register(channel, select.POLLIN)
171 start_time = time.time()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500172
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100173 while True:
174 ready = poll.poll(self.channel_timeout)
175 if not any(ready):
176 if not self._is_timed_out(start_time):
177 continue
178 raise exceptions.TimeoutException(
179 "Command: '{0}' executed on host '{1}'.".format(
180 cmd, self.host))
181 if not ready[0]: # If there is nothing to read.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500182 continue
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100183 out_chunk = err_chunk = None
184 if channel.recv_ready():
185 out_chunk = channel.recv(self.buf_size)
186 out_data_chunks += out_chunk,
187 if channel.recv_stderr_ready():
188 err_chunk = channel.recv_stderr(self.buf_size)
189 err_data_chunks += err_chunk,
190 if not err_chunk and not out_chunk:
191 break
192 out_data = b''.join(out_data_chunks)
193 err_data = b''.join(err_data_chunks)
194 # Just read from the channels
195 else:
196 out_file = channel.makefile('rb', self.buf_size)
197 err_file = channel.makefile_stderr('rb', self.buf_size)
198 out_data = out_file.read()
199 err_data = err_file.read()
200 if encoding:
201 out_data = out_data.decode(encoding)
202 err_data = err_data.decode(encoding)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500203
Georgy Dyuldinbce51c52016-08-22 15:28:46 +0300204 exit_status = channel.recv_exit_status()
205
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100206 ssh.close()
207
208 if 0 != exit_status:
209 raise exceptions.SSHExecCommandFailed(
210 command=cmd, exit_status=exit_status,
211 stderr=err_data, stdout=out_data)
212 return out_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500213
214 def test_connection_auth(self):
215 """Raises an exception when we can not connect to server via ssh."""
216 connection = self._get_ssh_connection()
217 connection.close()
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900218
219 def _get_proxy_channel(self):
220 conn = self.proxy_client._get_ssh_connection()
221 # Keep a reference to avoid g/c
222 # https://github.com/paramiko/paramiko/issues/440
223 self._proxy_conn = conn
224 transport = conn.get_transport()
225 chan = transport.open_session()
226 cmd = 'nc %s %s' % (self.host, self.port)
227 chan.exec_command(cmd)
228 return chan
Rodolfo Alonso Hernandezbcfa06d2020-01-22 17:29:18 +0000229
230 def _get_proxy_client_info(self):
231 if not self.proxy_client:
232 return 'no proxy client'
233 nested_pclient = self.proxy_client._get_proxy_client_info()
234 return ('%(username)s@%(host)s:%(port)s, nested proxy client: '
235 '%(nested_pclient)s' % {'username': self.proxy_client.username,
236 'host': self.proxy_client.host,
237 'port': self.proxy_client.port,
238 'nested_pclient': nested_pclient})