fuel-devops/devops/tests/helpers/test_ssh_client.py

1977 lines
64 KiB
Python

# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import unicode_literals
# pylint: disable=no-self-use
import base64
import contextlib
from os import path
import posixpath
import stat
import unittest
import mock
import paramiko
# noinspection PyUnresolvedReferences
from six.moves import cStringIO
from devops import error
from devops.helpers import exec_result
from devops.helpers import ssh_client
def gen_private_keys(amount=1):
keys = []
for _ in range(amount):
keys.append(paramiko.RSAKey.generate(1024))
return keys
def gen_public_key(private_key=None):
if private_key is None:
private_key = paramiko.RSAKey.generate(1024)
return '{0} {1}'.format(private_key.get_name(), private_key.get_base64())
class FakeStream(object):
def __init__(self, *args):
self.__src = list(args)
def __iter__(self):
if len(self.__src) == 0:
raise IOError()
for _ in range(len(self.__src)):
yield self.__src.pop(0)
host = '127.0.0.1'
port = 22
username = 'user'
password = 'pass'
private_keys = []
command = 'ls ~ '
stdout_list = [b' \n', b'2\n', b'3\n', b' \n']
stderr_list = [b' \n', b'0\n', b'1\n', b' \n']
encoded_cmd = base64.b64encode(
"{}\n".format(command).encode('utf-8')
).decode('utf-8')
# noinspection PyTypeChecker
class TestSSHAuth(unittest.TestCase):
def tearDown(self):
ssh_client.SSHClient._clear_cache()
def init_checks(self, username=None, password=None, key=None, keys=None):
"""shared positive init checks
:type username: str
:type password: str
:type key: paramiko.RSAKey
:type keys: list
"""
auth = ssh_client.SSHAuth(
username=username,
password=password,
key=key,
keys=keys
)
int_keys = [None]
if key is not None:
int_keys.append(key)
if keys is not None:
for k in keys:
if k not in int_keys:
int_keys.append(k)
self.assertEqual(auth.username, username)
with contextlib.closing(cStringIO()) as tgt:
auth.enter_password(tgt)
self.assertEqual(tgt.getvalue(), '{}\n'.format(password))
self.assertEqual(
auth.public_key,
gen_public_key(key) if key is not None else None)
_key = (
None if auth.public_key is None else
'<private for pub: {}>'.format(auth.public_key)
)
_keys = []
for k in int_keys:
if k == key:
continue
_keys.append(
'<private for pub: {}>'.format(
gen_public_key(k)) if k is not None else None)
self.assertEqual(
repr(auth),
"{cls}("
"username={username}, "
"password=<*masked*>, "
"key={key}, "
"keys={keys})".format(
cls=ssh_client.SSHAuth.__name__,
username=auth.username,
key=_key,
keys=_keys
)
)
self.assertEqual(
str(auth),
'{cls} for {username}'.format(
cls=ssh_client.SSHAuth.__name__,
username=auth.username,
)
)
def test_init_username_only(self):
self.init_checks(
username=username
)
def test_init_username_password(self):
self.init_checks(
username=username,
password=password
)
def test_init_username_key(self):
self.init_checks(
username=username,
key=gen_private_keys(1).pop()
)
def test_init_username_password_key(self):
self.init_checks(
username=username,
password=password,
key=gen_private_keys(1).pop()
)
def test_init_username_password_keys(self):
self.init_checks(
username=username,
password=password,
keys=gen_private_keys(2)
)
def test_init_username_password_key_keys(self):
self.init_checks(
username=username,
password=password,
key=gen_private_keys(1).pop(),
keys=gen_private_keys(2)
)
# noinspection PyTypeChecker
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
@mock.patch(
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
@mock.patch('paramiko.SSHClient', autospec=True)
class TestSSHClientInit(unittest.TestCase):
def tearDown(self):
ssh_client.SSHClient._clear_cache()
def init_checks(
self,
client, policy, logger,
host=None, port=22,
username=None, password=None, private_keys=None,
auth=None
):
"""shared checks for positive cases
:type client: mock.Mock
:type policy: mock.Mock
:type logger: mock.Mock
:type host: str
:type port: int
:type username: str
:type password: str
:type private_keys: list
:type auth: ssh_client.SSHAuth
"""
_ssh = mock.call()
ssh = ssh_client.SSHClient(
host=host,
port=port,
username=username,
password=password,
private_keys=private_keys,
auth=auth
)
client.assert_called_once()
policy.assert_called_once()
if auth is None:
if private_keys is None or len(private_keys) == 0:
logger.assert_has_calls((
mock.call.debug(
'SSHClient('
'host={host}, port={port}, username={username}): '
'initialization by username/password/private_keys '
'is deprecated in favor of SSHAuth usage. '
'Please update your code'.format(
host=host, port=port, username=username
)),
mock.call.info(
'{0}:{1}> SSHAuth was made from old style creds: '
'SSHAuth for {2}'.format(host, port, username))
))
else:
logger.assert_has_calls((
mock.call.debug(
'SSHClient('
'host={host}, port={port}, username={username}): '
'initialization by username/password/private_keys '
'is deprecated in favor of SSHAuth usage. '
'Please update your code'.format(
host=host, port=port, username=username
)),
mock.call.debug(
'Main key has been updated, public key is: \n'
'{}'.format(ssh.auth.public_key)),
mock.call.info(
'{0}:{1}> SSHAuth was made from old style creds: '
'SSHAuth for {2}'.format(host, port, username))
))
else:
logger.assert_not_called()
if auth is None:
if private_keys is None or len(private_keys) == 0:
pkey = None
expected_calls = [
_ssh,
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
_ssh.connect(
hostname=host, password=password,
pkey=pkey,
port=port, username=username),
]
else:
pkey = private_keys[0]
expected_calls = [
_ssh,
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
_ssh.connect(
hostname=host, password=password,
pkey=None,
port=port, username=username),
_ssh.connect(
hostname=host, password=password,
pkey=pkey,
port=port, username=username),
]
self.assertIn(expected_calls, client.mock_calls)
self.assertEqual(
ssh.auth,
ssh_client.SSHAuth(
username=username,
password=password,
keys=private_keys
)
)
else:
self.assertEqual(ssh.auth, auth)
sftp = ssh._sftp
self.assertEqual(sftp, client().open_sftp())
self.assertEqual(ssh._ssh, client())
self.assertEqual(ssh.hostname, host)
self.assertEqual(ssh.port, port)
self.assertEqual(
repr(ssh),
'{cls}(host={host}, port={port}, auth={auth!r})'.format(
cls=ssh.__class__.__name__, host=ssh.hostname,
port=ssh.port,
auth=ssh.auth
)
)
def test_init_host(self, client, policy, logger):
"""Test with host only set"""
self.init_checks(
client, policy, logger,
host=host)
def test_init_alternate_port(self, client, policy, logger):
"""Test with alternate port"""
self.init_checks(
client, policy, logger,
host=host,
port=2222
)
def test_init_username(self, client, policy, logger):
"""Test with username only set from creds"""
self.init_checks(
client, policy, logger,
host=host,
username=username
)
def test_init_username_password(self, client, policy, logger):
"""Test with username and password set from creds"""
self.init_checks(
client, policy, logger,
host=host,
username=username,
password=password
)
def test_init_username_password_empty_keys(self, client, policy, logger):
"""Test with username, password and empty keys set from creds"""
self.init_checks(
client, policy, logger,
host=host,
username=username,
password=password,
private_keys=[]
)
def test_init_username_single_key(self, client, policy, logger):
"""Test with username and single key set from creds"""
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException, mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
self.init_checks(
client, policy, logger,
host=host,
username=username,
private_keys=gen_private_keys(1)
)
def test_init_username_password_single_key(self, client, policy, logger):
"""Test with username, password and single key set from creds"""
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException, mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
self.init_checks(
client, policy, logger,
host=host,
username=username,
password=password,
private_keys=gen_private_keys(1)
)
def test_init_username_multiple_keys(self, client, policy, logger):
"""Test with username and multiple keys set from creds"""
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException, mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
self.init_checks(
client, policy, logger,
host=host,
username=username,
private_keys=gen_private_keys(2)
)
def test_init_username_password_multiple_keys(
self, client, policy, logger):
"""Test with username, password and multiple keys set from creds"""
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException, mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException, mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
self.init_checks(
client, policy, logger,
host=host,
username=username,
password=password,
private_keys=gen_private_keys(2)
)
def test_init_auth(self, client, policy, logger):
self.init_checks(
client, policy, logger,
host=host,
auth=ssh_client.SSHAuth(
username=username,
password=password,
key=gen_private_keys(1).pop()
)
)
def test_init_auth_break(self, client, policy, logger):
self.init_checks(
client, policy, logger,
host=host,
username='Invalid',
password='Invalid',
private_keys=gen_private_keys(1),
auth=ssh_client.SSHAuth(
username=username,
password=password,
key=gen_private_keys(1).pop()
)
)
def test_init_context(self, client, policy, logger):
with ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth()) as ssh:
client.assert_called_once()
policy.assert_called_once()
logger.assert_not_called()
self.assertEqual(ssh.auth, ssh_client.SSHAuth())
sftp = ssh._sftp
self.assertEqual(sftp, client().open_sftp())
self.assertEqual(ssh._ssh, client())
self.assertEqual(ssh.hostname, host)
self.assertEqual(ssh.port, port)
def test_init_clear_failed(self, client, policy, logger):
"""Test reconnect
:type client: mock.Mock
:type policy: mock.Mock
:type logger: mock.Mock
"""
_ssh = mock.Mock()
_ssh.attach_mock(
mock.Mock(
side_effect=[
Exception('Mocked SSH close()'),
mock.Mock()
]),
'close')
_sftp = mock.Mock()
_sftp.attach_mock(
mock.Mock(
side_effect=[
Exception('Mocked SFTP close()'),
mock.Mock()
]),
'close')
client.return_value = _ssh
_ssh.attach_mock(mock.Mock(return_value=_sftp), 'open_sftp')
ssh = ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth())
client.assert_called_once()
policy.assert_called_once()
logger.assert_not_called()
self.assertEqual(ssh.auth, ssh_client.SSHAuth())
sftp = ssh._sftp
self.assertEqual(sftp, _sftp)
self.assertEqual(ssh._ssh, _ssh)
self.assertEqual(ssh.hostname, host)
self.assertEqual(ssh.port, port)
logger.reset_mock()
ssh.close()
logger.assert_has_calls((
mock.call.exception('Could not close ssh connection'),
mock.call.exception('Could not close sftp connection'),
))
def test_init_reconnect(self, client, policy, logger):
"""Test reconnect
:type client: mock.Mock
:type policy: mock.Mock
:type logger: mock.Mock
"""
ssh = ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth())
client.assert_called_once()
policy.assert_called_once()
logger.assert_not_called()
self.assertEqual(ssh.auth, ssh_client.SSHAuth())
sftp = ssh._sftp
self.assertEqual(sftp, client().open_sftp())
self.assertEqual(ssh._ssh, client())
client.reset_mock()
policy.reset_mock()
self.assertEqual(ssh.hostname, host)
self.assertEqual(ssh.port, port)
ssh.reconnect()
_ssh = mock.call()
expected_calls = [
_ssh.close(),
_ssh,
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
_ssh.connect(
hostname='127.0.0.1',
password=None,
pkey=None,
port=22,
username=None),
]
self.assertIn(
expected_calls,
client.mock_calls
)
client.assert_called_once()
policy.assert_called_once()
logger.assert_not_called()
self.assertEqual(ssh.auth, ssh_client.SSHAuth())
sftp = ssh._sftp
self.assertEqual(sftp, client().open_sftp())
self.assertEqual(ssh._ssh, client())
@mock.patch('time.sleep', autospec=True)
def test_init_password_required(self, sleep, client, policy, logger):
connect = mock.Mock(side_effect=paramiko.PasswordRequiredException)
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
with self.assertRaises(paramiko.PasswordRequiredException):
ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth())
logger.assert_has_calls((
mock.call.exception('No password has been set!'),
))
@mock.patch('time.sleep', autospec=True)
def test_init_password_broken(self, sleep, client, policy, logger):
connect = mock.Mock(side_effect=paramiko.PasswordRequiredException)
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
with self.assertRaises(paramiko.PasswordRequiredException):
ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(password=password))
logger.assert_has_calls((
mock.call.critical(
'Unexpected PasswordRequiredException, '
'when password is set!'
),
))
@mock.patch('time.sleep', autospec=True)
def test_init_auth_impossible_password(
self, sleep, client, policy, logger):
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
with self.assertRaises(paramiko.AuthenticationException):
ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(password=password))
logger.assert_has_calls(
(
mock.call.exception(
'Connection using stored authentication info failed!'),
) * 3
)
@mock.patch('time.sleep', autospec=True)
def test_init_auth_impossible_key(self, sleep, client, policy, logger):
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
with self.assertRaises(paramiko.AuthenticationException):
ssh_client.SSHClient(
host=host,
auth=ssh_client.SSHAuth(key=gen_private_keys(1).pop())
)
logger.assert_has_calls(
(
mock.call.exception(
'Connection using stored authentication info failed!'),
) * 3
)
def test_init_auth_pass_no_key(self, client, policy, logger):
connect = mock.Mock(
side_effect=[
paramiko.AuthenticationException,
mock.Mock()
])
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
key = gen_private_keys(1).pop()
ssh = ssh_client.SSHClient(
host=host,
auth=ssh_client.SSHAuth(
username=username,
password=password,
key=key
)
)
client.assert_called_once()
policy.assert_called_once()
logger.assert_has_calls((
mock.call.debug(
'Main key has been updated, public key is: \nNone'),
))
self.assertEqual(
ssh.auth,
ssh_client.SSHAuth(
username=username,
password=password,
keys=[key]
)
)
sftp = ssh._sftp
self.assertEqual(sftp, client().open_sftp())
self.assertEqual(ssh._ssh, client())
@mock.patch('time.sleep', autospec=True)
def test_init_auth_brute_impossible(self, sleep, client, policy, logger):
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
_ssh = mock.Mock()
_ssh.attach_mock(connect, 'connect')
client.return_value = _ssh
with self.assertRaises(paramiko.AuthenticationException):
ssh_client.SSHClient(
host=host,
username=username,
private_keys=gen_private_keys(2))
logger.assert_has_calls(
(
mock.call.debug(
'SSHClient('
'host={host}, port={port}, username={username}): '
'initialization by username/password/private_keys '
'is deprecated in favor of SSHAuth usage. '
'Please update your code'.format(
host=host, port=port, username=username
)),
) + (
mock.call.exception(
'Connection using stored authentication info failed!'),
) * 3
)
def test_init_no_sftp(self, client, policy, logger):
open_sftp = mock.Mock(side_effect=paramiko.SSHException)
_ssh = mock.Mock()
_ssh.attach_mock(open_sftp, 'open_sftp')
client.return_value = _ssh
ssh = ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(password=password))
with self.assertRaises(paramiko.SSHException):
# pylint: disable=pointless-statement
# noinspection PyStatementEffect
ssh._sftp
# pylint: enable=pointless-statement
logger.assert_has_calls((
mock.call.debug('SFTP is not connected, try to connect...'),
mock.call.warning(
'SFTP enable failed! SSH only is accessible.'),
))
def test_init_sftp_repair(self, client, policy, logger):
_sftp = mock.Mock()
open_sftp = mock.Mock(
side_effect=[
paramiko.SSHException,
_sftp, _sftp])
_ssh = mock.Mock()
_ssh.attach_mock(open_sftp, 'open_sftp')
client.return_value = _ssh
ssh = ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(password=password))
with self.assertRaises(paramiko.SSHException):
# pylint: disable=pointless-statement
# noinspection PyStatementEffect
ssh._sftp
# pylint: enable=pointless-statement
logger.reset_mock()
sftp = ssh._sftp
self.assertEqual(sftp, open_sftp())
logger.assert_has_calls((
mock.call.debug('SFTP is not connected, try to connect...'),
))
@mock.patch('devops.helpers.exec_result.ExecResult', autospec=True)
def test_init_memorize(
self,
Result,
client, policy, logger):
port1 = 2222
host1 = '127.0.0.2'
# 1. Normal init
ssh01 = ssh_client.SSHClient(host=host)
ssh02 = ssh_client.SSHClient(host=host)
ssh11 = ssh_client.SSHClient(host=host, port=port1)
ssh12 = ssh_client.SSHClient(host=host, port=port1)
ssh21 = ssh_client.SSHClient(host=host1)
ssh22 = ssh_client.SSHClient(host=host1)
self.assertTrue(ssh01 is ssh02)
self.assertTrue(ssh11 is ssh12)
self.assertTrue(ssh21 is ssh22)
self.assertFalse(ssh01 is ssh11)
self.assertFalse(ssh01 is ssh21)
self.assertFalse(ssh11 is ssh21)
# 2. Close connections check
client.reset_mock()
ssh01.close_connections(ssh01.hostname)
client.assert_has_calls((
mock.call().get_transport(),
mock.call().get_transport(),
mock.call().close(),
mock.call().close(),
))
client.reset_mock()
ssh01.close_connections()
# Mock returns false-connected state, so we just count close calls
client.assert_has_calls((
mock.call().get_transport(),
mock.call().get_transport(),
mock.call().get_transport(),
mock.call().close(),
mock.call().close(),
mock.call().close(),
))
# change creds
ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(username=username))
# Change back: new connection differs from old with the same creds
ssh004 = ssh_client.SSHAuth(host)
self.assertFalse(ssh01 is ssh004)
@mock.patch('warnings.warn')
def test_init_memorize_close_unused(self, warn, client, policy, logger):
ssh0 = ssh_client.SSHClient(host=host)
text = str(ssh0)
del ssh0 # remove reference - now it's cached and unused
client.reset_mock()
logger.reset_mock()
# New connection on the same host:port with different auth
ssh1 = ssh_client.SSHClient(
host=host, auth=ssh_client.SSHAuth(username=username))
logger.assert_has_calls((
mock.call.debug('Closing {} as unused'.format(text)),
))
client.assert_has_calls((
mock.call().close(),
))
text = str(ssh1)
del ssh1 # remove reference - now it's cached and unused
client.reset_mock()
logger.reset_mock()
ssh_client.SSHClient._clear_cache()
logger.assert_has_calls((
mock.call.debug('Closing {} as unused'.format(text)),
))
client.assert_has_calls((
mock.call().close(),
))
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute')
def test_init_memorize_reconnect(self, execute, client, policy, logger):
execute.side_effect = paramiko.SSHException
ssh_client.SSHClient(host=host)
client.reset_mock()
policy.reset_mock()
logger.reset_mock()
ssh_client.SSHClient(host=host)
client.assert_called_once()
policy.assert_called_once()
@mock.patch('warnings.warn')
def test_init_clear(self, warn, client, policy, logger):
ssh01 = ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth())
# noinspection PyDeprecation
ssh01.clear()
warn.assert_called_once_with(
"clear is removed: use close() only if it mandatory: "
"it's automatically called on revert|shutdown|suspend|destroy",
DeprecationWarning
)
self.assertNotIn(
mock.call.close(),
client.mock_calls
)
@mock.patch('warnings.warn')
def test_deprecated_host(self, warn, client, policy, logger):
ssh01 = ssh_client.SSHClient(host=host, auth=ssh_client.SSHAuth())
# noinspection PyDeprecation
self.assertEqual(ssh01.host, ssh01.hostname)
warn.assert_called_once_with(
'host has been deprecated in favor of hostname',
DeprecationWarning
)
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
@mock.patch(
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
@mock.patch('paramiko.SSHClient', autospec=True)
class TestExecute(unittest.TestCase):
def tearDown(self):
ssh_client.SSHClient._clear_cache()
@staticmethod
def get_ssh():
"""SSHClient object builder for execution tests
:rtype: ssh_client.SSHClient
"""
# noinspection PyTypeChecker
return ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
def test_execute_async(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('{}\n'.format(command))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
def test_execute_async_pty(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
# noinspection PyTypeChecker
result = ssh.execute_async(command=command, get_pty=True)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.get_pty(
term='vt100',
width=80, height=24,
width_pixels=0, height_pixels=0
),
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('{}\n'.format(command))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
def test_execute_async_sudo(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
ssh.sudo_mode = True
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command(
"sudo -S bash -c '"
"eval \"$(base64 -d <(echo \"{0}\"))\"'".format(encoded_cmd))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
def test_execute_async_with_sudo_enforce(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
self.assertFalse(ssh.sudo_mode)
with ssh_client.SSHClient.sudo(ssh, enforce=True):
self.assertTrue(ssh.sudo_mode)
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
self.assertFalse(ssh.sudo_mode)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command(
"sudo -S bash -c '"
"eval \"$(base64 -d <(echo \"{0}\"))\"'".format(encoded_cmd))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
def test_execute_async_with_no_sudo_enforce(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
ssh.sudo_mode = True
with ssh.sudo(enforce=False):
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('{}\n'.format(command))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
def test_execute_async_with_none_enforce(self, client, policy, logger):
chan = mock.Mock()
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
ssh.sudo_mode = False
with ssh.sudo():
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
get_transport.assert_called_once()
open_session.assert_called_once()
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('{}\n'.format(command))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
@mock.patch('devops.helpers.ssh_client.SSHAuth.enter_password')
def test_execute_async_sudo_password(
self, enter_password, client, policy, logger):
stdin = mock.Mock(name='stdin')
stdout = mock.Mock(name='stdout')
stdout_channel = mock.Mock()
stdout_channel.configure_mock(closed=False)
stdout.attach_mock(stdout_channel, 'channel')
makefile = mock.Mock(side_effect=[stdin, stdout])
chan = mock.Mock()
chan.attach_mock(makefile, 'makefile')
open_session = mock.Mock(return_value=chan)
transport = mock.Mock()
transport.attach_mock(open_session, 'open_session')
get_transport = mock.Mock(return_value=transport)
_ssh = mock.Mock()
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
ssh = self.get_ssh()
ssh.sudo_mode = True
# noinspection PyTypeChecker
result = ssh.execute_async(command=command)
get_transport.assert_called_once()
open_session.assert_called_once()
# raise ValueError(closed.mock_calls)
enter_password.assert_called_once_with(stdin)
stdin.assert_has_calls((mock.call.flush(), ))
self.assertIn(chan, result)
chan.assert_has_calls((
mock.call.makefile('wb'),
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command(
"sudo -S bash -c '"
"eval \"$(base64 -d <(echo \"{0}\"))\"'".format(encoded_cmd))
))
self.assertIn(
mock.call.debug(
"Executing command: {!r}".format(command.rstrip())),
logger.mock_calls
)
@staticmethod
def get_patched_execute_async_retval(ec=0, stderr_val=None):
"""get patched execute_async retval
:rtype:
Tuple(
mock.Mock,
str,
exec_result.ExecResult,
FakeStream,
FakeStream)
"""
out = stdout_list
err = stderr_list if stderr_val is None else []
stdout = FakeStream(*out)
stderr = FakeStream(*err)
exit_code = ec
chan = mock.Mock()
recv_exit_status = mock.Mock(return_value=exit_code)
chan.attach_mock(recv_exit_status, 'recv_exit_status')
wait = mock.Mock()
status_event = mock.Mock()
status_event.attach_mock(wait, 'wait')
chan.attach_mock(status_event, 'status_event')
chan.configure_mock(exit_status=exit_code)
# noinspection PyTypeChecker
exp_result = exec_result.ExecResult(
cmd=command,
stderr=err,
stdout=out,
exit_code=ec
)
return chan, '', exp_result, stderr, stdout
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute_async')
def test_execute(
self,
execute_async,
client, policy, logger):
(
chan, _stdin, exp_result, stderr, stdout
) = self.get_patched_execute_async_retval()
is_set = mock.Mock(return_value=True)
chan.status_event.attach_mock(is_set, 'is_set')
execute_async.return_value = chan, _stdin, stderr, stdout
ssh = self.get_ssh()
logger.reset_mock()
# noinspection PyTypeChecker
result = ssh.execute(command=command, verbose=False)
self.assertEqual(
result,
exp_result
)
execute_async.assert_called_once_with(command)
chan.assert_has_calls((mock.call.status_event.is_set(), ))
logger.assert_has_calls([
mock.call.debug(
"\nExecuting command: {!r}".format(command.rstrip())),
] + [
mock.call.debug(str(x.rstrip().decode('utf-8')))
for x in stdout_list
] + [
mock.call.debug(str(x.rstrip().decode('utf-8')))
for x in stderr_list
] + [
mock.call.debug(
'\n{cmd!r} execution results: '
'Exit code: {code!s}'.format(
cmd=command,
code=result.exit_code,
)),
])
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute_async')
def test_execute_verbose(
self,
execute_async,
client, policy, logger):
(
chan, _stdin, exp_result, stderr, stdout
) = self.get_patched_execute_async_retval()
is_set = mock.Mock(return_value=True)
chan.status_event.attach_mock(is_set, 'is_set')
execute_async.return_value = chan, _stdin, stderr, stdout
ssh = self.get_ssh()
logger.reset_mock()
# noinspection PyTypeChecker
result = ssh.execute(command=command, verbose=True)
self.assertEqual(
result,
exp_result
)
execute_async.assert_called_once_with(command)
chan.assert_has_calls((mock.call.status_event.is_set(), ))
logger.assert_has_calls([
mock.call.info(
"\nExecuting command: {!r}".format(command.rstrip())),
] + [
mock.call.info(str(x.rstrip().decode('utf-8')))
for x in stdout_list
] + [
mock.call.error(str(x.rstrip().decode('utf-8')))
for x in stderr_list
] + [
mock.call.info(
'\n{cmd!r} execution results: '
'Exit code: {code!s}'.format(
cmd=command,
code=result.exit_code,
)),
])
@mock.patch('time.sleep', autospec=True)
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute_async')
def test_execute_timeout(
self,
execute_async, sleep,
client, policy, logger):
(
chan, _stdin, exp_result, stderr, stdout
) = self.get_patched_execute_async_retval()
is_set = mock.Mock(return_value=True)
chan.status_event.attach_mock(is_set, 'is_set')
execute_async.return_value = chan, _stdin, stderr, stdout
ssh = self.get_ssh()
logger.reset_mock()
# noinspection PyTypeChecker
result = ssh.execute(command=command, verbose=False, timeout=1)
self.assertEqual(
result,
exp_result
)
execute_async.assert_called_once_with(command)
chan.assert_has_calls((mock.call.status_event.is_set(), ))
logger.assert_has_calls((
mock.call.debug(
'\n{cmd!r} execution results: '
'Exit code: {code!s}'.format(
cmd=exp_result.cmd,
code=exp_result.exit_code
)),
))
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute_async')
def test_execute_timeout_fail(
self,
execute_async,
client, policy, logger):
(
chan, _stdin, _, stderr, stdout
) = self.get_patched_execute_async_retval()
is_set = mock.Mock(return_value=False)
chan.status_event.attach_mock(is_set, 'is_set')
chan.status_event.attach_mock(mock.Mock(), 'wait')
execute_async.return_value = chan, _stdin, stderr, stdout
ssh = self.get_ssh()
logger.reset_mock()
with self.assertRaises(error.TimeoutError):
# noinspection PyTypeChecker
ssh.execute(command=command, verbose=False, timeout=1)
execute_async.assert_called_once_with(command)
chan.assert_has_calls((mock.call.status_event.is_set(), ))
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute_async')
def test_execute_together(self, execute_async, client, policy, logger):
(
chan, _stdin, _, stderr, stdout
) = self.get_patched_execute_async_retval()
execute_async.return_value = chan, _stdin, stderr, stdout
host2 = '127.0.0.2'
ssh = self.get_ssh()
# noinspection PyTypeChecker
ssh2 = ssh_client.SSHClient(
host=host2,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
remotes = [ssh, ssh2]
# noinspection PyTypeChecker
ssh_client.SSHClient.execute_together(
remotes=remotes, command=command)
self.assertEqual(execute_async.call_count, len(remotes))
chan.assert_has_calls((
mock.call.recv_exit_status(),
mock.call.close(),
mock.call.recv_exit_status(),
mock.call.close()
))
# noinspection PyTypeChecker
ssh_client.SSHClient.execute_together(
remotes=remotes, command=command, expected=[1], raise_on_err=False)
with self.assertRaises(error.DevopsCalledProcessError):
# noinspection PyTypeChecker
ssh_client.SSHClient.execute_together(
remotes=remotes, command=command, expected=[1])
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute')
def test_check_call(self, execute, client, policy, logger):
exit_code = 0
return_value = {
'stderr_str': '0\n1',
'stdout_str': '2\n3',
'stderr_brief': '0\n1',
'stdout_brief': '2\n3',
'exit_code': exit_code,
'stderr': [b' \n', b'0\n', b'1\n', b' \n'],
'stdout': [b' \n', b'2\n', b'3\n', b' \n']}
execute.return_value = return_value
verbose = False
ssh = self.get_ssh()
# noinspection PyTypeChecker
result = ssh.check_call(command=command, verbose=verbose, timeout=None)
execute.assert_called_once_with(command, verbose, None)
self.assertEqual(result, return_value)
exit_code = 1
return_value['exit_code'] = exit_code
execute.reset_mock()
execute.return_value = return_value
with self.assertRaises(error.DevopsCalledProcessError):
# noinspection PyTypeChecker
ssh.check_call(command=command, verbose=verbose, timeout=None)
execute.assert_called_once_with(command, verbose, None)
@mock.patch(
'devops.helpers.ssh_client.SSHClient.execute')
def test_check_call_expected(self, execute, client, policy, logger):
exit_code = 0
return_value = {
'stderr_str': '0\n1',
'stdout_str': '2\n3',
'stderr_brief': '0\n1',
'stdout_brief': '2\n3',
'exit_code': exit_code,
'stderr': [b' \n', b'0\n', b'1\n', b' \n'],
'stdout': [b' \n', b'2\n', b'3\n', b' \n']}
execute.return_value = return_value
verbose = False
ssh = self.get_ssh()
# noinspection PyTypeChecker
result = ssh.check_call(
command=command, verbose=verbose, timeout=None, expected=[0, 75])
execute.assert_called_once_with(command, verbose, None)
self.assertEqual(result, return_value)
exit_code = 1
return_value['exit_code'] = exit_code
execute.reset_mock()
execute.return_value = return_value
with self.assertRaises(error.DevopsCalledProcessError):
# noinspection PyTypeChecker
ssh.check_call(
command=command, verbose=verbose, timeout=None,
expected=[0, 75]
)
execute.assert_called_once_with(command, verbose, None)
@mock.patch(
'devops.helpers.ssh_client.SSHClient.check_call')
def test_check_stderr(self, check_call, client, policy, logger):
return_value = {
'stderr_str': '',
'stdout_str': '2\n3',
'stderr_brief': '',
'stdout_brief': '2\n3',
'exit_code': 0,
'stderr': [],
'stdout': [b' \n', b'2\n', b'3\n', b' \n']}
check_call.return_value = return_value
verbose = False
raise_on_err = True
ssh = self.get_ssh()
# noinspection PyTypeChecker
result = ssh.check_stderr(
command=command, verbose=verbose, timeout=None,
raise_on_err=raise_on_err)
check_call.assert_called_once_with(
command, verbose, timeout=None,
error_info=None, raise_on_err=raise_on_err)
self.assertEqual(result, return_value)
return_value['stderr_str'] = '0\n1'
return_value['stderr'] = [b' \n', b'0\n', b'1\n', b' \n']
check_call.reset_mock()
check_call.return_value = return_value
with self.assertRaises(error.DevopsCalledProcessError):
# noinspection PyTypeChecker
ssh.check_stderr(
command=command, verbose=verbose, timeout=None,
raise_on_err=raise_on_err)
check_call.assert_called_once_with(
command, verbose, timeout=None,
error_info=None, raise_on_err=raise_on_err)
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
@mock.patch(
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
@mock.patch('paramiko.SSHClient', autospec=True)
@mock.patch('paramiko.Transport', autospec=True)
class TestExecuteThrowHost(unittest.TestCase):
def tearDown(self):
ssh_client.SSHClient._clear_cache()
@staticmethod
def prepare_execute_through_host(transp, client, exit_code):
intermediate_channel = mock.Mock(name='intermediate_channel')
open_channel = mock.Mock(
return_value=intermediate_channel,
name='open_channel'
)
intermediate_transport = mock.Mock(name='intermediate_transport')
intermediate_transport.attach_mock(open_channel, 'open_channel')
get_transport = mock.Mock(
return_value=intermediate_transport,
name='get_transport'
)
_ssh = mock.Mock(neme='_ssh')
_ssh.attach_mock(get_transport, 'get_transport')
client.return_value = _ssh
transport = mock.Mock(name='transport')
transp.return_value = transport
recv_exit_status = mock.Mock(return_value=exit_code)
channel = mock.Mock()
channel.attach_mock(
mock.Mock(return_value=FakeStream(b' \n', b'2\n', b'3\n', b' \n')),
'makefile')
channel.attach_mock(
mock.Mock(return_value=FakeStream(b' \n', b'0\n', b'1\n', b' \n')),
'makefile_stderr')
channel.attach_mock(recv_exit_status, 'recv_exit_status')
open_session = mock.Mock(return_value=channel, name='open_session')
transport.attach_mock(open_session, 'open_session')
wait = mock.Mock()
status_event = mock.Mock()
status_event.attach_mock(wait, 'wait')
channel.attach_mock(status_event, 'status_event')
channel.configure_mock(exit_status=exit_code)
is_set = mock.Mock(return_value=True)
channel.status_event.attach_mock(is_set, 'is_set')
return (
open_session, transport, channel, get_transport,
open_channel, intermediate_channel
)
def test_execute_through_host_no_creds(
self, transp, client, policy, logger):
target = '127.0.0.2'
exit_code = 0
# noinspection PyTypeChecker
return_value = exec_result.ExecResult(
cmd=command,
stderr=[b' \n', b'0\n', b'1\n', b' \n'],
stdout=[b' \n', b'2\n', b'3\n', b' \n'],
exit_code=exit_code
)
(
open_session,
transport,
channel,
get_transport,
open_channel,
intermediate_channel
) = self.prepare_execute_through_host(
transp=transp,
client=client,
exit_code=exit_code)
# noinspection PyTypeChecker
ssh = ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
# noinspection PyTypeChecker
result = ssh.execute_through_host(target, command)
self.assertEqual(result, return_value)
get_transport.assert_called_once()
open_channel.assert_called_once()
transp.assert_called_once_with(intermediate_channel)
open_session.assert_called_once()
transport.assert_has_calls((
mock.call.connect(username=username, password=password, pkey=None),
mock.call.open_session()
))
channel.assert_has_calls((
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('ls ~ '),
mock.call.recv_ready(),
mock.call.recv_stderr_ready(),
mock.call.status_event.is_set(),
mock.call.close()
))
def test_execute_through_host_auth(
self, transp, client, policy, logger):
_login = 'cirros'
_password = 'cubswin:)'
target = '127.0.0.2'
exit_code = 0
# noinspection PyTypeChecker
return_value = exec_result.ExecResult(
cmd=command,
stderr=[b' \n', b'0\n', b'1\n', b' \n'],
stdout=[b' \n', b'2\n', b'3\n', b' \n'],
exit_code=exit_code
)
(
open_session, transport, channel, get_transport,
open_channel, intermediate_channel
) = self.prepare_execute_through_host(
transp, client, exit_code=exit_code)
# noinspection PyTypeChecker
ssh = ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
# noinspection PyTypeChecker
result = ssh.execute_through_host(
target, command,
auth=ssh_client.SSHAuth(username=_login, password=_password))
self.assertEqual(result, return_value)
get_transport.assert_called_once()
open_channel.assert_called_once()
transp.assert_called_once_with(intermediate_channel)
open_session.assert_called_once()
transport.assert_has_calls((
mock.call.connect(username=_login, password=_password, pkey=None),
mock.call.open_session()
))
channel.assert_has_calls((
mock.call.makefile('rb'),
mock.call.makefile_stderr('rb'),
mock.call.exec_command('ls ~ '),
mock.call.recv_ready(),
mock.call.recv_stderr_ready(),
mock.call.status_event.is_set(),
mock.call.close()
))
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
@mock.patch(
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
@mock.patch('paramiko.SSHClient', autospec=True)
class TestSftp(unittest.TestCase):
def tearDown(self):
ssh_client.SSHClient._clear_cache()
@staticmethod
def prepare_sftp_file_tests(client):
_ssh = mock.Mock()
client.return_value = _ssh
_sftp = mock.Mock()
open_sftp = mock.Mock(parent=_ssh, return_value=_sftp)
_ssh.attach_mock(open_sftp, 'open_sftp')
# noinspection PyTypeChecker
ssh = ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
return ssh, _sftp
def test_exists(self, client, policy, logger):
ssh, _sftp = self.prepare_sftp_file_tests(client)
lstat = mock.Mock()
_sftp.attach_mock(lstat, 'lstat')
dst = '/etc'
# noinspection PyTypeChecker
result = ssh.exists(dst)
self.assertTrue(result)
lstat.assert_called_once_with(dst)
# Negative scenario
lstat.reset_mock()
lstat.side_effect = IOError
# noinspection PyTypeChecker
result = ssh.exists(dst)
self.assertFalse(result)
lstat.assert_called_once_with(dst)
def test_stat(self, client, policy, logger):
ssh, _sftp = self.prepare_sftp_file_tests(client)
stat = mock.Mock()
_sftp.attach_mock(stat, 'stat')
stat.return_value = paramiko.sftp_attr.SFTPAttributes()
stat.return_value.st_size = 0
stat.return_value.st_uid = 0
stat.return_value.st_gid = 0
dst = '/etc/passwd'
# noinspection PyTypeChecker
result = ssh.stat(dst)
self.assertEqual(result.st_size, 0)
self.assertEqual(result.st_uid, 0)
self.assertEqual(result.st_gid, 0)
def test_isfile(self, client, policy, logger):
class Attrs(object):
def __init__(self, mode):
self.st_mode = mode
ssh, _sftp = self.prepare_sftp_file_tests(client)
lstat = mock.Mock()
_sftp.attach_mock(lstat, 'lstat')
lstat.return_value = Attrs(stat.S_IFREG)
dst = '/etc/passwd'
# noinspection PyTypeChecker
result = ssh.isfile(dst)
self.assertTrue(result)
lstat.assert_called_once_with(dst)
# Negative scenario
lstat.reset_mock()
lstat.return_value = Attrs(stat.S_IFDIR)
# noinspection PyTypeChecker
result = ssh.isfile(dst)
self.assertFalse(result)
lstat.assert_called_once_with(dst)
lstat.reset_mock()
lstat.side_effect = IOError
# noinspection PyTypeChecker
result = ssh.isfile(dst)
self.assertFalse(result)
lstat.assert_called_once_with(dst)
def test_isdir(self, client, policy, logger):
class Attrs(object):
def __init__(self, mode):
self.st_mode = mode
ssh, _sftp = self.prepare_sftp_file_tests(client)
lstat = mock.Mock()
_sftp.attach_mock(lstat, 'lstat')
lstat.return_value = Attrs(stat.S_IFDIR)
dst = '/etc/passwd'
# noinspection PyTypeChecker
result = ssh.isdir(dst)
self.assertTrue(result)
lstat.assert_called_once_with(dst)
# Negative scenario
lstat.reset_mock()
lstat.return_value = Attrs(stat.S_IFREG)
# noinspection PyTypeChecker
result = ssh.isdir(dst)
self.assertFalse(result)
lstat.assert_called_once_with(dst)
lstat.reset_mock()
lstat.side_effect = IOError
# noinspection PyTypeChecker
result = ssh.isdir(dst)
self.assertFalse(result)
lstat.assert_called_once_with(dst)
@mock.patch('devops.helpers.ssh_client.SSHClient.exists')
@mock.patch('devops.helpers.ssh_client.SSHClient.execute')
def test_mkdir(self, execute, exists, client, policy, logger):
exists.side_effect = [False, True]
dst = '~/tst'
# noinspection PyTypeChecker
ssh = ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
# Path not exists
# noinspection PyTypeChecker
ssh.mkdir(dst)
exists.assert_called_once_with(dst)
execute.assert_called_once_with("mkdir -p {}\n".format(dst))
# Path exists
exists.reset_mock()
execute.reset_mock()
# noinspection PyTypeChecker
ssh.mkdir(dst)
exists.assert_called_once_with(dst)
execute.assert_not_called()
@mock.patch('devops.helpers.ssh_client.SSHClient.execute')
def test_rm_rf(self, execute, client, policy, logger):
dst = '~/tst'
# noinspection PyTypeChecker
ssh = ssh_client.SSHClient(
host=host,
port=port,
auth=ssh_client.SSHAuth(
username=username,
password=password
))
# Path not exists
# noinspection PyTypeChecker
ssh.rm_rf(dst)
execute.assert_called_once_with("rm -rf {}".format(dst))
def test_open(self, client, policy, logger):
ssh, _sftp = self.prepare_sftp_file_tests(client)
fopen = mock.Mock(return_value=True)
_sftp.attach_mock(fopen, 'open')
dst = '/etc/passwd'
mode = 'r'
# noinspection PyTypeChecker
result = ssh.open(dst)
fopen.assert_called_once_with(dst, mode)
self.assertTrue(result)
@mock.patch('devops.helpers.ssh_client.SSHClient.exists')
@mock.patch('os.path.exists', autospec=True)
@mock.patch('devops.helpers.ssh_client.SSHClient.isdir')
@mock.patch('os.path.isdir', autospec=True)
def test_download(
self,
isdir, remote_isdir, exists, remote_exists, client, policy, logger
):
ssh, _sftp = self.prepare_sftp_file_tests(client)
isdir.return_value = True
exists.side_effect = [True, False, False]
remote_isdir.side_effect = [False, False, True]
remote_exists.side_effect = [True, False, False]
dst = '/etc/environment'
target = '/tmp/environment'
# noinspection PyTypeChecker
result = ssh.download(destination=dst, target=target)
self.assertTrue(result)
isdir.assert_called_once_with(target)
exists.assert_called_once_with(posixpath.join(
target, path.basename(dst)))
remote_isdir.assert_called_once_with(dst)
remote_exists.assert_called_once_with(dst)
_sftp.assert_has_calls((
mock.call.get(dst, posixpath.join(target, path.basename(dst))),
))
# Negative scenarios
logger.reset_mock()
# noinspection PyTypeChecker
result = ssh.download(destination=dst, target=target)
logger.assert_has_calls((
mock.call.debug(
"Copying '%s' -> '%s' from remote to local host",
'/etc/environment',
'/tmp/environment'),
mock.call.debug(
"Can't download %s because it doesn't exist",
'/etc/environment'
),
))
self.assertFalse(result)
logger.reset_mock()
# noinspection PyTypeChecker
ssh.download(destination=dst, target=target)
logger.assert_has_calls((
mock.call.debug(
"Copying '%s' -> '%s' from remote to local host",
'/etc/environment',
'/tmp/environment'),
mock.call.debug(
"Can't download %s because it is a directory",
'/etc/environment'
),
))
@mock.patch('devops.helpers.ssh_client.SSHClient.isdir')
@mock.patch('os.path.isdir', autospec=True)
def test_upload_file(
self, isdir, remote_isdir, client, policy, logger
):
ssh, _sftp = self.prepare_sftp_file_tests(client)
isdir.return_value = False
remote_isdir.return_value = False
target = '/etc/environment'
source = '/tmp/environment'
# noinspection PyTypeChecker
ssh.upload(source=source, target=target)
isdir.assert_called_once_with(source)
remote_isdir.assert_called_once_with(target)
_sftp.assert_has_calls((
mock.call.put(source, target),
))
@mock.patch('devops.helpers.ssh_client.SSHClient.exists')
@mock.patch('devops.helpers.ssh_client.SSHClient.mkdir')
@mock.patch('os.walk')
@mock.patch('devops.helpers.ssh_client.SSHClient.isdir')
@mock.patch('os.path.isdir', autospec=True)
def test_upload_dir(
self,
isdir, remote_isdir, walk, mkdir, exists,
client, policy, logger
):
ssh, _sftp = self.prepare_sftp_file_tests(client)
isdir.return_value = True
remote_isdir.return_value = True
exists.return_value = True
target = '/etc'
source = '/tmp/bash'
filename = 'bashrc'
walk.return_value = (source, '', [filename]),
expected_path = posixpath.join(target, path.basename(source))
expected_file = posixpath.join(expected_path, filename)
# noinspection PyTypeChecker
ssh.upload(source=source, target=target)
isdir.assert_called_once_with(source)
remote_isdir.assert_called_once_with(target)
mkdir.assert_called_once_with(expected_path)
exists.assert_called_once_with(expected_file)
_sftp.assert_has_calls((
mock.call.unlink(expected_file),
mock.call.put(posixpath.join(source, filename), expected_file),
))