blob: 0ba6ed31d3c2cec2990c2da021463e1d3a2a665a [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2014 OpenStack Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
songwenping0fa20692021-01-05 06:30:18 +000015from io import StringIO
Matthew Treinish9e26ca82016-02-23 11:43:20 -050016import socket
Sean McGinniseed80742020-04-18 12:01:03 -050017from unittest import mock
Matthew Treinish9e26ca82016-02-23 11:43:20 -050018
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import testtools
20
21from tempest.lib.common import ssh
22from tempest.lib import exceptions
Matthew Treinishffad78a2016-04-16 14:39:52 -040023from tempest.tests import base
Jordan Pittier0e53b612016-03-03 14:23:17 +010024import tempest.tests.utils as utils
Matthew Treinish9e26ca82016-02-23 11:43:20 -050025
26
27class TestSshClient(base.TestCase):
28
29 SELECT_POLLIN = 1
30
31 @mock.patch('paramiko.RSAKey.from_private_key')
songwenping0fa20692021-01-05 06:30:18 +000032 @mock.patch('io.StringIO')
Matthew Treinish9e26ca82016-02-23 11:43:20 -050033 def test_pkey_calls_paramiko_RSAKey(self, cs_mock, rsa_mock):
34 cs_mock.return_value = mock.sentinel.csio
35 pkey = 'mykey'
36 ssh.Client('localhost', 'root', pkey=pkey)
37 rsa_mock.assert_called_once_with(mock.sentinel.csio)
38 cs_mock.assert_called_once_with('mykey')
39 rsa_mock.reset_mock()
40 cs_mock.reset_mock()
41 pkey = mock.sentinel.pkey
42 # Shouldn't call out to load a file from RSAKey, since
43 # a sentinel isn't a basestring...
44 ssh.Client('localhost', 'root', pkey=pkey)
45 self.assertEqual(0, rsa_mock.call_count)
46 self.assertEqual(0, cs_mock.call_count)
47
48 def _set_ssh_connection_mocks(self):
49 client_mock = mock.MagicMock()
50 client_mock.connect.return_value = True
51 return (self.patch('paramiko.SSHClient'),
52 self.patch('paramiko.AutoAddPolicy'),
53 client_mock)
54
55 def test_get_ssh_connection(self):
56 c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
57 s_mock = self.patch('time.sleep')
58
59 c_mock.return_value = client_mock
60 aa_mock.return_value = mock.sentinel.aa
61
62 # Test normal case for successful connection on first try
63 client = ssh.Client('localhost', 'root', timeout=2)
64 client._get_ssh_connection(sleep=1)
65
66 aa_mock.assert_called_once_with()
67 client_mock.set_missing_host_key_policy.assert_called_once_with(
68 mock.sentinel.aa)
69 expected_connect = [mock.call(
70 'localhost',
Masayuki Igawa55b4cfd2016-08-30 10:29:46 +090071 port=22,
Matthew Treinish9e26ca82016-02-23 11:43:20 -050072 username='root',
73 pkey=None,
74 key_filename=None,
75 look_for_keys=False,
76 timeout=10.0,
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090077 password=None,
Julia Kregera6614d32023-02-02 08:24:51 -080078 sock=None,
79 allow_agent=True
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090080 )]
81 self.assertEqual(expected_connect, client_mock.connect.mock_calls)
82 self.assertEqual(0, s_mock.call_count)
83
84 def test_get_ssh_connection_over_ssh(self):
85 c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
86 proxy_client_mock = mock.MagicMock()
87 proxy_client_mock.connect.return_value = True
88 s_mock = self.patch('time.sleep')
89
90 c_mock.side_effect = [client_mock, proxy_client_mock]
91 aa_mock.return_value = mock.sentinel.aa
92
93 proxy_client = ssh.Client('proxy-host', 'proxy-user', timeout=2)
94 client = ssh.Client('localhost', 'root', timeout=2,
Julia Kregera6614d32023-02-02 08:24:51 -080095 proxy_client=proxy_client,
96 ssh_allow_agent=False)
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +090097 client._get_ssh_connection(sleep=1)
98
99 aa_mock.assert_has_calls([mock.call(), mock.call()])
100 proxy_client_mock.set_missing_host_key_policy.assert_called_once_with(
101 mock.sentinel.aa)
102 proxy_expected_connect = [mock.call(
103 'proxy-host',
104 port=22,
105 username='proxy-user',
106 pkey=None,
107 key_filename=None,
108 look_for_keys=False,
109 timeout=10.0,
110 password=None,
Julia Kregera6614d32023-02-02 08:24:51 -0800111 sock=None,
112 allow_agent=True
YAMAMOTO Takashiae015d12017-01-25 11:36:23 +0900113 )]
114 self.assertEqual(proxy_expected_connect,
115 proxy_client_mock.connect.mock_calls)
116 client_mock.set_missing_host_key_policy.assert_called_once_with(
117 mock.sentinel.aa)
118 expected_connect = [mock.call(
119 'localhost',
120 port=22,
121 username='root',
122 pkey=None,
123 key_filename=None,
124 look_for_keys=False,
125 timeout=10.0,
126 password=None,
Julia Kregera6614d32023-02-02 08:24:51 -0800127 sock=proxy_client_mock.get_transport().open_session(),
128 allow_agent=False
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500129 )]
130 self.assertEqual(expected_connect, client_mock.connect.mock_calls)
131 self.assertEqual(0, s_mock.call_count)
132
Jordan Pittier0e53b612016-03-03 14:23:17 +0100133 @mock.patch('time.sleep')
134 def test_get_ssh_connection_two_attemps(self, sleep_mock):
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500135 c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
136
137 c_mock.return_value = client_mock
138 client_mock.connect.side_effect = [
139 socket.error,
140 mock.MagicMock()
141 ]
142
143 client = ssh.Client('localhost', 'root', timeout=1)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500144 client._get_ssh_connection(sleep=1)
Jordan Pittier0e53b612016-03-03 14:23:17 +0100145 # We slept 2 seconds: because sleep is "1" and backoff is "1" too
146 sleep_mock.assert_called_once_with(2)
147 self.assertEqual(2, client_mock.connect.call_count)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500148
149 def test_get_ssh_connection_timeout(self):
150 c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
151
Jordan Pittier0e53b612016-03-03 14:23:17 +0100152 timeout = 2
153 time_mock = self.patch('time.time')
154 time_mock.side_effect = utils.generate_timeout_series(timeout + 1)
155
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500156 c_mock.return_value = client_mock
157 client_mock.connect.side_effect = [
158 socket.error,
159 socket.error,
160 socket.error,
161 ]
162
Jordan Pittier0e53b612016-03-03 14:23:17 +0100163 client = ssh.Client('localhost', 'root', timeout=timeout)
164 # We need to mock LOG here because LOG.info() calls time.time()
Rajesh Tailora85bdb42024-04-02 12:01:53 +0530165 # in order to prepend a timestamp.
Jordan Pittier0e53b612016-03-03 14:23:17 +0100166 with mock.patch.object(ssh, 'LOG'):
167 self.assertRaises(exceptions.SSHTimeout,
168 client._get_ssh_connection)
169
170 # time.time() should be called twice, first to start the timer
171 # and then to compute the timedelta
172 self.assertEqual(2, time_mock.call_count)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500173
174 @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
175 def test_timeout_in_exec_command(self):
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100176 chan_mock, poll_mock, _, _ = (
177 self._set_mocks_for_select([0, 0, 0], True))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500178
179 # Test for a timeout condition immediately raised
180 client = ssh.Client('localhost', 'root', timeout=2)
181 with testtools.ExpectedException(exceptions.TimeoutException):
182 client.exec_command("test")
183
184 chan_mock.fileno.assert_called_once_with()
185 chan_mock.exec_command.assert_called_once_with("test")
186 chan_mock.shutdown_write.assert_called_once_with()
187
188 poll_mock.register.assert_called_once_with(
189 chan_mock, self.SELECT_POLLIN)
190 poll_mock.poll.assert_called_once_with(10)
191
192 @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
193 def test_exec_command(self):
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100194 chan_mock, poll_mock, select_mock, client_mock = (
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500195 self._set_mocks_for_select([[1, 0, 0]], True))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500196
197 chan_mock.recv_exit_status.return_value = 0
198 chan_mock.recv.return_value = b''
199 chan_mock.recv_stderr.return_value = b''
200
201 client = ssh.Client('localhost', 'root', timeout=2)
202 client.exec_command("test")
203
204 chan_mock.fileno.assert_called_once_with()
205 chan_mock.exec_command.assert_called_once_with("test")
206 chan_mock.shutdown_write.assert_called_once_with()
207
208 select_mock.assert_called_once_with()
209 poll_mock.register.assert_called_once_with(
210 chan_mock, self.SELECT_POLLIN)
211 poll_mock.poll.assert_called_once_with(10)
212 chan_mock.recv_ready.assert_called_once_with()
213 chan_mock.recv.assert_called_once_with(1024)
214 chan_mock.recv_stderr_ready.assert_called_once_with()
215 chan_mock.recv_stderr.assert_called_once_with(1024)
216 chan_mock.recv_exit_status.assert_called_once_with()
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500217
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100218 client_mock.close.assert_called_once_with()
219
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500220 def _set_mocks_for_select(self, poll_data, ito_value=False):
221 gsc_mock = self.patch('tempest.lib.common.ssh.Client.'
222 '_get_ssh_connection')
223 ito_mock = self.patch('tempest.lib.common.ssh.Client._is_timed_out')
224 csp_mock = self.patch(
225 'tempest.lib.common.ssh.Client._can_system_poll')
226 csp_mock.return_value = True
227
228 select_mock = self.patch('select.poll', create=True)
229 client_mock = mock.MagicMock()
230 tran_mock = mock.MagicMock()
231 chan_mock = mock.MagicMock()
232 poll_mock = mock.MagicMock()
233
234 select_mock.return_value = poll_mock
235 gsc_mock.return_value = client_mock
236 ito_mock.return_value = ito_value
237 client_mock.get_transport.return_value = tran_mock
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100238 tran_mock.open_session().__enter__.return_value = chan_mock
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500239 if isinstance(poll_data[0], list):
240 poll_mock.poll.side_effect = poll_data
241 else:
242 poll_mock.poll.return_value = poll_data
243
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100244 return chan_mock, poll_mock, select_mock, client_mock
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500245
songwenpinga6ee2d12021-02-22 10:24:16 +0800246 _utf8_string = chr(1071)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500247 _utf8_bytes = _utf8_string.encode("utf-8")
248
249 @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
250 def test_exec_good_command_output(self):
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100251 chan_mock, poll_mock, _, _ = (
252 self._set_mocks_for_select([1, 0, 0]))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500253 closed_prop = mock.PropertyMock(return_value=True)
254 type(chan_mock).closed = closed_prop
255
256 chan_mock.recv_exit_status.return_value = 0
257 chan_mock.recv.side_effect = [self._utf8_bytes[0:1],
258 self._utf8_bytes[1:], b'R', b'']
259 chan_mock.recv_stderr.return_value = b''
260
261 client = ssh.Client('localhost', 'root', timeout=2)
262 out_data = client.exec_command("test")
263 self.assertEqual(self._utf8_string + 'R', out_data)
264
265 @mock.patch('select.POLLIN', SELECT_POLLIN, create=True)
266 def test_exec_bad_command_output(self):
Gregory Thiemonge690bae22019-11-20 11:33:56 +0100267 chan_mock, poll_mock, _, _ = (
268 self._set_mocks_for_select([1, 0, 0]))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500269 closed_prop = mock.PropertyMock(return_value=True)
270 type(chan_mock).closed = closed_prop
271
272 chan_mock.recv_exit_status.return_value = 1
273 chan_mock.recv.return_value = b''
274 chan_mock.recv_stderr.side_effect = [b'R', self._utf8_bytes[0:1],
275 self._utf8_bytes[1:], b'']
276
277 client = ssh.Client('localhost', 'root', timeout=2)
278 exc = self.assertRaises(exceptions.SSHExecCommandFailed,
279 client.exec_command, "test")
likui19b70a32020-12-02 13:25:18 +0800280 self.assertIn('R' + self._utf8_string, str(exc))
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500281
282 def test_exec_command_no_select(self):
283 gsc_mock = self.patch('tempest.lib.common.ssh.Client.'
284 '_get_ssh_connection')
285 csp_mock = self.patch(
286 'tempest.lib.common.ssh.Client._can_system_poll')
287 csp_mock.return_value = False
288
289 select_mock = self.patch('select.poll', create=True)
290 client_mock = mock.MagicMock()
291 tran_mock = mock.MagicMock()
292 chan_mock = mock.MagicMock()
293
294 # Test for proper reading of STDOUT and STDERROR
295
296 gsc_mock.return_value = client_mock
297 client_mock.get_transport.return_value = tran_mock
Lucas Alvares Gomes68c197e2016-04-19 18:18:05 +0100298 tran_mock.open_session().__enter__.return_value = chan_mock
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500299 chan_mock.recv_exit_status.return_value = 0
300
301 std_out_mock = mock.MagicMock(StringIO)
302 std_err_mock = mock.MagicMock(StringIO)
303 chan_mock.makefile.return_value = std_out_mock
304 chan_mock.makefile_stderr.return_value = std_err_mock
305
306 client = ssh.Client('localhost', 'root', timeout=2)
307 client.exec_command("test")
308
309 chan_mock.makefile.assert_called_once_with('rb', 1024)
310 chan_mock.makefile_stderr.assert_called_once_with('rb', 1024)
311 std_out_mock.read.assert_called_once_with()
312 std_err_mock.read.assert_called_once_with()
313 self.assertFalse(select_mock.called)