Merge "standardize ssh auth" into stable/danube
[yardstick.git] / tests / unit / test_ssh.py
1 # Copyright 2013: Mirantis Inc.
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 # yardstick comment: this file is a modified copy of
17 # rally/tests/unit/common/test_sshutils.py
18
19 from __future__ import absolute_import
20 import os
21 import socket
22 import unittest
23 from io import StringIO
24
25 import mock
26 from oslo_utils import encodeutils
27
28 from yardstick import ssh
29
30
31 class FakeParamikoException(Exception):
32     pass
33
34
35 class SSHTestCase(unittest.TestCase):
36     """Test all small SSH methods."""
37
38     def setUp(self):
39         super(SSHTestCase, self).setUp()
40         self.test_client = ssh.SSH("root", "example.net")
41
42     @mock.patch("yardstick.ssh.SSH._get_pkey")
43     def test_construct(self, mock_ssh__get_pkey):
44         mock_ssh__get_pkey.return_value = "pkey"
45         test_ssh = ssh.SSH("root", "example.net", port=33, pkey="key",
46                            key_filename="kf", password="secret")
47         mock_ssh__get_pkey.assert_called_once_with("key")
48         self.assertEqual("root", test_ssh.user)
49         self.assertEqual("example.net", test_ssh.host)
50         self.assertEqual(33, test_ssh.port)
51         self.assertEqual("pkey", test_ssh.pkey)
52         self.assertEqual("kf", test_ssh.key_filename)
53         self.assertEqual("secret", test_ssh.password)
54
55     @mock.patch("yardstick.ssh.SSH._get_pkey")
56     def test_ssh_from_node(self, mock_ssh__get_pkey):
57         mock_ssh__get_pkey.return_value = "pkey"
58         node = {
59             "user": "root", "ip": "example.net", "ssh_port": 33,
60             "key_filename": "kf", "password": "secret"
61         }
62         test_ssh = ssh.SSH.from_node(node)
63         self.assertEqual("root", test_ssh.user)
64         self.assertEqual("example.net", test_ssh.host)
65         self.assertEqual(33, test_ssh.port)
66         self.assertEqual("kf", test_ssh.key_filename)
67         self.assertEqual("secret", test_ssh.password)
68
69     @mock.patch("yardstick.ssh.SSH._get_pkey")
70     def test_ssh_from_node_password_default(self, mock_ssh__get_pkey):
71         mock_ssh__get_pkey.return_value = "pkey"
72         node = {
73             "user": "root", "ip": "example.net", "ssh_port": 33,
74             "key_filename": "kf"
75         }
76         test_ssh = ssh.SSH.from_node(node)
77         self.assertEqual("root", test_ssh.user)
78         self.assertEqual("example.net", test_ssh.host)
79         self.assertEqual(33, test_ssh.port)
80         self.assertEqual("kf", test_ssh.key_filename)
81         self.assertIsNone(test_ssh.password)
82
83     @mock.patch("yardstick.ssh.SSH._get_pkey")
84     def test_ssh_from_node_ssh_port_default(self, mock_ssh__get_pkey):
85         mock_ssh__get_pkey.return_value = "pkey"
86         node = {
87             "user": "root", "ip": "example.net",
88             "key_filename": "kf", "password": "secret"
89         }
90         test_ssh = ssh.SSH.from_node(node)
91         self.assertEqual("root", test_ssh.user)
92         self.assertEqual("example.net", test_ssh.host)
93         self.assertEqual(ssh.SSH_PORT, test_ssh.port)
94         self.assertEqual("kf", test_ssh.key_filename)
95         self.assertEqual("secret", test_ssh.password)
96
97     @mock.patch("yardstick.ssh.SSH._get_pkey")
98     def test_ssh_from_node_key_filename_default(self, mock_ssh__get_pkey):
99         mock_ssh__get_pkey.return_value = "pkey"
100         node = {
101             "user": "root", "ip": "example.net", "ssh_port": 33,
102             "password": "secret"
103         }
104         test_ssh = ssh.SSH.from_node(node)
105         self.assertEqual("root", test_ssh.user)
106         self.assertEqual("example.net", test_ssh.host)
107         self.assertEqual(33, test_ssh.port)
108         self.assertIsNone(test_ssh.key_filename)
109         self.assertEqual("secret", test_ssh.password)
110
111     def test_construct_default(self):
112         self.assertEqual("root", self.test_client.user)
113         self.assertEqual("example.net", self.test_client.host)
114         self.assertEqual(22, self.test_client.port)
115         self.assertIsNone(self.test_client.pkey)
116         self.assertIsNone(self.test_client.key_filename)
117         self.assertIsNone(self.test_client.password)
118
119     @mock.patch("yardstick.ssh.paramiko")
120     def test__get_pkey_invalid(self, mock_paramiko):
121         mock_paramiko.SSHException = FakeParamikoException
122         rsa = mock_paramiko.rsakey.RSAKey
123         dss = mock_paramiko.dsskey.DSSKey
124         rsa.from_private_key.side_effect = mock_paramiko.SSHException
125         dss.from_private_key.side_effect = mock_paramiko.SSHException
126         self.assertRaises(ssh.SSHError, self.test_client._get_pkey, "key")
127
128     @mock.patch("yardstick.ssh.six.moves.StringIO")
129     @mock.patch("yardstick.ssh.paramiko")
130     def test__get_pkey_dss(self, mock_paramiko, mock_string_io):
131         mock_paramiko.SSHException = FakeParamikoException
132         mock_string_io.return_value = "string_key"
133         mock_paramiko.dsskey.DSSKey.from_private_key.return_value = "dss_key"
134         rsa = mock_paramiko.rsakey.RSAKey
135         rsa.from_private_key.side_effect = mock_paramiko.SSHException
136         key = self.test_client._get_pkey("key")
137         dss_calls = mock_paramiko.dsskey.DSSKey.from_private_key.mock_calls
138         self.assertEqual([mock.call("string_key")], dss_calls)
139         self.assertEqual(key, "dss_key")
140         mock_string_io.assert_called_once_with("key")
141
142     @mock.patch("yardstick.ssh.six.moves.StringIO")
143     @mock.patch("yardstick.ssh.paramiko")
144     def test__get_pkey_rsa(self, mock_paramiko, mock_string_io):
145         mock_paramiko.SSHException = FakeParamikoException
146         mock_string_io.return_value = "string_key"
147         mock_paramiko.rsakey.RSAKey.from_private_key.return_value = "rsa_key"
148         dss = mock_paramiko.dsskey.DSSKey
149         dss.from_private_key.side_effect = mock_paramiko.SSHException
150         key = self.test_client._get_pkey("key")
151         rsa_calls = mock_paramiko.rsakey.RSAKey.from_private_key.mock_calls
152         self.assertEqual([mock.call("string_key")], rsa_calls)
153         self.assertEqual(key, "rsa_key")
154         mock_string_io.assert_called_once_with("key")
155
156     @mock.patch("yardstick.ssh.SSH._get_pkey")
157     @mock.patch("yardstick.ssh.paramiko")
158     def test__get_client(self, mock_paramiko, mock_ssh__get_pkey):
159         mock_ssh__get_pkey.return_value = "key"
160         fake_client = mock.Mock()
161         mock_paramiko.SSHClient.return_value = fake_client
162         mock_paramiko.AutoAddPolicy.return_value = "autoadd"
163
164         test_ssh = ssh.SSH("admin", "example.net", pkey="key")
165         client = test_ssh._get_client()
166
167         self.assertEqual(fake_client, client)
168         client_calls = [
169             mock.call.set_missing_host_key_policy("autoadd"),
170             mock.call.connect("example.net", username="admin",
171                               port=22, pkey="key", key_filename=None,
172                               password=None,
173                               allow_agent=False, look_for_keys=False,
174                               timeout=1),
175         ]
176         self.assertEqual(client_calls, client.mock_calls)
177
178     def test_close(self):
179         with mock.patch.object(self.test_client, "_client") as m_client:
180             self.test_client.close()
181         m_client.close.assert_called_once_with()
182         self.assertFalse(self.test_client._client)
183
184     @mock.patch("yardstick.ssh.six.moves.StringIO")
185     def test_execute(self, mock_string_io):
186         mock_string_io.side_effect = stdio = [mock.Mock(), mock.Mock()]
187         stdio[0].read.return_value = "stdout fake data"
188         stdio[1].read.return_value = "stderr fake data"
189         with mock.patch.object(self.test_client, "run", return_value=0)\
190                 as mock_run:
191             status, stdout, stderr = self.test_client.execute(
192                 "cmd",
193                 stdin="fake_stdin",
194                 timeout=43)
195         mock_run.assert_called_once_with(
196             "cmd", stdin="fake_stdin", stdout=stdio[0],
197             stderr=stdio[1], timeout=43, raise_on_error=False)
198         self.assertEqual(0, status)
199         self.assertEqual("stdout fake data", stdout)
200         self.assertEqual("stderr fake data", stderr)
201
202     @mock.patch("yardstick.ssh.time")
203     def test_wait_timeout(self, mock_time):
204         mock_time.time.side_effect = [1, 50, 150]
205         self.test_client.execute = mock.Mock(side_effect=[ssh.SSHError,
206                                                           ssh.SSHError,
207                                                           0])
208         self.assertRaises(ssh.SSHTimeout, self.test_client.wait)
209         self.assertEqual([mock.call("uname")] * 2,
210                          self.test_client.execute.mock_calls)
211
212     @mock.patch("yardstick.ssh.time")
213     def test_wait(self, mock_time):
214         mock_time.time.side_effect = [1, 50, 100]
215         self.test_client.execute = mock.Mock(side_effect=[ssh.SSHError,
216                                                           ssh.SSHError,
217                                                           0])
218         self.test_client.wait()
219         self.assertEqual([mock.call("uname")] * 3,
220                          self.test_client.execute.mock_calls)
221
222     @mock.patch("yardstick.ssh.paramiko")
223     def test_send_command(self, mock_paramiko):
224         paramiko_sshclient = self.test_client._get_client()
225         with mock.patch.object(paramiko_sshclient, "exec_command") \
226                 as mock_paramiko_exec_command:
227             self.test_client.send_command('cmd')
228         mock_paramiko_exec_command.assert_called_once_with('cmd',
229                                                            get_pty=True)
230
231
232 class SSHRunTestCase(unittest.TestCase):
233     """Test SSH.run method in different aspects.
234
235     Also tested method "execute".
236     """
237
238     def setUp(self):
239         super(SSHRunTestCase, self).setUp()
240
241         self.fake_client = mock.Mock()
242         self.fake_session = mock.Mock()
243         self.fake_transport = mock.Mock()
244
245         self.fake_transport.open_session.return_value = self.fake_session
246         self.fake_client.get_transport.return_value = self.fake_transport
247
248         self.fake_session.recv_ready.return_value = False
249         self.fake_session.recv_stderr_ready.return_value = False
250         self.fake_session.send_ready.return_value = False
251         self.fake_session.exit_status_ready.return_value = True
252         self.fake_session.recv_exit_status.return_value = 0
253
254         self.test_client = ssh.SSH("admin", "example.net")
255         self.test_client._get_client = mock.Mock(return_value=self.fake_client)
256
257     @mock.patch("yardstick.ssh.select")
258     def test_execute(self, mock_select):
259         mock_select.select.return_value = ([], [], [])
260         self.fake_session.recv_ready.side_effect = [1, 0, 0]
261         self.fake_session.recv_stderr_ready.side_effect = [1, 0]
262         self.fake_session.recv.return_value = "ok"
263         self.fake_session.recv_stderr.return_value = "error"
264         self.fake_session.exit_status_ready.return_value = 1
265         self.fake_session.recv_exit_status.return_value = 127
266         self.assertEqual((127, "ok", "error"), self.test_client.execute("cmd"))
267         self.fake_session.exec_command.assert_called_once_with("cmd")
268
269     @mock.patch("yardstick.ssh.select")
270     def test_execute_args(self, mock_select):
271         mock_select.select.return_value = ([], [], [])
272         self.fake_session.recv_ready.side_effect = [1, 0, 0]
273         self.fake_session.recv_stderr_ready.side_effect = [1, 0]
274         self.fake_session.recv.return_value = "ok"
275         self.fake_session.recv_stderr.return_value = "error"
276         self.fake_session.exit_status_ready.return_value = 1
277         self.fake_session.recv_exit_status.return_value = 127
278
279         result = self.test_client.execute("cmd arg1 'arg2 with space'")
280         self.assertEqual((127, "ok", "error"), result)
281         self.fake_session.exec_command.assert_called_once_with(
282             "cmd arg1 'arg2 with space'")
283
284     @mock.patch("yardstick.ssh.select")
285     def test_run(self, mock_select):
286         mock_select.select.return_value = ([], [], [])
287         self.assertEqual(0, self.test_client.run("cmd"))
288
289     @mock.patch("yardstick.ssh.select")
290     def test_run_nonzero_status(self, mock_select):
291         mock_select.select.return_value = ([], [], [])
292         self.fake_session.recv_exit_status.return_value = 1
293         self.assertRaises(ssh.SSHError, self.test_client.run, "cmd")
294         self.assertEqual(1, self.test_client.run("cmd", raise_on_error=False))
295
296     @mock.patch("yardstick.ssh.select")
297     def test_run_stdout(self, mock_select):
298         mock_select.select.return_value = ([], [], [])
299         self.fake_session.recv_ready.side_effect = [True, True, False]
300         self.fake_session.recv.side_effect = ["ok1", "ok2"]
301         stdout = mock.Mock()
302         self.test_client.run("cmd", stdout=stdout)
303         self.assertEqual([mock.call("ok1"), mock.call("ok2")],
304                          stdout.write.mock_calls)
305
306     @mock.patch("yardstick.ssh.select")
307     def test_run_stderr(self, mock_select):
308         mock_select.select.return_value = ([], [], [])
309         self.fake_session.recv_stderr_ready.side_effect = [True, False]
310         self.fake_session.recv_stderr.return_value = "error"
311         stderr = mock.Mock()
312         self.test_client.run("cmd", stderr=stderr)
313         stderr.write.assert_called_once_with("error")
314
315     @mock.patch("yardstick.ssh.select")
316     def test_run_stdin(self, mock_select):
317         """Test run method with stdin.
318
319         Third send call was called with "e2" because only 3 bytes was sent
320         by second call. So remainig 2 bytes of "line2" was sent by third call.
321         """
322         mock_select.select.return_value = ([], [], [])
323         self.fake_session.exit_status_ready.side_effect = [0, 0, 0, True]
324         self.fake_session.send_ready.return_value = True
325         self.fake_session.send.side_effect = [5, 3, 2]
326         fake_stdin = mock.Mock()
327         fake_stdin.read.side_effect = ["line1", "line2", ""]
328         fake_stdin.closed = False
329
330         def close():
331             fake_stdin.closed = True
332         fake_stdin.close = mock.Mock(side_effect=close)
333         self.test_client.run("cmd", stdin=fake_stdin)
334         call = mock.call
335         send_calls = [call(encodeutils.safe_encode("line1", "utf-8")),
336                       call(encodeutils.safe_encode("line2", "utf-8")),
337                       call(encodeutils.safe_encode("e2", "utf-8"))]
338         self.assertEqual(send_calls, self.fake_session.send.mock_calls)
339
340     @mock.patch("yardstick.ssh.select")
341     def test_run_stdin_keep_open(self, mock_select):
342         """Test run method with stdin.
343
344         Third send call was called with "e2" because only 3 bytes was sent
345         by second call. So remainig 2 bytes of "line2" was sent by third call.
346         """
347         mock_select.select.return_value = ([], [], [])
348         self.fake_session.exit_status_ready.side_effect = [0, 0, 0, True]
349         self.fake_session.send_ready.return_value = True
350         self.fake_session.send.side_effect = len
351         fake_stdin = StringIO(u"line1\nline2\n")
352         self.test_client.run("cmd", stdin=fake_stdin, keep_stdin_open=True)
353         call = mock.call
354         send_calls = [call(encodeutils.safe_encode("line1\nline2\n", "utf-8"))]
355         self.assertEqual(send_calls, self.fake_session.send.mock_calls)
356
357     @mock.patch("yardstick.ssh.select")
358     def test_run_select_error(self, mock_select):
359         self.fake_session.exit_status_ready.return_value = False
360         mock_select.select.return_value = ([], [], [True])
361         self.assertRaises(ssh.SSHError, self.test_client.run, "cmd")
362
363     @mock.patch("yardstick.ssh.time")
364     @mock.patch("yardstick.ssh.select")
365     def test_run_timemout(self, mock_select, mock_time):
366         mock_time.time.side_effect = [1, 3700]
367         mock_select.select.return_value = ([], [], [])
368         self.fake_session.exit_status_ready.return_value = False
369         self.assertRaises(ssh.SSHTimeout, self.test_client.run, "cmd")
370
371     @mock.patch("yardstick.ssh.open", create=True)
372     def test__put_file_shell(self, mock_open):
373         with mock.patch.object(self.test_client, "run") as run_mock:
374             self.test_client._put_file_shell("localfile", "remotefile", 0o42)
375             run_mock.assert_called_once_with(
376                 'cat > "remotefile"&& chmod -- 042 "remotefile"',
377                 stdin=mock_open.return_value.__enter__.return_value)
378
379     @mock.patch("yardstick.ssh.open", create=True)
380     def test__put_file_shell_space(self, mock_open):
381         with mock.patch.object(self.test_client, "run") as run_mock:
382             self.test_client._put_file_shell("localfile",
383                                              "filename with space", 0o42)
384             run_mock.assert_called_once_with(
385                 'cat > "filename with space"&& chmod -- 042 "filename with '
386                 'space"',
387                 stdin=mock_open.return_value.__enter__.return_value)
388
389     @mock.patch("yardstick.ssh.open", create=True)
390     def test__put_file_shell_tilde(self, mock_open):
391         with mock.patch.object(self.test_client, "run") as run_mock:
392             self.test_client._put_file_shell("localfile", "~/remotefile", 0o42)
393             run_mock.assert_called_once_with(
394                 'cat > ~/"remotefile"&& chmod -- 042 ~/"remotefile"',
395                 stdin=mock_open.return_value.__enter__.return_value)
396
397     @mock.patch("yardstick.ssh.open", create=True)
398     def test__put_file_shell_tilde_spaces(self, mock_open):
399         with mock.patch.object(self.test_client, "run") as run_mock:
400             self.test_client._put_file_shell("localfile", "~/file with space",
401                                              0o42)
402             run_mock.assert_called_once_with(
403                 'cat > ~/"file with space"&& chmod -- 042 ~/"file with space"',
404                 stdin=mock_open.return_value.__enter__.return_value)
405
406     @mock.patch("yardstick.ssh.os.stat")
407     def test__put_file_sftp(self, mock_stat):
408         sftp = self.fake_client.open_sftp.return_value = mock.MagicMock()
409         sftp.__enter__.return_value = sftp
410
411         mock_stat.return_value = os.stat_result([0o753] + [0] * 9)
412
413         self.test_client._put_file_sftp("localfile", "remotefile")
414
415         sftp.put.assert_called_once_with("localfile", "remotefile")
416         mock_stat.assert_called_once_with("localfile")
417         sftp.chmod.assert_called_once_with("remotefile", 0o753)
418         sftp.__exit__.assert_called_once_with(None, None, None)
419
420     def test__put_file_sftp_mode(self):
421         sftp = self.fake_client.open_sftp.return_value = mock.MagicMock()
422         sftp.__enter__.return_value = sftp
423
424         self.test_client._put_file_sftp("localfile", "remotefile", mode=0o753)
425
426         sftp.put.assert_called_once_with("localfile", "remotefile")
427         sftp.chmod.assert_called_once_with("remotefile", 0o753)
428         sftp.__exit__.assert_called_once_with(None, None, None)
429
430     def test_put_file_SSHException(self):
431         exc = ssh.paramiko.SSHException
432         self.test_client._put_file_sftp = mock.Mock(side_effect=exc())
433         self.test_client._put_file_shell = mock.Mock()
434
435         self.test_client.put_file("foo", "bar", 42)
436         self.test_client._put_file_sftp.assert_called_once_with("foo", "bar",
437                                                                 mode=42)
438         self.test_client._put_file_shell.assert_called_once_with("foo", "bar",
439                                                                  mode=42)
440
441     def test_put_file_socket_error(self):
442         exc = socket.error
443         self.test_client._put_file_sftp = mock.Mock(side_effect=exc())
444         self.test_client._put_file_shell = mock.Mock()
445
446         self.test_client.put_file("foo", "bar", 42)
447         self.test_client._put_file_sftp.assert_called_once_with("foo", "bar",
448                                                                 mode=42)
449         self.test_client._put_file_shell.assert_called_once_with("foo", "bar",
450                                                                  mode=42)
451
452
453 def main():
454     unittest.main()
455
456
457 if __name__ == '__main__':
458     main()