| # 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'] |