|  | #    Copyright 2013 - 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 | 
|  |  | 
|  | import base64 | 
|  | import os | 
|  | import posixpath | 
|  | import stat | 
|  | import sys | 
|  | import threading | 
|  | import time | 
|  | import warnings | 
|  |  | 
|  | import paramiko | 
|  | import six | 
|  |  | 
|  | from reclass_tools.helpers import decorators | 
|  | from reclass_tools.helpers import exec_result | 
|  | from reclass_tools.helpers import proc_enums | 
|  | from reclass_tools import logger | 
|  |  | 
|  |  | 
|  | def get_private_keys(home, identity_files=None): | 
|  | if not identity_files: | 
|  | identity_files = ['.ssh/id_rsa'] | 
|  | keys = [] | 
|  | for i in identity_files: | 
|  | with open(os.path.join(home, i)) as f: | 
|  | keys.append(paramiko.RSAKey.from_private_key(f)) | 
|  | return keys | 
|  |  | 
|  |  | 
|  | 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: | 
|  | # noinspection PyTypeChecker | 
|  | 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 | 
|  | """ | 
|  | # noinspection PyTypeChecker | 
|  | 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 __ne__(self, other): | 
|  | return not self.__eq__(other) | 
|  |  | 
|  | def __deepcopy__(self, memo): | 
|  | return self.__class__( | 
|  | username=self.username, | 
|  | password=self.__password, | 
|  | key=self.__key, | 
|  | keys=self.__keys.copy() | 
|  | ) | 
|  |  | 
|  | def copy(self): | 
|  | return self.__class__( | 
|  | username=self.username, | 
|  | password=self.__password, | 
|  | key=self.__key, | 
|  | keys=self.__keys | 
|  | ) | 
|  |  | 
|  | 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 | 
|  | # noinspection PyTypeChecker | 
|  | _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 _MemorizedSSH(type): | 
|  | """Memorize metaclass for SSHClient | 
|  |  | 
|  | This class implements caching and managing of SSHClient connections. | 
|  | Class is not in public scope: all required interfaces is accessible throw | 
|  | SSHClient classmethods. | 
|  |  | 
|  | Main flow is: | 
|  | SSHClient() -> check for cached connection and | 
|  | - If exists the same: check for alive, reconnect if required and return | 
|  | - If exists with different credentials: delete and continue processing | 
|  | create new connection and cache on success | 
|  | * Note: each invocation of SSHClient instance will return current dir to | 
|  | the root of the current user home dir ("cd ~"). | 
|  | It is necessary to avoid unpredictable behavior when the same | 
|  | connection is used from different places. | 
|  | If you need to enter some directory and execute command there, please | 
|  | use the following approach: | 
|  | cmd1 = "cd <some dir> && <command1>" | 
|  | cmd2 = "cd <some dir> && <command2>" | 
|  |  | 
|  | Close cached connections is allowed per-client and all stored: | 
|  | connection will be closed, but still stored in cache for faster reconnect | 
|  |  | 
|  | Clear cache is strictly not recommended: | 
|  | from this moment all open connections should be managed manually, | 
|  | duplicates is possible. | 
|  | """ | 
|  | __cache = {} | 
|  |  | 
|  | def __call__( | 
|  | cls, | 
|  | host, port=22, | 
|  | username=None, password=None, private_keys=None, | 
|  | auth=None | 
|  | ): | 
|  | """Main memorize method: check for cached instance and return it | 
|  |  | 
|  | :type host: str | 
|  | :type port: int | 
|  | :type username: str | 
|  | :type password: str | 
|  | :type private_keys: list | 
|  | :type auth: SSHAuth | 
|  | :rtype: SSHClient | 
|  | """ | 
|  | if (host, port) in cls.__cache: | 
|  | key = host, port | 
|  | if auth is None: | 
|  | auth = SSHAuth( | 
|  | username=username, password=password, keys=private_keys) | 
|  | if hash((cls, host, port, auth)) == hash(cls.__cache[key]): | 
|  | ssh = cls.__cache[key] | 
|  | # noinspection PyBroadException | 
|  | try: | 
|  | ssh.execute('cd ~', timeout=5) | 
|  | except BaseException:  # Note: Do not change to lower level! | 
|  | logger.debug('Reconnect {}'.format(ssh)) | 
|  | ssh.reconnect() | 
|  | return ssh | 
|  | if sys.getrefcount(cls.__cache[key]) == 2: | 
|  | # If we have only cache reference and temporary getrefcount | 
|  | # reference: close connection before deletion | 
|  | logger.debug('Closing {} as unused'.format(cls.__cache[key])) | 
|  | cls.__cache[key].close() | 
|  | del cls.__cache[key] | 
|  | # noinspection PyArgumentList | 
|  | return super( | 
|  | _MemorizedSSH, cls).__call__( | 
|  | host=host, port=port, | 
|  | username=username, password=password, private_keys=private_keys, | 
|  | auth=auth) | 
|  |  | 
|  | @classmethod | 
|  | def record(mcs, ssh): | 
|  | """Record SSH client to cache | 
|  |  | 
|  | :type ssh: SSHClient | 
|  | """ | 
|  | mcs.__cache[(ssh.hostname, ssh.port)] = ssh | 
|  |  | 
|  | @classmethod | 
|  | def clear_cache(mcs): | 
|  | """Clear cached connections for initialize new instance on next call""" | 
|  | n_count = 3 if six.PY3 else 4 | 
|  | # PY3: cache, ssh, temporary | 
|  | # PY4: cache, values mapping, ssh, temporary | 
|  | for ssh in mcs.__cache.values(): | 
|  | if sys.getrefcount(ssh) == n_count: | 
|  | logger.debug('Closing {} as unused'.format(ssh)) | 
|  | ssh.close() | 
|  | mcs.__cache = {} | 
|  |  | 
|  | @classmethod | 
|  | def close_connections(mcs, hostname=None): | 
|  | """Close connections for selected or all cached records | 
|  |  | 
|  | :type hostname: str | 
|  | """ | 
|  | if hostname is None: | 
|  | keys = [key for key, ssh in mcs.__cache.items() if ssh.is_alive] | 
|  | else: | 
|  | keys = [ | 
|  | (host, port) | 
|  | for (host, port), ssh | 
|  | in mcs.__cache.items() if host == hostname and ssh.is_alive] | 
|  | # raise ValueError(keys) | 
|  | for key in keys: | 
|  | mcs.__cache[key].close() | 
|  |  | 
|  |  | 
|  | class SSHClient(six.with_metaclass(_MemorizedSSH, object)): | 
|  | __slots__ = [ | 
|  | '__hostname', '__port', '__auth', '__ssh', '__sftp', 'sudo_mode', | 
|  | '__lock' | 
|  | ] | 
|  |  | 
|  | class __get_sudo(object): | 
|  | """Context manager for call commands with sudo""" | 
|  | def __init__(self, ssh, enforce=None): | 
|  | """Context manager for call commands with sudo | 
|  |  | 
|  | :type ssh: SSHClient | 
|  | :type enforce: bool | 
|  | """ | 
|  | self.__ssh = ssh | 
|  | self.__sudo_status = ssh.sudo_mode | 
|  | self.__enforce = enforce | 
|  |  | 
|  | def __enter__(self): | 
|  | self.__sudo_status = self.__ssh.sudo_mode | 
|  | if self.__enforce is not None: | 
|  | self.__ssh.sudo_mode = self.__enforce | 
|  |  | 
|  | def __exit__(self, exc_type, exc_val, exc_tb): | 
|  | self.__ssh.sudo_mode = self.__sudo_status | 
|  |  | 
|  | # noinspection PyPep8Naming | 
|  | class get_sudo(__get_sudo): | 
|  | """Context manager for call commands with sudo""" | 
|  |  | 
|  | def __init__(self, ssh, enforce=True): | 
|  | warnings.warn( | 
|  | 'SSHClient.get_sudo(SSHClient()) is deprecated in favor of ' | 
|  | 'SSHClient().sudo(enforce=...) , which is much more powerful.') | 
|  | super(self.__class__, self).__init__(ssh=ssh, enforce=enforce) | 
|  |  | 
|  | def __hash__(self): | 
|  | return hash(( | 
|  | self.__class__, | 
|  | self.hostname, | 
|  | self.port, | 
|  | self.auth)) | 
|  |  | 
|  | def __init__( | 
|  | self, | 
|  | 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.__lock = threading.RLock() | 
|  |  | 
|  | self.__hostname = host | 
|  | self.__port = port | 
|  |  | 
|  | self.sudo_mode = False | 
|  | self.__ssh = paramiko.SSHClient() | 
|  | self.__ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | 
|  | self.__sftp = None | 
|  |  | 
|  | self.__auth = auth if auth is None else auth.copy() | 
|  |  | 
|  | 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 | 
|  | )) | 
|  | warnings.warn(msg, DeprecationWarning) | 
|  | logger.debug(msg) | 
|  |  | 
|  | self.__auth = SSHAuth( | 
|  | username=username, | 
|  | password=password, | 
|  | keys=private_keys | 
|  | ) | 
|  |  | 
|  | self.__connect() | 
|  | _MemorizedSSH.record(ssh=self) | 
|  | if auth is None: | 
|  | logger.info( | 
|  | '{0}:{1}> SSHAuth was made from old style creds: ' | 
|  | '{2}'.format(self.hostname, self.port, self.auth)) | 
|  |  | 
|  | @property | 
|  | def lock(self): | 
|  | """Connection lock | 
|  |  | 
|  | :rtype: threading.RLock | 
|  | """ | 
|  | return self.__lock | 
|  |  | 
|  | @property | 
|  | def auth(self): | 
|  | """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 | 
|  | def hostname(self): | 
|  | """Connected remote host name | 
|  |  | 
|  | :rtype: str | 
|  | """ | 
|  | return self.__hostname | 
|  |  | 
|  | @property | 
|  | def host(self): | 
|  | """Hostname access for backward compatibility | 
|  |  | 
|  | :rtype: str | 
|  | """ | 
|  | warnings.warn( | 
|  | 'host has been deprecated in favor of hostname', | 
|  | DeprecationWarning | 
|  | ) | 
|  | return self.hostname | 
|  |  | 
|  | @property | 
|  | def port(self): | 
|  | """Connected remote port number | 
|  |  | 
|  | :rtype: int | 
|  | """ | 
|  | return self.__port | 
|  |  | 
|  | @property | 
|  | def is_alive(self): | 
|  | """Paramiko status: ready to use|reconnect required | 
|  |  | 
|  | :rtype: bool | 
|  | """ | 
|  | return self.__ssh.get_transport() is not None | 
|  |  | 
|  | 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 | 
|  | def _ssh(self): | 
|  | """ssh client object getter for inheritance support only | 
|  |  | 
|  | Attention: ssh client object creation and change | 
|  | is allowed only by __init__ and reconnect call. | 
|  |  | 
|  | :rtype: paramiko.SSHClient | 
|  | """ | 
|  | return self.__ssh | 
|  |  | 
|  | @decorators.retry(paramiko.SSHException, count=3, delay=3) | 
|  | def __connect(self): | 
|  | """Main method for connection open""" | 
|  | with self.lock: | 
|  | self.auth.connect( | 
|  | client=self.__ssh, | 
|  | hostname=self.hostname, port=self.port, | 
|  | log=True) | 
|  |  | 
|  | def __connect_sftp(self): | 
|  | """SFTP connection opener""" | 
|  | with self.lock: | 
|  | try: | 
|  | self.__sftp = self.__ssh.open_sftp() | 
|  | except paramiko.SSHException: | 
|  | logger.warning('SFTP enable failed! SSH only is accessible.') | 
|  |  | 
|  | @property | 
|  | def _sftp(self): | 
|  | """SFTP channel access for inheritance | 
|  |  | 
|  | :rtype: paramiko.sftp_client.SFTPClient | 
|  | :raises: paramiko.SSHException | 
|  | """ | 
|  | if self.__sftp is not None: | 
|  | return self.__sftp | 
|  | logger.debug('SFTP is not connected, try to connect...') | 
|  | self.__connect_sftp() | 
|  | if self.__sftp is not None: | 
|  | return self.__sftp | 
|  | raise paramiko.SSHException('SFTP connection failed') | 
|  |  | 
|  | def close(self): | 
|  | """Close SSH and SFTP sessions""" | 
|  | with self.lock: | 
|  | # noinspection PyBroadException | 
|  | try: | 
|  | self.__ssh.close() | 
|  | self.__sftp = None | 
|  | except Exception: | 
|  | logger.exception("Could not close ssh connection") | 
|  | if self.__sftp is not None: | 
|  | # noinspection PyBroadException | 
|  | try: | 
|  | self.__sftp.close() | 
|  | except Exception: | 
|  | logger.exception("Could not close sftp connection") | 
|  |  | 
|  | @staticmethod | 
|  | def clear(): | 
|  | warnings.warn( | 
|  | "clear is removed: use close() only if it mandatory: " | 
|  | "it's automatically called on revert|shutdown|suspend|destroy", | 
|  | DeprecationWarning | 
|  | ) | 
|  |  | 
|  | @classmethod | 
|  | def _clear_cache(cls): | 
|  | """Enforce clear memorized records""" | 
|  | warnings.warn( | 
|  | '_clear_cache() is dangerous and not recommended for normal use!', | 
|  | Warning | 
|  | ) | 
|  | _MemorizedSSH.clear_cache() | 
|  |  | 
|  | @classmethod | 
|  | def close_connections(cls, hostname=None): | 
|  | """Close cached connections: if hostname is not set, then close all | 
|  |  | 
|  | :type hostname: str | 
|  | """ | 
|  | _MemorizedSSH.close_connections(hostname=hostname) | 
|  |  | 
|  | def __del__(self): | 
|  | """Destructor helper: close channel and threads BEFORE closing others | 
|  |  | 
|  | Due to threading in paramiko, default destructor could generate asserts | 
|  | on close, so we calling channel close before closing main ssh object. | 
|  | """ | 
|  | self.__ssh.close() | 
|  | self.__sftp = None | 
|  |  | 
|  | def __enter__(self): | 
|  | return self | 
|  |  | 
|  | def __exit__(self, exc_type, exc_val, exc_tb): | 
|  | pass | 
|  |  | 
|  | def reconnect(self): | 
|  | """Reconnect SSH session""" | 
|  | with self.lock: | 
|  | self.close() | 
|  |  | 
|  | self.__ssh = paramiko.SSHClient() | 
|  | self.__ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | 
|  |  | 
|  | self.__connect() | 
|  |  | 
|  | def sudo(self, enforce=None): | 
|  | """Call contextmanager for sudo mode change | 
|  |  | 
|  | :type enforce: bool | 
|  | :param enforce: Enforce sudo enabled or disabled. By default: None | 
|  | """ | 
|  | return self.__get_sudo(ssh=self, enforce=enforce) | 
|  |  | 
|  | def check_call( | 
|  | self, | 
|  | command, verbose=False, timeout=None, | 
|  | error_info=None, | 
|  | expected=None, raise_on_err=True, **kwargs): | 
|  | """Execute command and check for return code | 
|  |  | 
|  | :type command: str | 
|  | :type verbose: bool | 
|  | :type timeout: int | 
|  | :type error_info: str | 
|  | :type expected: list | 
|  | :type raise_on_err: bool | 
|  | :rtype: ExecResult | 
|  | :raises: DevopsCalledProcessError | 
|  | """ | 
|  | if expected is None: | 
|  | expected = [proc_enums.ExitCodes.EX_OK] | 
|  | else: | 
|  | expected = [ | 
|  | proc_enums.ExitCodes(code) | 
|  | if ( | 
|  | isinstance(code, int) and | 
|  | code in proc_enums.ExitCodes.__members__.values()) | 
|  | else code | 
|  | for code in expected | 
|  | ] | 
|  | ret = self.execute(command, verbose, timeout, **kwargs) | 
|  | if ret['exit_code'] not in expected: | 
|  | message = ( | 
|  | "{append}Command '{cmd!r}' returned exit code {code!s} while " | 
|  | "expected {expected!s}\n".format( | 
|  | append=error_info + '\n' if error_info else '', | 
|  | cmd=command, | 
|  | code=ret['exit_code'], | 
|  | expected=expected, | 
|  | )) | 
|  | logger.error(message) | 
|  | if raise_on_err: | 
|  | raise SSHCalledProcessError( | 
|  | command, ret['exit_code'], | 
|  | expected=expected, | 
|  | stdout=ret['stdout_brief'], | 
|  | stderr=ret['stdout_brief']) | 
|  | return ret | 
|  |  | 
|  | def check_stderr( | 
|  | self, | 
|  | command, verbose=False, timeout=None, | 
|  | error_info=None, | 
|  | raise_on_err=True, **kwargs): | 
|  | """Execute command expecting return code 0 and empty STDERR | 
|  |  | 
|  | :type command: str | 
|  | :type verbose: bool | 
|  | :type timeout: int | 
|  | :type error_info: str | 
|  | :type raise_on_err: bool | 
|  | :rtype: ExecResult | 
|  | :raises: DevopsCalledProcessError | 
|  | """ | 
|  | ret = self.check_call( | 
|  | command, verbose, timeout=timeout, | 
|  | error_info=error_info, raise_on_err=raise_on_err, **kwargs) | 
|  | if ret['stderr']: | 
|  | message = ( | 
|  | "{append}Command '{cmd!r}' STDERR while not expected\n" | 
|  | "\texit code: {code!s}\n".format( | 
|  | append=error_info + '\n' if error_info else '', | 
|  | cmd=command, | 
|  | code=ret['exit_code'], | 
|  | )) | 
|  | logger.error(message) | 
|  | if raise_on_err: | 
|  | raise SSHCalledProcessError( | 
|  | command, | 
|  | ret['exit_code'], | 
|  | stdout=ret['stdout_brief'], | 
|  | stderr=ret['stdout_brief']) | 
|  | return ret | 
|  |  | 
|  | @classmethod | 
|  | def execute_together( | 
|  | cls, remotes, command, expected=None, raise_on_err=True, **kwargs): | 
|  | """Execute command on multiple remotes in async mode | 
|  |  | 
|  | :type remotes: list | 
|  | :type command: str | 
|  | :type expected: list | 
|  | :type raise_on_err: bool | 
|  | :raises: DevopsCalledProcessError | 
|  | """ | 
|  | if expected is None: | 
|  | expected = [0] | 
|  | futures = {} | 
|  | errors = {} | 
|  | for remote in set(remotes):  # Use distinct remotes | 
|  | chan, _, _, _ = remote.execute_async(command, **kwargs) | 
|  | futures[remote] = chan | 
|  | for remote, chan in futures.items(): | 
|  | ret = chan.recv_exit_status() | 
|  | chan.close() | 
|  | if ret not in expected: | 
|  | errors[remote.hostname] = ret | 
|  | if errors and raise_on_err: | 
|  | raise SSHCalledProcessError(command, errors) | 
|  |  | 
|  | @classmethod | 
|  | def __exec_command( | 
|  | cls, command, channel, stdout, stderr, timeout, verbose=False): | 
|  | """Get exit status from channel with timeout | 
|  |  | 
|  | :type command: str | 
|  | :type channel: paramiko.channel.Channel | 
|  | :type stdout: paramiko.channel.ChannelFile | 
|  | :type stderr: paramiko.channel.ChannelFile | 
|  | :type timeout: int | 
|  | :type verbose: bool | 
|  | :rtype: ExecResult | 
|  | :raises: TimeoutError | 
|  | """ | 
|  | def poll_stream(src, verb_logger=None): | 
|  | dst = [] | 
|  | try: | 
|  | for line in src: | 
|  | dst.append(line) | 
|  | if verb_logger is not None: | 
|  | verb_logger( | 
|  | line.decode('utf-8', | 
|  | errors='backslashreplace').rstrip() | 
|  | ) | 
|  | except IOError: | 
|  | pass | 
|  | return dst | 
|  |  | 
|  | def poll_streams(result, channel, stdout, stderr, verbose): | 
|  | if channel.recv_ready(): | 
|  | result.stdout += poll_stream( | 
|  | src=stdout, | 
|  | verb_logger=logger.info if verbose else logger.debug) | 
|  | if channel.recv_stderr_ready(): | 
|  | result.stderr += poll_stream( | 
|  | src=stderr, | 
|  | verb_logger=logger.error if verbose else logger.debug) | 
|  |  | 
|  | @decorators.threaded(started=True) | 
|  | def poll_pipes(stdout, stderr, result, stop, channel): | 
|  | """Polling task for FIFO buffers | 
|  |  | 
|  | :type stdout: paramiko.channel.ChannelFile | 
|  | :type stderr: paramiko.channel.ChannelFile | 
|  | :type result: ExecResult | 
|  | :type stop: Event | 
|  | :type channel: paramiko.channel.Channel | 
|  | """ | 
|  |  | 
|  | while not stop.isSet(): | 
|  | time.sleep(0.1) | 
|  | poll_streams( | 
|  | result=result, | 
|  | channel=channel, | 
|  | stdout=stdout, | 
|  | stderr=stderr, | 
|  | verbose=verbose | 
|  | ) | 
|  |  | 
|  | if channel.status_event.is_set(): | 
|  | result.exit_code = result.exit_code = channel.exit_status | 
|  |  | 
|  | result.stdout += poll_stream( | 
|  | src=stdout, | 
|  | verb_logger=logger.info if verbose else logger.debug) | 
|  | result.stderr += poll_stream( | 
|  | src=stderr, | 
|  | verb_logger=logger.error if verbose else logger.debug) | 
|  |  | 
|  | stop.set() | 
|  |  | 
|  | # channel.status_event.wait(timeout) | 
|  | result = exec_result.ExecResult(cmd=command) | 
|  | stop_event = threading.Event() | 
|  | if verbose: | 
|  | logger.info("\nExecuting command: {!r}".format(command.rstrip())) | 
|  | else: | 
|  | logger.debug("\nExecuting command: {!r}".format(command.rstrip())) | 
|  | poll_pipes( | 
|  | stdout=stdout, | 
|  | stderr=stderr, | 
|  | result=result, | 
|  | stop=stop_event, | 
|  | channel=channel | 
|  | ) | 
|  |  | 
|  | stop_event.wait(timeout) | 
|  |  | 
|  | # Process closed? | 
|  | if stop_event.isSet(): | 
|  | stop_event.clear() | 
|  | channel.close() | 
|  | return result | 
|  |  | 
|  | stop_event.set() | 
|  | channel.close() | 
|  |  | 
|  | wait_err_msg = ('Wait for {0!r} during {1}s: no return code!\n' | 
|  | .format(command, timeout)) | 
|  | output_brief_msg = ('\tSTDOUT:\n' | 
|  | '{0}\n' | 
|  | '\tSTDERR"\n' | 
|  | '{1}'.format(result.stdout_brief, | 
|  | result.stderr_brief)) | 
|  | logger.debug(wait_err_msg) | 
|  | raise SSHTimeoutError(wait_err_msg + output_brief_msg) | 
|  |  | 
|  | def execute(self, command, verbose=False, timeout=None, **kwargs): | 
|  | """Execute command and wait for return code | 
|  |  | 
|  | :type command: str | 
|  | :type verbose: bool | 
|  | :type timeout: int | 
|  | :rtype: ExecResult | 
|  | :raises: TimeoutError | 
|  | """ | 
|  | chan, _, stderr, stdout = self.execute_async(command, **kwargs) | 
|  |  | 
|  | result = self.__exec_command( | 
|  | command, chan, stdout, stderr, timeout, | 
|  | verbose=verbose | 
|  | ) | 
|  |  | 
|  | message = ( | 
|  | '\n{cmd!r} execution results: Exit code: {code!s}'.format( | 
|  | cmd=command, | 
|  | code=result.exit_code | 
|  | )) | 
|  | if verbose: | 
|  | logger.info(message) | 
|  | else: | 
|  | logger.debug(message) | 
|  | return result | 
|  |  | 
|  | def execute_async(self, command, get_pty=False): | 
|  | """Execute command in async mode and return channel with IO objects | 
|  |  | 
|  | :type command: str | 
|  | :type get_pty: bool | 
|  | :rtype: | 
|  | tuple( | 
|  | paramiko.Channel, | 
|  | paramiko.ChannelFile, | 
|  | paramiko.ChannelFile, | 
|  | paramiko.ChannelFile | 
|  | ) | 
|  | """ | 
|  | logger.debug("Executing command: {!r}".format(command.rstrip())) | 
|  |  | 
|  | chan = self._ssh.get_transport().open_session() | 
|  |  | 
|  | if get_pty: | 
|  | # Open PTY | 
|  | chan.get_pty( | 
|  | term='vt100', | 
|  | width=80, height=24, | 
|  | width_pixels=0, height_pixels=0 | 
|  | ) | 
|  |  | 
|  | stdin = chan.makefile('wb') | 
|  | stdout = chan.makefile('rb') | 
|  | stderr = chan.makefile_stderr('rb') | 
|  | cmd = "{}\n".format(command) | 
|  | if self.sudo_mode: | 
|  | encoded_cmd = base64.b64encode(cmd.encode('utf-8')).decode('utf-8') | 
|  | cmd = ("sudo -S bash -c 'eval \"$(base64 -d " | 
|  | "<(echo \"{0}\"))\"'").format( | 
|  | encoded_cmd | 
|  | ) | 
|  | chan.exec_command(cmd) | 
|  | if stdout.channel.closed is False: | 
|  | self.auth.enter_password(stdin) | 
|  | stdin.flush() | 
|  | else: | 
|  | chan.exec_command(cmd) | 
|  | return chan, stdin, stderr, stdout | 
|  |  | 
|  | def execute_through_host( | 
|  | self, | 
|  | hostname, | 
|  | cmd, | 
|  | auth=None, | 
|  | target_port=22, | 
|  | timeout=None, | 
|  | verbose=False | 
|  | ): | 
|  | """Execute command on remote host through currently connected host | 
|  |  | 
|  | :type hostname: str | 
|  | :type cmd: str | 
|  | :type auth: SSHAuth | 
|  | :type target_port: int | 
|  | :type timeout: int | 
|  | :type verbose: bool | 
|  | :rtype: ExecResult | 
|  | :raises: TimeoutError | 
|  | """ | 
|  | if auth is None: | 
|  | auth = self.auth | 
|  |  | 
|  | intermediate_channel = self._ssh.get_transport().open_channel( | 
|  | kind='direct-tcpip', | 
|  | dest_addr=(hostname, target_port), | 
|  | src_addr=(self.hostname, 0)) | 
|  | transport = paramiko.Transport(sock=intermediate_channel) | 
|  |  | 
|  | # start client and authenticate transport | 
|  | auth.connect(transport) | 
|  |  | 
|  | # open ssh session | 
|  | channel = transport.open_session() | 
|  |  | 
|  | # Make proxy objects for read | 
|  | stdout = channel.makefile('rb') | 
|  | stderr = channel.makefile_stderr('rb') | 
|  |  | 
|  | channel.exec_command(cmd) | 
|  |  | 
|  | # noinspection PyDictCreation | 
|  | result = self.__exec_command( | 
|  | cmd, channel, stdout, stderr, timeout, verbose=verbose) | 
|  |  | 
|  | intermediate_channel.close() | 
|  |  | 
|  | return result | 
|  |  | 
|  | def mkdir(self, path): | 
|  | """run 'mkdir -p path' on remote | 
|  |  | 
|  | :type path: str | 
|  | """ | 
|  | if self.exists(path): | 
|  | return | 
|  | logger.debug("Creating directory: {}".format(path)) | 
|  | # noinspection PyTypeChecker | 
|  | self.execute("mkdir -p {}\n".format(path)) | 
|  |  | 
|  | def rm_rf(self, path): | 
|  | """run 'rm -rf path' on remote | 
|  |  | 
|  | :type path: str | 
|  | """ | 
|  | logger.debug("rm -rf {}".format(path)) | 
|  | # noinspection PyTypeChecker | 
|  | self.execute("rm -rf {}".format(path)) | 
|  |  | 
|  | def open(self, path, mode='r'): | 
|  | """Open file on remote using SFTP session | 
|  |  | 
|  | :type path: str | 
|  | :type mode: str | 
|  | :return: file.open() stream | 
|  | """ | 
|  | return self._sftp.open(path, mode) | 
|  |  | 
|  | def upload(self, source, target): | 
|  | """Upload file(s) from source to target using SFTP session | 
|  |  | 
|  | :type source: str | 
|  | :type target: str | 
|  | """ | 
|  | logger.debug("Copying '%s' -> '%s'", source, target) | 
|  |  | 
|  | if self.isdir(target): | 
|  | target = posixpath.join(target, os.path.basename(source)) | 
|  |  | 
|  | source = os.path.expanduser(source) | 
|  | if not os.path.isdir(source): | 
|  | self._sftp.put(source, target) | 
|  | return | 
|  |  | 
|  | for rootdir, _, files in os.walk(source): | 
|  | targetdir = os.path.normpath( | 
|  | os.path.join( | 
|  | target, | 
|  | os.path.relpath(rootdir, source))).replace("\\", "/") | 
|  |  | 
|  | self.mkdir(targetdir) | 
|  |  | 
|  | for entry in files: | 
|  | local_path = os.path.join(rootdir, entry) | 
|  | remote_path = posixpath.join(targetdir, entry) | 
|  | if self.exists(remote_path): | 
|  | self._sftp.unlink(remote_path) | 
|  | self._sftp.put(local_path, remote_path) | 
|  |  | 
|  | def download(self, destination, target): | 
|  | """Download file(s) to target from destination | 
|  |  | 
|  | :type destination: str | 
|  | :type target: str | 
|  | :rtype: bool | 
|  | """ | 
|  | logger.debug( | 
|  | "Copying '%s' -> '%s' from remote to local host", | 
|  | destination, target | 
|  | ) | 
|  |  | 
|  | if os.path.isdir(target): | 
|  | target = posixpath.join(target, os.path.basename(destination)) | 
|  |  | 
|  | if not self.isdir(destination): | 
|  | if self.exists(destination): | 
|  | self._sftp.get(destination, target) | 
|  | else: | 
|  | logger.debug( | 
|  | "Can't download %s because it doesn't exist", destination | 
|  | ) | 
|  | else: | 
|  | logger.debug( | 
|  | "Can't download %s because it is a directory", destination | 
|  | ) | 
|  | return os.path.exists(target) | 
|  |  | 
|  | def exists(self, path): | 
|  | """Check for file existence using SFTP session | 
|  |  | 
|  | :type path: str | 
|  | :rtype: bool | 
|  | """ | 
|  | try: | 
|  | self._sftp.lstat(path) | 
|  | return True | 
|  | except IOError: | 
|  | return False | 
|  |  | 
|  | def stat(self, path): | 
|  | """Get stat info for path with following symlinks | 
|  |  | 
|  | :type path: str | 
|  | :rtype: paramiko.sftp_attr.SFTPAttributes | 
|  | """ | 
|  | return self._sftp.stat(path) | 
|  |  | 
|  | def isfile(self, path, follow_symlink=False): | 
|  | """Check, that path is file using SFTP session | 
|  |  | 
|  | :type path: str | 
|  | :type follow_symlink: bool (default=False), resolve symlinks | 
|  | :rtype: bool | 
|  | """ | 
|  | try: | 
|  | if follow_symlink: | 
|  | attrs = self._sftp.stat(path) | 
|  | else: | 
|  | attrs = self._sftp.lstat(path) | 
|  | return attrs.st_mode & stat.S_IFREG != 0 | 
|  | except IOError: | 
|  | return False | 
|  |  | 
|  | def isdir(self, path, follow_symlink=False): | 
|  | """Check, that path is directory using SFTP session | 
|  |  | 
|  | :type path: str | 
|  | :type follow_symlink: bool (default=False), resolve symlinks | 
|  | :rtype: bool | 
|  | """ | 
|  | try: | 
|  | if follow_symlink: | 
|  | attrs = self._sftp.stat(path) | 
|  | else: | 
|  | attrs = self._sftp.lstat(path) | 
|  | return attrs.st_mode & stat.S_IFDIR != 0 | 
|  | except IOError: | 
|  | return False | 
|  |  | 
|  | def walk(self, path): | 
|  | files=[] | 
|  | folders=[] | 
|  | try: | 
|  | for item in self._sftp.listdir_iter(path): | 
|  | if item.st_mode & stat.S_IFDIR: | 
|  | folders.append(item.filename) | 
|  | else: | 
|  | files.append(item.filename) | 
|  | except IOError as e: | 
|  | print("Error opening directory {0}: {1}".format(path, e)) | 
|  |  | 
|  | yield path, folders, files | 
|  | for folder in folders: | 
|  | for res in self.walk(os.path.join(path, folder)): | 
|  | yield res | 
|  |  | 
|  |  | 
|  | class SSHClientError(Exception): | 
|  | """Base class for errors""" | 
|  |  | 
|  |  | 
|  | class SSHCalledProcessError(SSHClientError): | 
|  | @staticmethod | 
|  | def _makestr(data): | 
|  | if isinstance(data, six.binary_type): | 
|  | return data.decode('utf-8', errors='backslashreplace') | 
|  | elif isinstance(data, six.text_type): | 
|  | return data | 
|  | else: | 
|  | return repr(data) | 
|  |  | 
|  | def __init__( | 
|  | self, command, returncode, expected=0, stdout=None, stderr=None): | 
|  | self.returncode = returncode | 
|  | self.expected = expected | 
|  | self.cmd = command | 
|  | self.stdout = stdout | 
|  | self.stderr = stderr | 
|  | message = ( | 
|  | "Command '{cmd}' returned exit code {code} while " | 
|  | "expected {expected}".format( | 
|  | cmd=self._makestr(self.cmd), | 
|  | code=self.returncode, | 
|  | expected=self.expected | 
|  | )) | 
|  | if self.stdout: | 
|  | message += "\n\tSTDOUT:\n{}".format(self._makestr(self.stdout)) | 
|  | if self.stderr: | 
|  | message += "\n\tSTDERR:\n{}".format(self._makestr(self.stderr)) | 
|  | super(SSHCalledProcessError, self).__init__(message) | 
|  |  | 
|  | @property | 
|  | def output(self): | 
|  | warnings.warn( | 
|  | 'output is deprecated, please use stdout and stderr separately', | 
|  | DeprecationWarning) | 
|  | return self.stdout + self.stderr | 
|  |  | 
|  |  | 
|  | class SSHTimeoutError(SSHClientError): | 
|  | pass | 
|  |  | 
|  |  | 
|  | __all__ = ['SSHAuth', 'SSHClient', 'SSHClientError', 'SSHCalledProcessError', 'SSHTimeoutError'] |