blob: 657c0c143b455b26f13ed96f415627a451f929c6 [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
17import select
18import socket
19import time
20import warnings
21
22from oslo_log import log as logging
23import six
24
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
68 if isinstance(pkey, six.string_types):
69 pkey = paramiko.RSAKey.from_private_key(
70 six.StringIO(str(pkey)))
71 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
78 self._proxy_conn = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -050079
80 def _get_ssh_connection(self, sleep=1.5, backoff=1):
81 """Returns an ssh connection to the specified host."""
82 bsleep = sleep
83 ssh = paramiko.SSHClient()
84 ssh.set_missing_host_key_policy(
85 paramiko.AutoAddPolicy())
86 _start_time = time.time()
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090087 if self.proxy_client is not None:
88 proxy_chan = self._get_proxy_channel()
89 else:
90 proxy_chan = None
Matthew Treinish9e26ca82016-02-23 11:43:20 -050091 if self.pkey is not None:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090092 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050093 " with public key authentication",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090094 self.host, self.port, self.username)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050095 else:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090096 LOG.info("Creating ssh connection to '%s:%d' as '%s'"
Matthew Treinish9e26ca82016-02-23 11:43:20 -050097 " with password %s",
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090098 self.host, self.port, self.username, str(self.password))
Matthew Treinish9e26ca82016-02-23 11:43:20 -050099 attempts = 0
100 while True:
101 try:
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +0900102 ssh.connect(self.host, port=self.port, username=self.username,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500103 password=self.password,
104 look_for_keys=self.look_for_keys,
105 key_filename=self.key_filename,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900106 timeout=self.channel_timeout, pkey=self.pkey,
107 sock=proxy_chan)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500108 LOG.info("ssh connection to %s@%s successfully created",
109 self.username, self.host)
110 return ssh
111 except (EOFError,
Eugene Bagdasaryane56dd322016-06-03 14:47:04 +0300112 socket.error, socket.timeout,
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500113 paramiko.SSHException) as e:
Matthew Treinishd4e041d2017-03-01 09:58:57 -0500114 ssh.close()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500115 if self._is_timed_out(_start_time):
116 LOG.exception("Failed to establish authenticated ssh"
117 " connection to %s@%s after %d attempts",
118 self.username, self.host, attempts)
119 raise exceptions.SSHTimeout(host=self.host,
120 user=self.username,
121 password=self.password)
122 bsleep += backoff
123 attempts += 1
124 LOG.warning("Failed to establish authenticated ssh"
125 " connection to %s@%s (%s). Number attempts: %s."
126 " Retry after %d seconds.",
127 self.username, self.host, e, attempts, bsleep)
128 time.sleep(bsleep)
129
130 def _is_timed_out(self, start_time):
131 return (time.time() - self.timeout) > start_time
132
133 @staticmethod
134 def _can_system_poll():
135 return hasattr(select, 'poll')
136
137 def exec_command(self, cmd, encoding="utf-8"):
138 """Execute the specified command on the server
139
140 Note that this method is reading whole command outputs to memory, thus
141 shouldn't be used for large outputs.
142
143 :param str cmd: Command to run at remote server.
144 :param str encoding: Encoding for result from paramiko.
145 Result will not be decoded if None.
146 :returns: data read from standard output of the command.
147 :raises: SSHExecCommandFailed if command returns nonzero
148 status. The exception contains command status stderr content.
149 :raises: TimeoutException if cmd doesn't end when timeout expires.
150 """
151 ssh = self._get_ssh_connection()
152 transport = ssh.get_transport()
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100153 with transport.open_session() as channel:
154 channel.fileno() # Register event pipe
155 channel.exec_command(cmd)
156 channel.shutdown_write()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500157
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100158 # If the executing host is linux-based, poll the channel
159 if self._can_system_poll():
160 out_data_chunks = []
161 err_data_chunks = []
162 poll = select.poll()
163 poll.register(channel, select.POLLIN)
164 start_time = time.time()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500165
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100166 while True:
167 ready = poll.poll(self.channel_timeout)
168 if not any(ready):
169 if not self._is_timed_out(start_time):
170 continue
171 raise exceptions.TimeoutException(
172 "Command: '{0}' executed on host '{1}'.".format(
173 cmd, self.host))
174 if not ready[0]: # If there is nothing to read.
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500175 continue
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100176 out_chunk = err_chunk = None
177 if channel.recv_ready():
178 out_chunk = channel.recv(self.buf_size)
179 out_data_chunks += out_chunk,
180 if channel.recv_stderr_ready():
181 err_chunk = channel.recv_stderr(self.buf_size)
182 err_data_chunks += err_chunk,
183 if not err_chunk and not out_chunk:
184 break
185 out_data = b''.join(out_data_chunks)
186 err_data = b''.join(err_data_chunks)
187 # Just read from the channels
188 else:
189 out_file = channel.makefile('rb', self.buf_size)
190 err_file = channel.makefile_stderr('rb', self.buf_size)
191 out_data = out_file.read()
192 err_data = err_file.read()
193 if encoding:
194 out_data = out_data.decode(encoding)
195 err_data = err_data.decode(encoding)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500196
Georgy Dyuldinbce51c52016-08-22 15:28:46 +0300197 exit_status = channel.recv_exit_status()
198
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100199 if 0 != exit_status:
200 raise exceptions.SSHExecCommandFailed(
201 command=cmd, exit_status=exit_status,
202 stderr=err_data, stdout=out_data)
203 return out_data
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500204
205 def test_connection_auth(self):
206 """Raises an exception when we can not connect to server via ssh."""
207 connection = self._get_ssh_connection()
208 connection.close()
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900209
210 def _get_proxy_channel(self):
211 conn = self.proxy_client._get_ssh_connection()
212 # Keep a reference to avoid g/c
213 # https://github.com/paramiko/paramiko/issues/440
214 self._proxy_conn = conn
215 transport = conn.get_transport()
216 chan = transport.open_session()
217 cmd = 'nc %s %s' % (self.host, self.port)
218 chan.exec_command(cmd)
219 return chan