SSHClient rework for SSH Manager integration
1. Implemented SSHAuth 2. Old initialization API has been marked as deprecated 3. SFTP is started on demand with 3 retries 4. Reworked unit test to cover 100% 5. Added docstrings 6. Remove cyclic SSH session initialization in helper 7. Code is ready for adopted memorize pattern blueprint: sshmanager-integration Change-Id: I49d0aa635ba3f3125ab17531c0790a0106b87fea
This commit is contained in:
parent
621b1bdc5f
commit
aae58e25da
|
@ -37,6 +37,7 @@ from six.moves import xmlrpc_client
|
||||||
from devops.error import AuthenticationError
|
from devops.error import AuthenticationError
|
||||||
from devops.error import DevopsError
|
from devops.error import DevopsError
|
||||||
from devops.error import TimeoutError
|
from devops.error import TimeoutError
|
||||||
|
from devops.helpers.ssh_client import SSHAuth
|
||||||
from devops.helpers.ssh_client import SSHClient
|
from devops.helpers.ssh_client import SSHClient
|
||||||
from devops import logger
|
from devops import logger
|
||||||
from devops.settings import KEYSTONE_CREDS
|
from devops.settings import KEYSTONE_CREDS
|
||||||
|
@ -151,8 +152,9 @@ def wait_ssh_cmd(
|
||||||
password=SSH_CREDENTIALS['password'],
|
password=SSH_CREDENTIALS['password'],
|
||||||
timeout=0):
|
timeout=0):
|
||||||
ssh_client = SSHClient(host=host, port=port,
|
ssh_client = SSHClient(host=host, port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password)
|
password=password))
|
||||||
wait(lambda: not ssh_client.execute(check_cmd)['exit_code'],
|
wait(lambda: not ssh_client.execute(check_cmd)['exit_code'],
|
||||||
timeout=timeout)
|
timeout=timeout)
|
||||||
|
|
||||||
|
@ -198,10 +200,12 @@ def get_node_remote(env, node_name, login=SSH_SLAVE_CREDENTIALS['login'],
|
||||||
name=node_name).interfaces[0].mac_address)
|
name=node_name).interfaces[0].mac_address)
|
||||||
wait(lambda: tcp_ping(ip, 22), timeout=180,
|
wait(lambda: tcp_ping(ip, 22), timeout=180,
|
||||||
timeout_msg="Node {ip} is not accessible by SSH.".format(ip=ip))
|
timeout_msg="Node {ip} is not accessible by SSH.".format(ip=ip))
|
||||||
return SSHClient(ip,
|
return SSHClient(
|
||||||
|
ip,
|
||||||
|
auth=SSHAuth(
|
||||||
username=login,
|
username=login,
|
||||||
password=password,
|
password=password,
|
||||||
private_keys=get_private_keys(env))
|
keys=get_private_keys(env)))
|
||||||
|
|
||||||
|
|
||||||
def get_admin_ip(env):
|
def get_admin_ip(env):
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import stat
|
import stat
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
import six
|
import six
|
||||||
|
@ -24,7 +25,163 @@ from devops.helpers.retry import retry
|
||||||
from devops import logger
|
from devops import logger
|
||||||
|
|
||||||
|
|
||||||
|
class SSHAuth(object):
|
||||||
|
__slots__ = ['__username', '__password', '__key', '__keys']
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
username=None, password=None, key=None, keys=None):
|
||||||
|
"""SSH authorisation object
|
||||||
|
|
||||||
|
Used to authorize SSHClient.
|
||||||
|
Single SSHAuth object is associated with single host:port.
|
||||||
|
Password and key is private, other data is read-only.
|
||||||
|
|
||||||
|
:type username: str
|
||||||
|
:type password: str
|
||||||
|
:type key: paramiko.RSAKey
|
||||||
|
:type keys: list
|
||||||
|
"""
|
||||||
|
self.__username = username
|
||||||
|
self.__password = password
|
||||||
|
self.__key = key
|
||||||
|
self.__keys = [None]
|
||||||
|
if key is not None:
|
||||||
|
self.__keys.append(key)
|
||||||
|
if keys is not None:
|
||||||
|
for key in keys:
|
||||||
|
if key not in self.__keys:
|
||||||
|
self.__keys.append(key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
"""Username for auth
|
||||||
|
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self.__username
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_public_key(key):
|
||||||
|
"""Internal method for get public key from private
|
||||||
|
|
||||||
|
:type key: paramiko.RSAKey
|
||||||
|
"""
|
||||||
|
if key is None:
|
||||||
|
return None
|
||||||
|
return '{0} {1}'.format(key.get_name(), key.get_base64())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self):
|
||||||
|
"""public key for stored private key if presents else None
|
||||||
|
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self.__get_public_key(self.__key)
|
||||||
|
|
||||||
|
def enter_password(self, tgt):
|
||||||
|
"""Enter password to STDIN
|
||||||
|
|
||||||
|
Note: required for 'sudo' call
|
||||||
|
|
||||||
|
:type tgt: file
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return tgt.write('{}\n'.format(self.__password))
|
||||||
|
|
||||||
|
def connect(self, client, hostname=None, port=22, log=True):
|
||||||
|
"""Connect SSH client object using credentials
|
||||||
|
|
||||||
|
:type client:
|
||||||
|
paramiko.client.SSHClient
|
||||||
|
paramiko.transport.Transport
|
||||||
|
:type log: bool
|
||||||
|
:raises paramiko.AuthenticationException
|
||||||
|
"""
|
||||||
|
kwargs = {
|
||||||
|
'username': self.username,
|
||||||
|
'password': self.__password}
|
||||||
|
if hostname is not None:
|
||||||
|
kwargs['hostname'] = hostname
|
||||||
|
kwargs['port'] = port
|
||||||
|
|
||||||
|
keys = [self.__key]
|
||||||
|
keys.extend([k for k in self.__keys if k != self.__key])
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
kwargs['pkey'] = key
|
||||||
|
try:
|
||||||
|
client.connect(**kwargs)
|
||||||
|
if self.__key != key:
|
||||||
|
self.__key = key
|
||||||
|
logger.debug(
|
||||||
|
'Main key has been updated, public key is: \n'
|
||||||
|
'{}'.format(self.public_key))
|
||||||
|
return
|
||||||
|
except paramiko.PasswordRequiredException:
|
||||||
|
if self.__password is None:
|
||||||
|
logger.exception('No password has been set!')
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
logger.critical(
|
||||||
|
'Unexpected PasswordRequiredException, '
|
||||||
|
'when password is set!')
|
||||||
|
raise
|
||||||
|
except paramiko.AuthenticationException:
|
||||||
|
continue
|
||||||
|
msg = 'Connection using stored authentication info failed!'
|
||||||
|
if log:
|
||||||
|
logger.exception(
|
||||||
|
'Connection using stored authentication info failed!')
|
||||||
|
raise paramiko.AuthenticationException(msg)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.__class__,
|
||||||
|
self.username,
|
||||||
|
self.__password,
|
||||||
|
tuple(self.__keys)
|
||||||
|
))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
_key = (
|
||||||
|
None if self.__key is None else
|
||||||
|
'<private for pub: {}>'.format(self.public_key)
|
||||||
|
)
|
||||||
|
_keys = []
|
||||||
|
for k in self.__keys:
|
||||||
|
if k == self.__key:
|
||||||
|
continue
|
||||||
|
_keys.append(
|
||||||
|
'<private for pub: {}>'.format(
|
||||||
|
self.__get_public_key(key=k)) if k is not None else None)
|
||||||
|
|
||||||
|
return (
|
||||||
|
'{cls}(username={username}, '
|
||||||
|
'password=<*masked*>, key={key}, keys={keys})'.format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
username=self.username,
|
||||||
|
key=_key,
|
||||||
|
keys=_keys)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
'{cls} for {username}'.format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
username=self.username,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SSHClient(object):
|
class SSHClient(object):
|
||||||
|
__slots__ = [
|
||||||
|
'__hostname', '__port', '__auth', '__ssh', '__sftp', 'sudo_mode'
|
||||||
|
]
|
||||||
|
|
||||||
class get_sudo(object):
|
class get_sudo(object):
|
||||||
"""Context manager for call commands with sudo"""
|
"""Context manager for call commands with sudo"""
|
||||||
def __init__(self, ssh):
|
def __init__(self, ssh):
|
||||||
|
@ -36,42 +193,128 @@ class SSHClient(object):
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
self.ssh.sudo_mode = False
|
self.ssh.sudo_mode = False
|
||||||
|
|
||||||
def __init__(self, host, port=22, username=None, password=None,
|
def __hash__(self):
|
||||||
private_keys=None):
|
return hash((
|
||||||
self.host = str(host)
|
self.__class__,
|
||||||
self.port = int(port)
|
self.hostname,
|
||||||
self.username = username
|
self.port,
|
||||||
self.__password = password
|
self.auth))
|
||||||
if not private_keys:
|
|
||||||
private_keys = []
|
def __init__(
|
||||||
self.__private_keys = private_keys
|
self,
|
||||||
self.__actual_pkey = None
|
host, port=22,
|
||||||
|
username=None, password=None, private_keys=None,
|
||||||
|
auth=None
|
||||||
|
):
|
||||||
|
"""SSHClient helper
|
||||||
|
|
||||||
|
:type host: str
|
||||||
|
:type port: int
|
||||||
|
:type username: str
|
||||||
|
:type password: str
|
||||||
|
:type private_keys: list
|
||||||
|
:type auth: SSHAuth
|
||||||
|
"""
|
||||||
|
self.__hostname = host
|
||||||
|
self.__port = port
|
||||||
|
|
||||||
self.sudo_mode = False
|
self.sudo_mode = False
|
||||||
self.sudo = self.get_sudo(self)
|
self.__ssh = paramiko.SSHClient()
|
||||||
self._ssh = None
|
self.__ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
self.__sftp = None
|
self.__sftp = None
|
||||||
|
|
||||||
self.reconnect()
|
self.__auth = auth
|
||||||
|
|
||||||
|
if auth is None:
|
||||||
|
msg = (
|
||||||
|
'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
|
||||||
|
))
|
||||||
|
warn(msg, DeprecationWarning)
|
||||||
|
logger.debug(msg)
|
||||||
|
|
||||||
|
self.__auth = SSHAuth(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
keys=private_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__connect()
|
||||||
|
if auth is None:
|
||||||
|
logger.info(
|
||||||
|
'{0}:{1}> SSHAuth was made from old style creds: '
|
||||||
|
'{2}'.format(self.hostname, self.port, self.auth))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def password(self):
|
def auth(self):
|
||||||
return self.__password
|
"""Internal authorisation object
|
||||||
|
|
||||||
|
Attention: this public property is mainly for inheritance,
|
||||||
|
debug and information purposes.
|
||||||
|
Calls outside SSHClient and child classes is sign of incorrect design.
|
||||||
|
Change is completely disallowed.
|
||||||
|
|
||||||
|
:rtype: SSHAuth
|
||||||
|
"""
|
||||||
|
return self.__auth
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_keys(self):
|
def hostname(self):
|
||||||
return self.__private_keys
|
"""Connected remote host name
|
||||||
|
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self.__hostname
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key(self):
|
def port(self):
|
||||||
return self.__actual_pkey
|
"""Connected remote port number
|
||||||
|
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
return self.__port
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '{cls}(host={host}, port={port}, auth={auth!r})'.format(
|
||||||
|
cls=self.__class__.__name__, host=self.hostname, port=self.port,
|
||||||
|
auth=self.auth
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{cls}(host={host}, port={port}) for user {user}'.format(
|
||||||
|
cls=self.__class__.__name__, host=self.hostname, port=self.port,
|
||||||
|
user=self.auth.username
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_key(self):
|
def _ssh(self):
|
||||||
if self.private_key is None:
|
"""ssh client object getter for inheritance support only
|
||||||
return None
|
|
||||||
key = self.private_key
|
Attention: ssh client object creation and change
|
||||||
return '{0} {1}'.format(key.get_name(), key.get_base64())
|
is allowed only by __init__ and reconnect call.
|
||||||
|
|
||||||
|
:rtype: paramiko.SSHClient
|
||||||
|
"""
|
||||||
|
return self.__ssh
|
||||||
|
|
||||||
|
@retry(count=3, delay=3)
|
||||||
|
def __connect(self):
|
||||||
|
"""Main method for connection open"""
|
||||||
|
self.auth.connect(
|
||||||
|
client=self.__ssh,
|
||||||
|
hostname=self.hostname, port=self.port,
|
||||||
|
log=True)
|
||||||
|
|
||||||
|
@retry(3, delay=0)
|
||||||
|
def __connect_sftp(self):
|
||||||
|
"""SFTP connection opener"""
|
||||||
|
try:
|
||||||
|
self.__sftp = self.__ssh.open_sftp()
|
||||||
|
except paramiko.SSHException:
|
||||||
|
logger.warning('SFTP enable failed! SSH only is accessible.')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _sftp(self):
|
def _sftp(self):
|
||||||
|
@ -81,25 +324,28 @@ class SSHClient(object):
|
||||||
"""
|
"""
|
||||||
if self.__sftp is not None:
|
if self.__sftp is not None:
|
||||||
return self.__sftp
|
return self.__sftp
|
||||||
logger.warning('SFTP is not connected, try to reconnect')
|
logger.debug('SFTP is not connected, try to connect...')
|
||||||
self._connect_sftp()
|
self.__connect_sftp()
|
||||||
if self.__sftp is not None:
|
if self.__sftp is not None:
|
||||||
return self.__sftp
|
return self.__sftp
|
||||||
raise paramiko.SSHException('SFTP connection failed')
|
raise paramiko.SSHException('SFTP connection failed')
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Clear SSH and SFTP sessions"""
|
||||||
|
try:
|
||||||
|
self.__ssh.close()
|
||||||
|
self.__sftp = None
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Could not close ssh connection")
|
||||||
if self.__sftp is not None:
|
if self.__sftp is not None:
|
||||||
try:
|
try:
|
||||||
self.__sftp.close()
|
self.__sftp.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Could not close sftp connection")
|
logger.exception("Could not close sftp connection")
|
||||||
try:
|
|
||||||
self._ssh.close()
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Could not close ssh connection")
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.clear()
|
self.__ssh.close()
|
||||||
|
self.__sftp = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
@ -107,39 +353,14 @@ class SSHClient(object):
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
@retry(count=3, delay=3)
|
|
||||||
def connect(self):
|
|
||||||
logger.debug(
|
|
||||||
"Connect to '{0}:{1}' as '{2}:{3}'".format(
|
|
||||||
self.host, self.port, self.username, self.password))
|
|
||||||
for private_key in self.private_keys:
|
|
||||||
try:
|
|
||||||
self._ssh.connect(
|
|
||||||
self.host, port=self.port, username=self.username,
|
|
||||||
password=self.password, pkey=private_key)
|
|
||||||
self.__actual_pkey = private_key
|
|
||||||
return
|
|
||||||
except paramiko.AuthenticationException:
|
|
||||||
continue
|
|
||||||
if self.private_keys:
|
|
||||||
logger.error("Authentication with keys failed")
|
|
||||||
|
|
||||||
self.__actual_pkey = None
|
|
||||||
self._ssh.connect(
|
|
||||||
self.host, port=self.port, username=self.username,
|
|
||||||
password=self.password)
|
|
||||||
|
|
||||||
def _connect_sftp(self):
|
|
||||||
try:
|
|
||||||
self.__sftp = self._ssh.open_sftp()
|
|
||||||
except paramiko.SSHException:
|
|
||||||
logger.warning('SFTP enable failed! SSH only is accessible.')
|
|
||||||
|
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
self._ssh = paramiko.SSHClient()
|
"""Reconnect SSH and SFTP session"""
|
||||||
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
self.clear()
|
||||||
self.connect()
|
|
||||||
self._connect_sftp()
|
self.__ssh = paramiko.SSHClient()
|
||||||
|
self.__ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
self.__connect()
|
||||||
|
|
||||||
def check_call(
|
def check_call(
|
||||||
self,
|
self,
|
||||||
|
@ -232,7 +453,7 @@ class SSHClient(object):
|
||||||
ret = chan.recv_exit_status()
|
ret = chan.recv_exit_status()
|
||||||
chan.close()
|
chan.close()
|
||||||
if ret not in expected:
|
if ret not in expected:
|
||||||
errors[remote.host] = ret
|
errors[remote.hostname] = ret
|
||||||
if errors and raise_on_err:
|
if errors and raise_on_err:
|
||||||
raise DevopsCalledProcessError(command, errors)
|
raise DevopsCalledProcessError(command, errors)
|
||||||
|
|
||||||
|
@ -301,7 +522,7 @@ class SSHClient(object):
|
||||||
cmd = 'sudo -S bash -c "%s"' % cmd.replace('"', '\\"')
|
cmd = 'sudo -S bash -c "%s"' % cmd.replace('"', '\\"')
|
||||||
chan.exec_command(cmd)
|
chan.exec_command(cmd)
|
||||||
if stdout.channel.closed is False:
|
if stdout.channel.closed is False:
|
||||||
stdin.write('%s\n' % self.password)
|
self.auth.enter_password(stdin)
|
||||||
stdin.flush()
|
stdin.flush()
|
||||||
else:
|
else:
|
||||||
chan.exec_command(cmd)
|
chan.exec_command(cmd)
|
||||||
|
@ -311,23 +532,27 @@ class SSHClient(object):
|
||||||
self,
|
self,
|
||||||
hostname,
|
hostname,
|
||||||
cmd,
|
cmd,
|
||||||
username=None,
|
auth=None,
|
||||||
password=None,
|
|
||||||
key=None,
|
|
||||||
target_port=22):
|
target_port=22):
|
||||||
if username is None and password is None and key is None:
|
"""Execute command on remote host through currently connected host
|
||||||
username = self.username
|
|
||||||
password = self.__password
|
:type hostname: str
|
||||||
key = self.private_key
|
:type cmd: str
|
||||||
|
:type auth: SSHAuth
|
||||||
|
:type target_port: int
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
if auth is None:
|
||||||
|
auth = self.auth
|
||||||
|
|
||||||
intermediate_channel = self._ssh.get_transport().open_channel(
|
intermediate_channel = self._ssh.get_transport().open_channel(
|
||||||
kind='direct-tcpip',
|
kind='direct-tcpip',
|
||||||
dest_addr=(hostname, target_port),
|
dest_addr=(hostname, target_port),
|
||||||
src_addr=(self.host, 0))
|
src_addr=(self.hostname, 0))
|
||||||
transport = paramiko.Transport(sock=intermediate_channel)
|
transport = paramiko.Transport(sock=intermediate_channel)
|
||||||
|
|
||||||
# start client and authenticate transport
|
# start client and authenticate transport
|
||||||
transport.connect(username=username, password=password, pkey=key)
|
auth.connect(transport)
|
||||||
|
|
||||||
# open ssh session
|
# open ssh session
|
||||||
channel = transport.open_session()
|
channel = transport.open_session()
|
||||||
|
@ -336,7 +561,6 @@ class SSHClient(object):
|
||||||
stdout = channel.makefile('rb')
|
stdout = channel.makefile('rb')
|
||||||
stderr = channel.makefile_stderr('rb')
|
stderr = channel.makefile_stderr('rb')
|
||||||
|
|
||||||
logger.info("Executing command: {}".format(cmd))
|
|
||||||
channel.exec_command(cmd)
|
channel.exec_command(cmd)
|
||||||
|
|
||||||
# TODO(astepanov): make a logic for controlling channel state
|
# TODO(astepanov): make a logic for controlling channel state
|
||||||
|
@ -474,3 +698,5 @@ class SSHClient(object):
|
||||||
return attrs.st_mode & stat.S_IFDIR != 0
|
return attrs.st_mode & stat.S_IFDIR != 0
|
||||||
except IOError:
|
except IOError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
__all__ = ['SSHAuth', 'SSHClient']
|
||||||
|
|
|
@ -26,6 +26,7 @@ from devops.error import DevopsEnvironmentError
|
||||||
from devops.error import DevopsError
|
from devops.error import DevopsError
|
||||||
from devops.error import DevopsObjNotFound
|
from devops.error import DevopsObjNotFound
|
||||||
from devops.helpers.network import IpNetworksPool
|
from devops.helpers.network import IpNetworksPool
|
||||||
|
from devops.helpers.ssh_client import SSHAuth
|
||||||
from devops.helpers.ssh_client import SSHClient
|
from devops.helpers.ssh_client import SSHClient
|
||||||
from devops.helpers.templates import create_devops_config
|
from devops.helpers.templates import create_devops_config
|
||||||
from devops.helpers.templates import get_devops_config
|
from devops.helpers.templates import get_devops_config
|
||||||
|
@ -362,30 +363,36 @@ class Environment(BaseModel):
|
||||||
key=lambda node: node.name
|
key=lambda node: node.name
|
||||||
)[0]
|
)[0]
|
||||||
return admin.remote(
|
return admin.remote(
|
||||||
self.admin_net,
|
self.admin_net, auth=SSHAuth(
|
||||||
login=login,
|
username=login,
|
||||||
password=password)
|
password=password))
|
||||||
|
|
||||||
# LEGACY, for fuel-qa compatibility
|
# LEGACY, for fuel-qa compatibility
|
||||||
# @logwrap
|
# @logwrap
|
||||||
def get_ssh_to_remote(self, ip,
|
def get_ssh_to_remote(self, ip,
|
||||||
login=settings.SSH_SLAVE_CREDENTIALS['login'],
|
login=settings.SSH_SLAVE_CREDENTIALS['login'],
|
||||||
password=settings.SSH_SLAVE_CREDENTIALS['password']):
|
password=settings.SSH_SLAVE_CREDENTIALS['password']):
|
||||||
|
warn('LEGACY, for fuel-qa compatibility', DeprecationWarning)
|
||||||
keys = []
|
keys = []
|
||||||
|
remote = self.get_admin_remote()
|
||||||
for key_string in ['/root/.ssh/id_rsa',
|
for key_string in ['/root/.ssh/id_rsa',
|
||||||
'/root/.ssh/bootstrap.rsa']:
|
'/root/.ssh/bootstrap.rsa']:
|
||||||
if self.get_admin_remote().isfile(key_string):
|
if remote.isfile(key_string):
|
||||||
with self.get_admin_remote().open(key_string) as f:
|
with remote.open(key_string) as f:
|
||||||
keys.append(RSAKey.from_private_key(f))
|
keys.append(RSAKey.from_private_key(f))
|
||||||
|
|
||||||
return SSHClient(ip,
|
return SSHClient(
|
||||||
|
ip,
|
||||||
|
auth=SSHAuth(
|
||||||
username=login,
|
username=login,
|
||||||
password=password,
|
password=password,
|
||||||
private_keys=keys)
|
keys=keys))
|
||||||
|
|
||||||
# LEGACY, for fuel-qa compatibility
|
# LEGACY, for fuel-qa compatibility
|
||||||
# @logwrap
|
# @logwrap
|
||||||
def get_ssh_to_remote_by_key(self, ip, keyfile):
|
@staticmethod
|
||||||
|
def get_ssh_to_remote_by_key(ip, keyfile):
|
||||||
|
warn('LEGACY, for fuel-qa compatibility', DeprecationWarning)
|
||||||
try:
|
try:
|
||||||
with open(keyfile) as f:
|
with open(keyfile) as f:
|
||||||
keys = [RSAKey.from_private_key(f)]
|
keys = [RSAKey.from_private_key(f)]
|
||||||
|
@ -393,7 +400,7 @@ class Environment(BaseModel):
|
||||||
logger.warning('Loading of SSH key from file failed. Trying to use'
|
logger.warning('Loading of SSH key from file failed. Trying to use'
|
||||||
' SSH agent ...')
|
' SSH agent ...')
|
||||||
keys = Agent().get_keys()
|
keys = Agent().get_keys()
|
||||||
return SSHClient(ip, private_keys=keys)
|
return SSHClient(ip, auth=SSHAuth(keys=keys))
|
||||||
|
|
||||||
# LEGACY, TO REMOVE (for fuel-qa compatibility)
|
# LEGACY, TO REMOVE (for fuel-qa compatibility)
|
||||||
def nodes(self): # migrated from EnvironmentModel.nodes()
|
def nodes(self): # migrated from EnvironmentModel.nodes()
|
||||||
|
|
|
@ -270,7 +270,9 @@ class Node(six.with_metaclass(ExtendableNodeType, ParamedModel, BaseModel)):
|
||||||
interface = self.get_interface_by_nailgun_network_name(name)
|
interface = self.get_interface_by_nailgun_network_name(name)
|
||||||
return interface.address_set.first().ip_address
|
return interface.address_set.first().ip_address
|
||||||
|
|
||||||
def remote(self, network_name, login, password=None, private_keys=None):
|
def remote(
|
||||||
|
self, network_name, login=None, password=None, private_keys=None,
|
||||||
|
auth=None):
|
||||||
"""Create SSH-connection to the network
|
"""Create SSH-connection to the network
|
||||||
|
|
||||||
:rtype : SSHClient
|
:rtype : SSHClient
|
||||||
|
@ -278,7 +280,7 @@ class Node(six.with_metaclass(ExtendableNodeType, ParamedModel, BaseModel)):
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
self.get_ip_address_by_network_name(network_name),
|
self.get_ip_address_by_network_name(network_name),
|
||||||
username=login,
|
username=login,
|
||||||
password=password, private_keys=private_keys)
|
password=password, private_keys=private_keys, auth=auth)
|
||||||
|
|
||||||
def await(self, network_name, timeout=120, by_port=22):
|
def await(self, network_name, timeout=120, by_port=22):
|
||||||
wait_pass(
|
wait_pass(
|
||||||
|
|
|
@ -28,6 +28,7 @@ from six.moves import xrange
|
||||||
|
|
||||||
from devops import error
|
from devops import error
|
||||||
from devops.helpers import helpers
|
from devops.helpers import helpers
|
||||||
|
from devops.helpers.ssh_client import SSHAuth
|
||||||
|
|
||||||
|
|
||||||
class TestHelpersHelpers(unittest.TestCase):
|
class TestHelpersHelpers(unittest.TestCase):
|
||||||
|
@ -198,7 +199,8 @@ class TestHelpersHelpers(unittest.TestCase):
|
||||||
helpers.wait_ssh_cmd(
|
helpers.wait_ssh_cmd(
|
||||||
host, port, check_cmd, username, password, timeout)
|
host, port, check_cmd, username, password, timeout)
|
||||||
ssh.assert_called_once_with(
|
ssh.assert_called_once_with(
|
||||||
host=host, port=port, username=username, password=password
|
host=host, port=port,
|
||||||
|
auth=SSHAuth(username=username, password=password)
|
||||||
)
|
)
|
||||||
wait.assert_called_once()
|
wait.assert_called_once()
|
||||||
# Todo: cover ssh_client.execute
|
# Todo: cover ssh_client.execute
|
||||||
|
@ -292,13 +294,12 @@ class TestHelpersHelpers(unittest.TestCase):
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
uri = 'http://127.0.0.1'
|
uri = 'http://127.0.0.1'
|
||||||
srv.return_value = Success()
|
srv.side_effect = [Success(), Success(), Fail()]
|
||||||
result = helpers.xmlrpcmethod(uri, 'success')
|
result = helpers.xmlrpcmethod(uri, 'success')
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
srv.assert_called_once_with(uri)
|
srv.assert_called_once_with(uri)
|
||||||
|
|
||||||
srv.reset_mock()
|
srv.reset_mock()
|
||||||
srv.return_value = Success()
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
AttributeError,
|
AttributeError,
|
||||||
helpers.xmlrpcmethod,
|
helpers.xmlrpcmethod,
|
||||||
|
@ -307,7 +308,6 @@ class TestHelpersHelpers(unittest.TestCase):
|
||||||
srv.assert_called_once_with(uri)
|
srv.assert_called_once_with(uri)
|
||||||
|
|
||||||
srv.reset_mock()
|
srv.reset_mock()
|
||||||
srv.return_value = Fail()
|
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
AttributeError,
|
AttributeError,
|
||||||
helpers.xmlrpcmethod,
|
helpers.xmlrpcmethod,
|
||||||
|
@ -323,11 +323,15 @@ class TestHelpersHelpers(unittest.TestCase):
|
||||||
rand.assert_called_once_with(5)
|
rand.assert_called_once_with(5)
|
||||||
|
|
||||||
def test_deepgetattr(self):
|
def test_deepgetattr(self):
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
class Tst(object):
|
class Tst(object):
|
||||||
one = 1
|
one = 1
|
||||||
|
|
||||||
tst = Tst()
|
tst = Tst()
|
||||||
tst2 = Tst()
|
tst2 = Tst()
|
||||||
tst2.two = Tst()
|
tst2.two = Tst()
|
||||||
|
# pylint: enable=attribute-defined-outside-init
|
||||||
|
|
||||||
result = helpers.deepgetattr(tst, 'one')
|
result = helpers.deepgetattr(tst, 'one')
|
||||||
self.assertEqual(result, 1)
|
self.assertEqual(result, 1)
|
||||||
result = helpers.deepgetattr(tst2, 'two.one')
|
result = helpers.deepgetattr(tst2, 'two.one')
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
import posixpath
|
import posixpath
|
||||||
import stat
|
import stat
|
||||||
|
@ -21,9 +22,12 @@ from unittest import TestCase
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import paramiko
|
import paramiko
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
from six.moves import cStringIO
|
||||||
from six import PY2
|
from six import PY2
|
||||||
|
|
||||||
from devops.error import DevopsCalledProcessError
|
from devops.error import DevopsCalledProcessError
|
||||||
|
from devops.helpers.ssh_client import SSHAuth
|
||||||
from devops.helpers.ssh_client import SSHClient
|
from devops.helpers.ssh_client import SSHClient
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,20 +52,136 @@ private_keys = []
|
||||||
command = 'ls ~ '
|
command = 'ls ~ '
|
||||||
|
|
||||||
|
|
||||||
|
class TestSSHAuth(TestCase):
|
||||||
|
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 = 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 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=SSHAuth.__name__,
|
||||||
|
username=auth.username,
|
||||||
|
key=_key,
|
||||||
|
keys=_keys
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
str(auth),
|
||||||
|
'{cls} for {username}'.format(
|
||||||
|
cls=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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('devops.helpers.retry.sleep', autospec=True)
|
||||||
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
|
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
|
'paramiko.AutoAddPolicy', autospec=True, return_value='AutoAddPolicy')
|
||||||
@mock.patch('paramiko.SSHClient', autospec=True)
|
@mock.patch('paramiko.SSHClient', autospec=True)
|
||||||
class TestSSHClient(TestCase):
|
class TestSSHClientInit(TestCase):
|
||||||
def check_defaults(
|
def init_checks(
|
||||||
self, obj, host, port, username, password, private_keys):
|
self,
|
||||||
self.assertEqual(obj.host, host)
|
client, policy, logger,
|
||||||
self.assertEqual(obj.port, port)
|
host=None, port=22,
|
||||||
self.assertEqual(obj.username, username)
|
username=None, password=None, private_keys=None,
|
||||||
self.assertEqual(obj.password, password)
|
auth=None
|
||||||
self.assertEqual(obj.private_keys, private_keys)
|
):
|
||||||
|
"""shared checks for positive cases
|
||||||
|
|
||||||
def test_init_passwd(self, client, policy, logger):
|
: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: SSHAuth
|
||||||
|
"""
|
||||||
_ssh = mock.call()
|
_ssh = mock.call()
|
||||||
|
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
|
@ -69,150 +189,572 @@ class TestSSHClient(TestCase):
|
||||||
port=port,
|
port=port,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
private_keys=private_keys)
|
private_keys=private_keys,
|
||||||
|
auth=auth
|
||||||
|
)
|
||||||
client.assert_called_once()
|
client.assert_called_once()
|
||||||
policy.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 = [
|
expected_calls = [
|
||||||
_ssh,
|
_ssh,
|
||||||
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
|
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
|
||||||
_ssh.connect(
|
_ssh.connect(
|
||||||
host, password=password,
|
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),
|
port=port, username=username),
|
||||||
_ssh.open_sftp()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertIn(expected_calls, client.mock_calls)
|
self.assertIn(expected_calls, client.mock_calls)
|
||||||
|
|
||||||
self.check_defaults(ssh, host, port, username, password, private_keys)
|
self.assertEqual(
|
||||||
self.assertIsNone(ssh.private_key)
|
ssh.auth,
|
||||||
self.assertIsNone(ssh.public_key)
|
SSHAuth(
|
||||||
|
username=username,
|
||||||
self.assertIn(
|
password=password,
|
||||||
mock.call.debug("Connect to '{0}:{1}' as '{2}:{3}'".format(
|
keys=private_keys
|
||||||
host, port, username, password
|
|
||||||
)),
|
|
||||||
logger.mock_calls
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(ssh.auth, auth)
|
||||||
|
|
||||||
sftp = ssh._sftp
|
sftp = ssh._sftp
|
||||||
self.assertEqual(sftp, client().open_sftp())
|
self.assertEqual(sftp, client().open_sftp())
|
||||||
|
|
||||||
def test_init_keys(self, client, policy, logger):
|
self.assertEqual(ssh._ssh, client())
|
||||||
_ssh = mock.call()
|
|
||||||
|
|
||||||
private_keys = gen_private_keys(1)
|
self.assertEqual(ssh.hostname, host)
|
||||||
|
self.assertEqual(ssh.port, port)
|
||||||
|
|
||||||
ssh = SSHClient(
|
self.assertEqual(
|
||||||
host=host,
|
repr(ssh),
|
||||||
port=port,
|
'{cls}(host={host}, port={port}, auth={auth!r})'.format(
|
||||||
username=username,
|
cls=ssh.__class__.__name__, host=ssh.hostname,
|
||||||
password=password,
|
port=ssh.port,
|
||||||
private_keys=private_keys)
|
auth=ssh.auth
|
||||||
|
)
|
||||||
client.assert_called_once()
|
|
||||||
policy.assert_called_once()
|
|
||||||
|
|
||||||
expected_calls = [
|
|
||||||
_ssh,
|
|
||||||
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
|
|
||||||
_ssh.connect(
|
|
||||||
host, password=password, pkey=private_keys[0],
|
|
||||||
port=port, username=username),
|
|
||||||
_ssh.open_sftp()
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertIn(expected_calls, client.mock_calls)
|
|
||||||
|
|
||||||
self.check_defaults(ssh, host, port, username, password, private_keys)
|
|
||||||
self.assertEqual(ssh.private_key, private_keys[0])
|
|
||||||
self.assertEqual(ssh.public_key, gen_public_key(private_keys[0]))
|
|
||||||
|
|
||||||
self.assertIn(
|
|
||||||
mock.call.debug("Connect to '{0}:{1}' as '{2}:{3}'".format(
|
|
||||||
host, port, username, password
|
|
||||||
)),
|
|
||||||
logger.mock_calls
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_init_as_context(self, client, policy, logger):
|
def test_init_host(self, client, policy, logger, sleep):
|
||||||
_ssh = mock.call()
|
"""Test with host only set"""
|
||||||
|
self.init_checks(
|
||||||
|
client, policy, logger,
|
||||||
|
host=host)
|
||||||
|
|
||||||
private_keys = gen_private_keys(1)
|
def test_init_alternate_port(self, client, policy, logger, sleep):
|
||||||
|
"""Test with alternate port"""
|
||||||
with SSHClient(
|
self.init_checks(
|
||||||
|
client, policy, logger,
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=2222
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
private_keys=private_keys) as ssh:
|
|
||||||
|
|
||||||
client.assert_called_once()
|
|
||||||
policy.assert_called_once()
|
|
||||||
|
|
||||||
expected_calls = [
|
|
||||||
_ssh,
|
|
||||||
_ssh.set_missing_host_key_policy('AutoAddPolicy'),
|
|
||||||
_ssh.connect(
|
|
||||||
host, password=password, pkey=private_keys[0],
|
|
||||||
port=port, username=username),
|
|
||||||
_ssh.open_sftp()
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertIn(expected_calls, client.mock_calls)
|
|
||||||
|
|
||||||
self.check_defaults(ssh, host, port, username, password,
|
|
||||||
private_keys)
|
|
||||||
|
|
||||||
self.assertIn(
|
|
||||||
mock.call.debug("Connect to '{0}:{1}' as '{2}:{3}'".format(
|
|
||||||
host, port, username, password
|
|
||||||
)),
|
|
||||||
logger.mock_calls
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_init_fail_sftp(self, client, policy, logger):
|
def test_init_username(self, client, policy, logger, sleep):
|
||||||
|
"""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, sleep):
|
||||||
|
"""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, sleep):
|
||||||
|
"""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, sleep):
|
||||||
|
"""Test with username and single key set from creds"""
|
||||||
|
connect = mock.Mock(
|
||||||
|
side_effect=[
|
||||||
|
paramiko.AuthenticationException, mock.Mock()
|
||||||
|
])
|
||||||
_ssh = mock.Mock()
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
client.return_value = _ssh
|
client.return_value = _ssh
|
||||||
open_sftp = mock.Mock(parent=_ssh, side_effect=paramiko.SSHException)
|
|
||||||
_ssh.attach_mock(open_sftp, 'open_sftp')
|
|
||||||
warning = mock.Mock(parent=logger)
|
|
||||||
logger.attach_mock(warning, 'warning')
|
|
||||||
|
|
||||||
ssh = SSHClient(
|
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, sleep):
|
||||||
|
"""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,
|
host=host,
|
||||||
port=port,
|
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
private_keys=private_keys)
|
private_keys=gen_private_keys(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_init_username_multiple_keys(self, client, policy, logger, sleep):
|
||||||
|
"""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, sleep):
|
||||||
|
"""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, sleep):
|
||||||
|
self.init_checks(
|
||||||
|
client, policy, logger,
|
||||||
|
host=host,
|
||||||
|
auth=SSHAuth(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
key=gen_private_keys(1).pop()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_init_auth_break(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
self.init_checks(
|
||||||
|
client, policy, logger,
|
||||||
|
host=host,
|
||||||
|
username='Invalid',
|
||||||
|
password='Invalid',
|
||||||
|
private_keys=gen_private_keys(1),
|
||||||
|
auth=SSHAuth(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
key=gen_private_keys(1).pop()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_init_context(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
with SSHClient(host=host, auth=SSHAuth()) as ssh:
|
||||||
|
client.assert_called_once()
|
||||||
|
policy.assert_called_once()
|
||||||
|
|
||||||
|
logger.assert_not_called()
|
||||||
|
|
||||||
|
self.assertEqual(ssh.auth, 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, sleep):
|
||||||
|
"""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 = SSHClient(host=host, auth=SSHAuth())
|
||||||
|
client.assert_called_once()
|
||||||
|
policy.assert_called_once()
|
||||||
|
|
||||||
|
logger.assert_not_called()
|
||||||
|
|
||||||
|
self.assertEqual(ssh.auth, 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.clear()
|
||||||
|
|
||||||
|
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, sleep):
|
||||||
|
"""Test reconnect
|
||||||
|
|
||||||
|
:type client: mock.Mock
|
||||||
|
:type policy: mock.Mock
|
||||||
|
:type logger: mock.Mock
|
||||||
|
"""
|
||||||
|
ssh = SSHClient(host=host, auth=SSHAuth())
|
||||||
|
client.assert_called_once()
|
||||||
|
policy.assert_called_once()
|
||||||
|
|
||||||
|
logger.assert_not_called()
|
||||||
|
|
||||||
|
self.assertEqual(ssh.auth, 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()
|
client.assert_called_once()
|
||||||
policy.assert_called_once()
|
policy.assert_called_once()
|
||||||
|
|
||||||
self.check_defaults(ssh, host, port, username, password, private_keys)
|
logger.assert_not_called()
|
||||||
|
|
||||||
warning.assert_called_once_with(
|
self.assertEqual(ssh.auth, SSHAuth())
|
||||||
'SFTP enable failed! SSH only is accessible.'
|
|
||||||
|
sftp = ssh._sftp
|
||||||
|
self.assertEqual(sftp, client().open_sftp())
|
||||||
|
|
||||||
|
self.assertEqual(ssh._ssh, client())
|
||||||
|
|
||||||
|
def test_init_password_required(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
connect = mock.Mock(side_effect=paramiko.PasswordRequiredException)
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
with self.assertRaises(paramiko.PasswordRequiredException):
|
||||||
|
SSHClient(host=host, auth=SSHAuth())
|
||||||
|
logger.assert_has_calls((
|
||||||
|
mock.call.exception('No password has been set!'),
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_init_password_broken(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
connect = mock.Mock(side_effect=paramiko.PasswordRequiredException)
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
with self.assertRaises(paramiko.PasswordRequiredException):
|
||||||
|
SSHClient(host=host, auth=SSHAuth(password=password))
|
||||||
|
|
||||||
|
logger.assert_has_calls((
|
||||||
|
mock.call.critical(
|
||||||
|
'Unexpected PasswordRequiredException, '
|
||||||
|
'when password is set!'
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_init_auth_impossible_password(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
|
||||||
|
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
with self.assertRaises(paramiko.AuthenticationException):
|
||||||
|
SSHClient(host=host, auth=SSHAuth(password=password))
|
||||||
|
|
||||||
|
logger.assert_has_calls(
|
||||||
|
(
|
||||||
|
mock.call.exception(
|
||||||
|
'Connection using stored authentication info failed!'),
|
||||||
|
) * 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_init_auth_impossible_key(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
|
||||||
|
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
with self.assertRaises(paramiko.AuthenticationException):
|
||||||
|
SSHClient(
|
||||||
|
host=host,
|
||||||
|
auth=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, sleep):
|
||||||
|
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 = SSHClient(
|
||||||
|
host=host,
|
||||||
|
auth=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,
|
||||||
|
SSHAuth(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
keys=[key]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sftp = ssh._sftp
|
||||||
|
self.assertEqual(sftp, client().open_sftp())
|
||||||
|
|
||||||
|
self.assertEqual(ssh._ssh, client())
|
||||||
|
|
||||||
|
def test_init_auth_brute_impossible(
|
||||||
|
self, client, policy, logger, sleep):
|
||||||
|
connect = mock.Mock(side_effect=paramiko.AuthenticationException)
|
||||||
|
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(connect, 'connect')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
with self.assertRaises(paramiko.AuthenticationException):
|
||||||
|
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, sleep):
|
||||||
|
open_sftp = mock.Mock(side_effect=paramiko.SSHException)
|
||||||
|
|
||||||
|
_ssh = mock.Mock()
|
||||||
|
_ssh.attach_mock(open_sftp, 'open_sftp')
|
||||||
|
client.return_value = _ssh
|
||||||
|
|
||||||
|
ssh = SSHClient(host=host, auth=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, sleep):
|
||||||
|
_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 = SSHClient(host=host, auth=SSHAuth(password=password))
|
||||||
|
|
||||||
with self.assertRaises(paramiko.SSHException):
|
with self.assertRaises(paramiko.SSHException):
|
||||||
# pylint: disable=pointless-statement
|
# pylint: disable=pointless-statement
|
||||||
# noinspection PyStatementEffect
|
# noinspection PyStatementEffect
|
||||||
ssh._sftp
|
ssh._sftp
|
||||||
# pylint: enable=pointless-statement
|
# pylint: enable=pointless-statement
|
||||||
|
|
||||||
warning.assert_has_calls([
|
logger.reset_mock()
|
||||||
mock.call('SFTP enable failed! SSH only is accessible.'),
|
|
||||||
mock.call('SFTP is not connected, try to reconnect'),
|
|
||||||
mock.call('SFTP enable failed! SSH only is accessible.')])
|
|
||||||
|
|
||||||
# Unblock sftp connection
|
|
||||||
# (reset_mock is not possible to use in this case)
|
|
||||||
_sftp = mock.Mock()
|
|
||||||
open_sftp = mock.Mock(parent=_ssh, return_value=_sftp)
|
|
||||||
_ssh.attach_mock(open_sftp, 'open_sftp')
|
|
||||||
sftp = ssh._sftp
|
sftp = ssh._sftp
|
||||||
self.assertEqual(sftp, _sftp)
|
self.assertEqual(sftp, open_sftp())
|
||||||
|
logger.assert_has_calls((
|
||||||
|
mock.call.debug('SFTP is not connected, try to connect...'),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
|
@mock.patch('devops.helpers.ssh_client.logger', autospec=True)
|
||||||
|
@ -229,9 +771,10 @@ class TestExecute(TestCase):
|
||||||
return SSHClient(
|
return SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
def test_execute_async(self, client, policy, logger):
|
def test_execute_async(self, client, policy, logger):
|
||||||
chan = mock.Mock()
|
chan = mock.Mock()
|
||||||
|
@ -325,8 +868,9 @@ class TestExecute(TestCase):
|
||||||
logger.mock_calls
|
logger.mock_calls
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock.patch('devops.helpers.ssh_client.SSHAuth.enter_password')
|
||||||
def test_execute_async_sudo_password(
|
def test_execute_async_sudo_password(
|
||||||
self, client, policy, logger):
|
self, enter_password, client, policy, logger):
|
||||||
stdin = mock.Mock(name='stdin')
|
stdin = mock.Mock(name='stdin')
|
||||||
stdout = mock.Mock(name='stdout')
|
stdout = mock.Mock(name='stdout')
|
||||||
stdout_channel = mock.Mock()
|
stdout_channel = mock.Mock()
|
||||||
|
@ -350,6 +894,7 @@ class TestExecute(TestCase):
|
||||||
get_transport.assert_called_once()
|
get_transport.assert_called_once()
|
||||||
open_session.assert_called_once()
|
open_session.assert_called_once()
|
||||||
# raise ValueError(closed.mock_calls)
|
# raise ValueError(closed.mock_calls)
|
||||||
|
enter_password.assert_called_once_with(stdin)
|
||||||
stdin.assert_has_calls((mock.call.flush(), ))
|
stdin.assert_has_calls((mock.call.flush(), ))
|
||||||
|
|
||||||
self.assertIn(chan, result)
|
self.assertIn(chan, result)
|
||||||
|
@ -365,8 +910,7 @@ class TestExecute(TestCase):
|
||||||
logger.mock_calls
|
logger.mock_calls
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def get_patched_execute_async_retval(self, ec=0, stderr_val=True):
|
||||||
def get_patched_execute_async_retval(ec=0, stderr_val=True):
|
|
||||||
stderr = mock.Mock()
|
stderr = mock.Mock()
|
||||||
stdout = mock.Mock()
|
stdout = mock.Mock()
|
||||||
|
|
||||||
|
@ -464,9 +1008,10 @@ class TestExecute(TestCase):
|
||||||
ssh2 = SSHClient(
|
ssh2 = SSHClient(
|
||||||
host=host2,
|
host=host2,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
remotes = [ssh, ssh2]
|
remotes = [ssh, ssh2]
|
||||||
|
|
||||||
|
@ -615,9 +1160,10 @@ class TestExecuteThrowHost(TestCase):
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
result = ssh.execute_through_host(target, command)
|
result = ssh.execute_through_host(target, command)
|
||||||
self.assertEqual(result, return_value)
|
self.assertEqual(result, return_value)
|
||||||
|
@ -660,13 +1206,14 @@ class TestExecuteThrowHost(TestCase):
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
result = ssh.execute_through_host(
|
result = ssh.execute_through_host(
|
||||||
target, command,
|
target, command,
|
||||||
username=_login, password=_password)
|
auth=SSHAuth(username=_login, password=_password))
|
||||||
self.assertEqual(result, return_value)
|
self.assertEqual(result, return_value)
|
||||||
get_transport.assert_called_once()
|
get_transport.assert_called_once()
|
||||||
open_channel.assert_called_once()
|
open_channel.assert_called_once()
|
||||||
|
@ -701,9 +1248,10 @@ class TestSftp(TestCase):
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
return ssh, _sftp
|
return ssh, _sftp
|
||||||
|
|
||||||
def test_exists(self, client, policy, logger):
|
def test_exists(self, client, policy, logger):
|
||||||
|
@ -793,9 +1341,10 @@ class TestSftp(TestCase):
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
# Path not exists
|
# Path not exists
|
||||||
ssh.mkdir(path)
|
ssh.mkdir(path)
|
||||||
|
@ -817,9 +1366,10 @@ class TestSftp(TestCase):
|
||||||
ssh = SSHClient(
|
ssh = SSHClient(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
auth=SSHAuth(
|
||||||
username=username,
|
username=username,
|
||||||
password=password
|
password=password
|
||||||
)
|
))
|
||||||
|
|
||||||
# Path not exists
|
# Path not exists
|
||||||
ssh.rm_rf(path)
|
ssh.rm_rf(path)
|
||||||
|
|
Loading…
Reference in New Issue