def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    # Agent connections cannot be used as non-blocking
    s.agent_auth(args.user)
    # Now we can set non-blocking mode
    s.set_blocking(False)
    chan = s.open_session()
    while chan == LIBSSH2_ERROR_EAGAIN:
        print("Would block on session open, waiting for socket to be ready")
        wait_socket(sock, s)
        chan = s.open_session()
    while chan.execute(args.cmd) == LIBSSH2_ERROR_EAGAIN:
        print("Would block on channel execute, waiting for socket to be ready")
        wait_socket(sock, s)
    while chan.wait_eof() == LIBSSH2_ERROR_EAGAIN:
        print("Waiting for command to finish")
        wait_socket(sock, s)
    size, data = chan.read()
    while size == LIBSSH2_ERROR_EAGAIN:
        print("Waiting to read data from channel")
        wait_socket(sock, s)
        size, data = chan.read()
    while size > 0:
        print(data)
        size, data = chan.read()
示例#2
0
    def _establish_ssh_session(self):
        # Connect to remote host.
        try:
            sock = socket.create_connection(
                (str(self._ssh_host), self._ssh_port))
        except Exception:
            log.error("Cannot connect to host '%s' (%s, %d).", self.name,
                      self._ssh_host, self._ssh_port)
            raise

        # SSH handshake.
        ssh_session = Session()
        ssh_session.handshake(sock)

        # Verify host key. Accept keys from previously unknown hosts on first connection.
        hosts = ssh_session.knownhost_init()
        testbed_root = os.path.dirname(os.path.abspath(inspect.stack()[-1][1]))
        known_hosts_path = os.path.join(testbed_root, KNOWN_HOSTS_FILE)
        try:
            hosts.readfile(known_hosts_path)
        except ssh2.exceptions.KnownHostReadFileError:
            pass  # ignore, file is created/overwritten later

        host_key, key_type = ssh_session.hostkey()
        server_type = None
        if key_type == LIBSSH2_HOSTKEY_TYPE_RSA:
            server_type = LIBSSH2_KNOWNHOST_KEY_SSHRSA
        else:
            server_type = LIBSSH2_KNOWNHOST_KEY_SSHDSS
        type_mask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | server_type

        try:
            hosts.checkp(
                str(self._ssh_host).encode('utf-8'), self._ssh_port, host_key,
                type_mask)
        except ssh2.exceptions.KnownHostCheckNotFoundError:
            log.warn("Host key of '%s' (%s, %d) added to known hosts.",
                     self.name, self._ssh_host, self._ssh_port)
            hosts.addc(
                str(self._ssh_host).encode('utf-8'), host_key, type_mask)
            hosts.writefile(known_hosts_path)
        except ssh2.exceptions.KnownHostCheckMisMatchError:
            log.error("Host key of '%s' (%s, %d) does not match known key.",
                      self.name, self._ssh_host, self._ssh_port)
            raise

        # Authenticate at remote host.
        try:
            if self._identity_file is None:
                ssh_session.agent_auth(self._username)
            else:
                ssh_session.userauth_publickey_fromfile(
                    self._username, self._identity_file)
        except Exception:
            log.error("Authentication at host '%s' (%s, %d) failed.",
                      self.name, self._ssh_host, self._ssh_port)
            ssh_session.disconnect()
            raise

        return ssh_session
示例#3
0
def DOS_attack_SSH():
    host = TARGET
    password = input('enter password > ')
    usr = os.getlogin()
    try:
        while True:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((TARGET, PORT))
            print(f'ssh connected: {TARGER} on PORT {PORT}')
            # SSH exclusive msg sending
            session = Session()
            session.handshake(s)
            session.agent_auth(usr)

            # trying an ssh session
            try:
                channel = session.open_session()
                channel.execute('echo "u heb bin heked";')
                channel.close()
            except:
                pass

            global Breach_count
            Breach_count += 1
            print(Breach_count)

            s.close()
    except Exception as e:
        print(e)
示例#4
0
 def start_session(self, hostname, username):
     sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
     sock.connect((hostname, 22))
     s = Session()
     s.handshake(sock)
     auth_methods = s.userauth_list(username)
     if 'publickey' in auth_methods:
         s.agent_auth(username)
         return s
     else:
         # print("Available authentiation methods: %s" % auth_methods)
         sys.exit("Only publickey is supported now!")
示例#5
0
def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    s.agent_auth(args.user)
    chan = s.open_session()
    chan.execute(args.cmd)
    size, data = chan.read()
    while size > 0:
        print(data)
        size, data = chan.read()
示例#6
0
def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    s.agent_auth(args.user)
    sftp = s.sftp_init()
    now = datetime.now()
    print("Starting read for remote file %s" % (args.file, ))
    with sftp.open(args.file, 0, 0) as fh:
        for size, data in fh:
            pass
    print("Finished file read in %s" % (datetime.now() - now, ))
示例#7
0
 def authorize(self, session: Session):
     try:
         session.agent_auth(self.username)
     except ssh2.exceptions.AgentConnectionError as exc:
         raise AgentAuthorizationError(
             "Failed to connect to agent") from exc
     except ssh2.exceptions.AgentListIdentitiesError as exc:
         raise AgentAuthorizationError(
             "Failed to get identities from agent") from exc
     except ssh2.exceptions.AgentAuthenticationError as exc:
         raise AgentAuthorizationError(
             "Failed to get known identity from agent") from exc
     except ssh2.exceptions.AgentAuthenticationError as exc:
         raise AgentAuthorizationError(
             "Failed to auth with all identities") from exc
示例#8
0
def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    s.agent_auth(args.user)
    fileinfo = os.stat(args.source)
    chan = s.scp_send64(args.destination, fileinfo.st_mode & 777,
                        fileinfo.st_size, fileinfo.st_mtime, fileinfo.st_atime)
    print("Starting SCP of local file %s to remote %s:%s" %
          (args.source, args.host, args.destination))
    now = datetime.now()
    with open(args.source, 'rb') as local_fh:
        for data in local_fh:
            chan.write(data)
    taken = datetime.now() - now
    rate = (fileinfo.st_size / (1024000.0)) / taken.total_seconds()
    print("Finished writing remote file in %s, transfer rate %s MB/s" %
          (taken, rate))
示例#9
0
def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    s.agent_auth(args.user)
    sftp = s.sftp_init()
    mode = LIBSSH2_SFTP_S_IRUSR | \
           LIBSSH2_SFTP_S_IWUSR | \
           LIBSSH2_SFTP_S_IRGRP | \
           LIBSSH2_SFTP_S_IROTH
    f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE
    print("Starting copy of local file %s to remote %s:%s" %
          (args.source, args.host, args.destination))
    now = datetime.now()
    with open(args.source, 'rb') as local_fh, \
            sftp.open(args.destination, f_flags, mode) as remote_fh:
        for data in local_fh:
            remote_fh.write(data)
    print("Finished writing remote file in %s" % (datetime.now() - now, ))
def main():
    args = parser.parse_args()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    s = Session()
    s.handshake(sock)
    s.agent_auth(args.user)
    sftp = s.sftp_init()
    now = datetime.now()
    print("Starting read for remote dir %s" % (args.dir, ))
    with sftp.opendir(args.dir) as fh:
        # Can set blocking to false at any point, as long as the
        # libssh2 operations support running in non-blocking mode.
        s.set_blocking(False)
        for size, buf, attrs in fh.readdir():
            if size == LIBSSH2_ERROR_EAGAIN:
                print("Would block on readdir, waiting on socket..")
                wait_socket(sock, s)
                continue
            print(buf)
    print("Finished read dir in %s" % (datetime.now() - now, ))
示例#11
0
def exec_ssh_cmd(cmd, host):
    username = '******'

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, 22))

    session = Session()
    session.handshake(sock)
    session.agent_auth(username)

    session = Session()
    session.userauth_publickey_fromfile(username, '~/.ssh/keys/exoscale')
    channel = session.open_session()
    channel.execute(cmd)

    size, data = channel.read()

    while size > 0:
        print(data)
        size, data = channel.read()
    channel.close()
    print("Exit status: %s" % channel.get_exit_status())
示例#12
0
class RemoteSsh2(object):
    def __init__(self,
                 hostname,
                 ssh_username=None,
                 ssh_password=None,
                 ssh_identity_file=None):
        if hasattr(hostname, "ip"):
            self.hostname = hostname.ip
        else:
            self.hostname = hostname
        if not self.hostname:
            raise NoHostsSpecified("No SSH host specified")
        self.ssh_username = ssh_username
        self.ssh_password = ssh_password
        self.ssh_identity_file = ssh_identity_file
        self.channel = None
        self.session = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.close()

    def _connect(self):
        try:
            if self.session == None:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect((self.hostname, 22))
                self.session = Session()
                self.session.handshake(sock)
                if self.ssh_identity_file != None:
                    self.session.userauth_publickey_fromfile(
                        self.ssh_username, self.ssh_identity_file, '', None)
                elif self.ssh_password != None:
                    self.session.userauth_password(self.ssh_username,
                                                   self.ssh_password)
                elif self.ssh_username == None:
                    user = os.getlogin()
                    self.session.agent_auth(user)
                else:
                    self.session.agent_auth(self.ssh_username)
            self.channel = self.session.open_session()
        except:
            self.channel = None
            self.session = None
            raise BadSSHHost(
                "Could not establish an SSH connection to host %s" %
                (self.hostname, ))

    def run_job(self, file, jobid, timeout=None, env={}):
        try:
            jobs_dir = ".cstar/remote-jobs/" + jobid
            self.run(("mkdir", "-p", jobs_dir))
            self.put_command(file, "%s/job" % (jobs_dir, ))

            # Manually insert environment into script, since passing env into exec_command leads to it being
            # ignored on most ssh servers. :-(

            for key in env:
                if _alnum_re.search(key):
                    raise BadEnvironmentVariable(key)

            env_str = " ".join(key + "=" + self.escape(value)
                               for key, value in env.items())
            remote_script = resource_string('cstar.resources',
                                            'scripts/remote_job.sh')
            wrapper = remote_script.decode("utf-8") % (env_str, )
            self.write_command(wrapper, "%s/wrapper" % (jobs_dir, ))
            cmd_cd = "cd %s" % (self.escape(jobs_dir), )
            cmd_wrapper = "nohup ./wrapper"
            self.exec_command(cmd_cd + ";" + cmd_wrapper)
            out, err_output, status = self.read_channel()
            real_output = self.read_file(jobs_dir + "/stdout")
            real_error = self.read_file(jobs_dir + "/stderr")
            real_status = int(self.read_file(jobs_dir + "/status"))
            return ExecutionResult(cmd_wrapper, real_status, real_output,
                                   real_error)
        except:
            err("Command failed : ", sys.exc_info()[0])
            raise BadSSHHost("SSH connection to host %s was reset" %
                             (self.hostname, ))

    def get_job_status(self, jobid):
        pass

    def exec_command(self, command):
        self._connect()
        self.channel.execute(command)

    def run(self, argv):
        try:
            cmd = " ".join(self.escape(s) for s in argv)
            self.exec_command(cmd)
            out, error, status = self.read_channel()
            if status != 0:
                err("Command %s failed with status %d on host %s" %
                    (cmd, status, self.hostname))
            else:
                debug("Command %s succeeded on host %s, output was %s and %s" %
                      (cmd, self.hostname, out, error))
            return ExecutionResult(cmd, status, out, error)
        except:
            self.client = None
            raise BadSSHHost("SSH connection to host %s was reset" %
                             (self.hostname, ))

    # read stderr, stdout and exit code from the ssh2 channel object
    def read_channel(self):
        stdout = []
        stderr = []

        # read stdout
        size, stdout_part = self.channel.read()
        stdout.append(stdout_part.decode())
        while size > 0:
            size, stdout_part = self.channel.read()
            stdout.append(stdout_part.decode())

        # read stderr
        size, stderr_part = self.channel.read()
        stderr.append(stderr_part.decode())
        while size > 0:
            size, stderr_part = self.channel.read()
            stderr.append(stdout_part.decode())

        status = self.channel.get_exit_status()

        return "".join(stdout), "".join(stderr), status

    @staticmethod
    def escape(input):
        if _alnum_re.search(input):
            return "'" + input.replace("'", r"'\''") + "'"
        return input

    def read_file(self, remotepath):
        self._connect()
        debug("Retrieving %s through SCP" % (remotepath))
        channel, info = self.session.scp_recv(remotepath)
        if info.st_size == 0:
            return ""
        size, content = channel.read(info.st_size - 1)
        channel.close()
        return content.decode("utf-8")

    def put_file(self, localpath, remotepath):
        self._connect()
        fileinfo = os.stat(localpath)
        chan = self.session.scp_send64(remotepath, fileinfo.st_mode & 755,
                                       fileinfo.st_size, fileinfo.st_mtime,
                                       fileinfo.st_atime)
        debug("Starting SCP of local file %s to remote %s:%s" %
              (localpath, self.hostname, remotepath))
        with open(localpath, 'rb') as local_fh:
            for data in local_fh:
                chan.write(data)

    def put_command(self, localpath, remotepath):
        self.put_file(localpath, remotepath)
        self.run(("chmod", "755", remotepath))

    def write_command(self, definition, remotepath):
        self._connect()
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(str.encode(definition))
            fp.flush()
            self.put_file(fp.name, remotepath)
        self.run(("chmod", "755", remotepath))

    def mkdir(self, path):
        self.run(("mkdir", path))

    def close(self):
        if self.channel:
            self.channel.close()
        self.channel = None
        self.session = None
示例#13
0
class SSHClient(BaseSSHClient):
    """ssh2-python (libssh2) based non-blocking SSH client."""
    def __init__(
        self,
        host,
        user=None,
        password=None,
        port=None,
        pkey=None,
        num_retries=DEFAULT_RETRIES,
        retry_delay=RETRY_DELAY,
        allow_agent=True,
        timeout=None,
        forward_ssh_agent=False,
        proxy_host=None,
        proxy_port=None,
        proxy_pkey=None,
        proxy_user=None,
        proxy_password=None,
        _auth_thread_pool=True,
        keepalive_seconds=60,
        identity_auth=True,
    ):
        """:param host: Host name or IP to connect to.
        :type host: str
        :param user: User to connect as. Defaults to logged in user.
        :type user: str
        :param password: Password to use for password authentication.
        :type password: str
        :param port: SSH port to connect to. Defaults to SSH default (22)
        :type port: int
        :param pkey: Private key file path to use for authentication. Path must
          be either absolute path or relative to user home directory
          like ``~/<path>``.
        :type pkey: str
        :param num_retries: (Optional) Number of connection and authentication
          attempts before the client gives up. Defaults to 3.
        :type num_retries: int
        :param retry_delay: Number of seconds to wait between retries. Defaults
          to :py:class:`pssh.constants.RETRY_DELAY`
        :type retry_delay: int
        :param timeout: SSH session timeout setting in seconds. This controls
          timeout setting of authenticated SSH sessions.
        :type timeout: int
        :param allow_agent: (Optional) set to False to disable connecting to
          the system's SSH agent
        :type allow_agent: bool
        :param identity_auth: (Optional) set to False to disable attempting to
          authenticate with default identity files from
          `pssh.clients.base.single.BaseSSHClient.IDENTITIES`
        :type identity_auth: bool
        :param forward_ssh_agent: Unused - agent forwarding not implemented.
        :type forward_ssh_agent: bool
        :param proxy_host: Connect to target host via given proxy host.
        :type proxy_host: str
        :param proxy_port: Port to use for proxy connection. Defaults to self.port
        :type proxy_port: int
        :param keepalive_seconds: Interval of keep alive messages being sent to
          server. Set to ``0`` or ``False`` to disable.

        :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
          provided private key.
        """
        self.forward_ssh_agent = forward_ssh_agent
        self._forward_requested = False
        self.keepalive_seconds = keepalive_seconds
        self._keepalive_greenlet = None
        self._proxy_client = None
        self.host = host
        self.port = port if port is not None else 22
        if proxy_host is not None:
            _port = port if proxy_port is None else proxy_port
            _pkey = pkey if proxy_pkey is None else proxy_pkey
            _user = user if proxy_user is None else proxy_user
            _password = password if proxy_password is None else proxy_password
            proxy_port = self._connect_proxy(
                proxy_host,
                _port,
                _pkey,
                user=_user,
                password=_password,
                num_retries=num_retries,
                retry_delay=retry_delay,
                allow_agent=allow_agent,
                timeout=timeout,
                keepalive_seconds=keepalive_seconds,
                identity_auth=identity_auth,
            )
            proxy_host = '127.0.0.1'
        super(SSHClient, self).__init__(host,
                                        user=user,
                                        password=password,
                                        port=port,
                                        pkey=pkey,
                                        num_retries=num_retries,
                                        retry_delay=retry_delay,
                                        allow_agent=allow_agent,
                                        _auth_thread_pool=_auth_thread_pool,
                                        timeout=timeout,
                                        proxy_host=proxy_host,
                                        proxy_port=proxy_port,
                                        identity_auth=identity_auth)

    def _shell(self, channel):
        return self._eagain(channel.shell)

    def _connect_proxy(self,
                       proxy_host,
                       proxy_port,
                       proxy_pkey,
                       user=None,
                       password=None,
                       num_retries=DEFAULT_RETRIES,
                       retry_delay=RETRY_DELAY,
                       allow_agent=True,
                       timeout=None,
                       forward_ssh_agent=False,
                       keepalive_seconds=60,
                       identity_auth=True):
        assert isinstance(self.port, int)
        try:
            self._proxy_client = SSHClient(proxy_host,
                                           port=proxy_port,
                                           pkey=proxy_pkey,
                                           num_retries=num_retries,
                                           user=user,
                                           password=password,
                                           retry_delay=retry_delay,
                                           allow_agent=allow_agent,
                                           timeout=timeout,
                                           forward_ssh_agent=forward_ssh_agent,
                                           identity_auth=identity_auth,
                                           keepalive_seconds=keepalive_seconds,
                                           _auth_thread_pool=False)
        except Exception as ex:
            msg = "Proxy authentication failed. " \
                  "Exception from tunnel client: %s"
            logger.error(msg, ex)
            raise ProxyError(msg, ex)
        if not FORWARDER.started.is_set():
            FORWARDER.start()
            FORWARDER.started.wait()
        FORWARDER.enqueue(self._proxy_client, self.host, self.port)
        proxy_local_port = FORWARDER.out_q.get()
        return proxy_local_port

    def disconnect(self):
        """Attempt to disconnect session.

        Any errors on calling disconnect are suppressed by this function.
        """
        self._keepalive_greenlet = None
        if self.session is not None:
            try:
                self._disconnect_eagain()
            except Exception:
                pass
            self.session = None
        self.sock = None
        if isinstance(self._proxy_client, SSHClient):
            self._proxy_client.disconnect()

    def spawn_send_keepalive(self):
        """Spawns a new greenlet that sends keep alive messages every
        self.keepalive_seconds"""
        return spawn(self._send_keepalive)

    def _send_keepalive(self):
        while True:
            sleep(self._eagain(self.session.keepalive_send))

    def configure_keepalive(self):
        self.session.keepalive_config(False, self.keepalive_seconds)

    def _init_session(self, retries=1):
        self.session = Session()
        if self.timeout:
            # libssh2 timeout is in ms
            self.session.set_timeout(self.timeout * 1000)
        try:
            if self._auth_thread_pool:
                THREAD_POOL.apply(self.session.handshake, (self.sock, ))
            else:
                self.session.handshake(self.sock)
        except Exception as ex:
            if retries < self.num_retries:
                sleep(self.retry_delay)
                return self._connect_init_session_retry(retries=retries + 1)
            msg = "Error connecting to host %s:%s - %s"
            logger.error(msg, self.host, self.port, ex)
            if isinstance(ex, SSH2Timeout):
                raise Timeout(msg, self.host, self.port, ex)
            ex.host = self.host
            ex.port = self.port
            raise

    def _keepalive(self):
        if self.keepalive_seconds:
            self.configure_keepalive()
            self._keepalive_greenlet = self.spawn_send_keepalive()

    def _agent_auth(self):
        self.session.agent_auth(self.user)

    def _pkey_auth(self, pkey_file, password=None):
        self.session.userauth_publickey_fromfile(
            self.user,
            pkey_file,
            passphrase=password if password is not None else b'')

    def _password_auth(self):
        self.session.userauth_password(self.user, self.password)

    def _open_session(self):
        chan = self._eagain(self.session.open_session)
        return chan

    def open_session(self):
        """Open new channel from session"""
        try:
            chan = self._open_session()
        except Exception as ex:
            raise SessionError(ex)
        if self.forward_ssh_agent and not self._forward_requested:
            if not hasattr(chan, 'request_auth_agent'):
                warn("Requested SSH Agent forwarding but libssh2 version used "
                     "does not support it - ignoring")
                return chan
            # self._eagain(chan.request_auth_agent)
            # self._forward_requested = True
        return chan

    def _make_output_readers(self, channel, stdout_buffer, stderr_buffer):
        _stdout_reader = spawn(self._read_output_to_buffer, channel.read,
                               stdout_buffer)
        _stderr_reader = spawn(self._read_output_to_buffer,
                               channel.read_stderr, stderr_buffer)
        return _stdout_reader, _stderr_reader

    def execute(self, cmd, use_pty=False, channel=None):
        """Execute command on remote server.

        :param cmd: Command to execute.
        :type cmd: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param channel: Use provided channel for execute rather than creating
          a new one.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        channel = self.open_session() if channel is None else channel
        if use_pty:
            self._eagain(channel.pty)
        logger.debug("Executing command '%s'", cmd)
        self._eagain(channel.execute, cmd)
        return channel

    def _read_output_to_buffer(self, read_func, _buffer):
        try:
            while True:
                size, data = read_func()
                while size == LIBSSH2_ERROR_EAGAIN:
                    self.poll()
                    size, data = read_func()
                if size <= 0:
                    break
                _buffer.write(data)
        finally:
            _buffer.eof.set()

    def wait_finished(self, host_output, timeout=None):
        """Wait for EOF from channel and close channel.

        Used to wait for remote command completion and be able to gather
        exit code.

        :param host_output: Host output of command to wait for.
        :type host_output: :py:class:`pssh.output.HostOutput`
        :param timeout: Timeout value in seconds - defaults to no timeout.
        :type timeout: float

        :raises: :py:class:`pssh.exceptions.Timeout` after <timeout> seconds if
          timeout given.
        """
        if not isinstance(host_output, HostOutput):
            raise ValueError("%s is not a HostOutput object" % (host_output, ))
        channel = host_output.channel
        if channel is None:
            return
        self._eagain(channel.wait_eof, timeout=timeout)
        # Close channel to indicate no more commands will be sent over it
        self.close_channel(channel)

    def close_channel(self, channel):
        logger.debug("Closing channel")
        self._eagain(channel.close)

    def _eagain(self, func, *args, **kwargs):
        return self._eagain_errcode(func, LIBSSH2_ERROR_EAGAIN, *args,
                                    **kwargs)

    def _make_sftp_eagain(self):
        return self._eagain(self.session.sftp_init)

    def _make_sftp(self):
        """Make SFTP client from open transport"""
        try:
            sftp = self._make_sftp_eagain()
        except Exception as ex:
            raise SFTPError(ex)
        return sftp

    def _mkdir(self, sftp, directory):
        """Make directory via SFTP channel

        :param sftp: SFTP client object
        :type sftp: :py:class:`ssh2.sftp.SFTP`
        :param directory: Remote directory to create
        :type directory: str

        :raises: :py:class:`pssh.exceptions.SFTPIOError` on SFTP IO errors
        """
        mode = LIBSSH2_SFTP_S_IRUSR | \
            LIBSSH2_SFTP_S_IWUSR | \
            LIBSSH2_SFTP_S_IXUSR | \
            LIBSSH2_SFTP_S_IRGRP | \
            LIBSSH2_SFTP_S_IROTH | \
            LIBSSH2_SFTP_S_IXGRP | \
            LIBSSH2_SFTP_S_IXOTH
        try:
            self._eagain(sftp.mkdir, directory, mode)
        except SFTPProtocolError as error:
            msg = "Error occured creating directory %s on host %s - %s"
            logger.error(msg, directory, self.host, error)
            raise SFTPIOError(msg, directory, self.host, error)
        logger.debug("Created remote directory %s", directory)

    def copy_file(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SFTP.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool
        :param sftp: SFTP channel to use instead of creating a
          new one.
        :type sftp: :py:class:`ssh2.sftp.SFTP`

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        if os.path.isdir(local_file) and recurse:
            return self._copy_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be true if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self.sftp_put(sftp, local_file, remote_file)
        logger.info("Copied local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _sftp_put(self, remote_fh, local_file):
        with open(local_file, 'rb', 2097152) as local_fh:
            for data in local_fh:
                self.eagain_write(remote_fh.write, data)

    def sftp_put(self, sftp, local_file, remote_file):
        mode = LIBSSH2_SFTP_S_IRUSR | \
               LIBSSH2_SFTP_S_IWUSR | \
               LIBSSH2_SFTP_S_IRGRP | \
               LIBSSH2_SFTP_S_IROTH
        f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC
        with self._sftp_openfh(sftp.open, remote_file, f_flags,
                               mode) as remote_fh:
            try:
                self._sftp_put(remote_fh, local_file)
            except SFTPProtocolError as ex:
                msg = "Error writing to remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def mkdir(self, sftp, directory):
        """Make directory via SFTP channel.

        Parent paths in the directory are created if they do not exist.

        :param sftp: SFTP client object
        :type sftp: :py:class:`ssh2.sftp.SFTP`
        :param directory: Remote directory to create
        :type directory: str

        Catches and logs at error level remote IOErrors on creating directory.
        """
        _paths_to_create = deque()
        for d in directory.split('/'):
            if not d:
                continue
            _paths_to_create.append(d)
        cwd = '' if directory.startswith('/') else '.'
        while _paths_to_create:
            cur_dir = _paths_to_create.popleft()
            cwd = '/'.join([cwd, cur_dir])
            try:
                self._eagain(sftp.stat, cwd)
            except (SFTPHandleError, SFTPProtocolError) as ex:
                logger.debug("Stat for %s failed with %s", cwd, ex)
                self._mkdir(sftp, cwd)

    def copy_remote_file(self,
                         remote_file,
                         local_file,
                         recurse=False,
                         sftp=None,
                         encoding='utf-8'):
        """Copy remote file to local host via SFTP.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths.
        :type encoding: str
        :param sftp: SFTP channel to use instead of creating a
          new one.
        :type sftp: :py:class:`ssh2.sftp.SFTP`

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors reading
          from SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        try:
            self._eagain(sftp.stat, remote_file)
        except (SFTPHandleError, SFTPProtocolError):
            msg = "Remote file or directory %s does not exist"
            logger.error(msg, remote_file)
            raise SFTPIOError(msg, remote_file)
        try:
            dir_h = self._sftp_openfh(sftp.opendir, remote_file)
        except SFTPError:
            pass
        else:
            if not recurse:
                raise ValueError("Recurse must be true if remote_file is a "
                                 "directory.")
            file_list = self._sftp_readdir(dir_h)
            return self._copy_remote_dir(file_list,
                                         remote_file,
                                         local_file,
                                         sftp,
                                         encoding=encoding)
        destination = os.path.join(
            os.path.sep,
            os.path.sep.join([_dir for _dir in local_file.split('/')
                              if _dir][:-1]))
        self._make_local_dir(destination)
        self.sftp_get(sftp, remote_file, local_file)
        logger.info("Copied local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_recv_recursive(self,
                            remote_file,
                            local_file,
                            sftp,
                            encoding='utf-8'):
        try:
            self._eagain(sftp.stat, remote_file)
        except (SFTPHandleError, SFTPProtocolError):
            msg = "Remote file or directory %s does not exist"
            logger.error(msg, remote_file)
            raise SCPError(msg, remote_file)
        try:
            dir_h = self._sftp_openfh(sftp.opendir, remote_file)
        except SFTPError:
            # remote_file is not a dir, scp file
            return self.scp_recv(remote_file, local_file, encoding=encoding)
        try:
            os.makedirs(local_file)
        except OSError:
            pass
        file_list = self._sftp_readdir(dir_h)
        return self._scp_recv_dir(file_list,
                                  remote_file,
                                  local_file,
                                  sftp,
                                  encoding=encoding)

    def scp_recv(self,
                 remote_file,
                 local_file,
                 recurse=False,
                 sftp=None,
                 encoding='utf-8'):
        """Copy remote file to local host via SCP.

        Note - Remote directory listings are gathered via SFTP when
        ``recurse`` is enabled - SCP lacks directory list support.
        Enabling recursion therefore involves creating an extra SFTP channel
        and requires SFTP support on the server.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths when recursion is
          enabled.
        :type encoding: str

        :raises: :py:class:`pssh.exceptions.SCPError` on errors copying file.
        :raises: :py:class:`IOError` on local file IO errors.
        :raises: :py:class:`OSError` on local OS errors like permission denied.
        """
        if recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            return self._scp_recv_recursive(remote_file,
                                            local_file,
                                            sftp,
                                            encoding=encoding)
        elif local_file.endswith('/'):
            remote_filename = remote_file.rsplit('/')[-1]
            local_file += remote_filename
        destination = os.path.join(
            os.path.sep,
            os.path.sep.join([_dir for _dir in local_file.split('/')
                              if _dir][:-1]))
        self._make_local_dir(destination)
        self._scp_recv(remote_file, local_file)
        logger.info("SCP local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_recv(self, remote_file, local_file):
        try:
            (file_chan, fileinfo) = self._eagain(self.session.scp_recv2,
                                                 remote_file)
        except Exception as ex:
            msg = "Error copying file %s from host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        local_fh = open(local_file, 'wb')
        try:
            total = 0
            while total < fileinfo.st_size:
                size, data = file_chan.read(size=fileinfo.st_size - total)
                if size == LIBSSH2_ERROR_EAGAIN:
                    self.poll()
                    continue
                total += size
                local_fh.write(data)
        finally:
            local_fh.close()
            file_chan.close()

    def scp_send(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SCP.

        Note - Directories are created via SFTP when ``recurse`` is enabled -
        SCP lacks directory create support. Enabling recursion therefore
        involves creating an extra SFTP channel and requires SFTP support on the
        server.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        if os.path.isdir(local_file) and recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            return self._scp_send_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be True if local_file is a "
                             "directory.")
        if recurse:
            destination = self._remote_paths_split(remote_file)
            if destination is not None:
                sftp = self._make_sftp() if sftp is None else sftp
                try:
                    self._eagain(sftp.stat, destination)
                except (SFTPHandleError, SFTPProtocolError):
                    self.mkdir(sftp, destination)
        elif remote_file.endswith('/'):
            local_filename = local_file.rsplit('/')[-1]
            remote_file += local_filename
        self._scp_send(local_file, remote_file)
        logger.info("SCP local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_send(self, local_file, remote_file):
        fileinfo = os.stat(local_file)
        try:
            chan = self._eagain(self.session.scp_send64, remote_file,
                                fileinfo.st_mode & 0o777, fileinfo.st_size,
                                fileinfo.st_mtime, fileinfo.st_atime)
        except Exception as ex:
            msg = "Error opening remote file %s for writing on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        try:
            with open(local_file, 'rb', 2097152) as local_fh:
                for data in local_fh:
                    self.eagain_write(chan.write, data)
        except Exception as ex:
            msg = "Error writing to remote file %s on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        finally:
            chan.close()

    def _sftp_openfh(self, open_func, remote_file, *args):
        try:
            fh = self._eagain(open_func, remote_file, *args)
        except Exception as ex:
            raise SFTPError(ex)
        return fh

    def _sftp_get(self, remote_fh, local_file):
        with open(local_file, 'wb') as local_fh:
            for size, data in remote_fh:
                if size == LIBSSH2_ERROR_EAGAIN:
                    self.poll()
                    continue
                local_fh.write(data)

    def sftp_get(self, sftp, remote_file, local_file):
        with self._sftp_openfh(sftp.open, remote_file, LIBSSH2_FXF_READ,
                               LIBSSH2_SFTP_S_IRUSR) as remote_fh:
            try:
                self._sftp_get(remote_fh, local_file)
            except SFTPProtocolError as ex:
                msg = "Error reading from remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def get_exit_status(self, channel):
        """Get exit status code for channel or ``None`` if not ready.

        :param channel: The channel to get status from.
        :type channel: :py:mod:`ssh2.channel.Channel`
        :rtype: int or ``None``
        """
        if not channel.eof():
            return
        return channel.get_exit_status()

    def finished(self, channel):
        """Checks if remote command has finished - has server sent client
        EOF.

        :rtype: bool
        """
        if channel is None:
            return
        return channel.eof()

    def poll(self, timeout=None):
        """Perform co-operative gevent poll on ssh2 session socket.

        Blocks current greenlet only if socket has pending read or write operations
        in the appropriate direction.
        """
        self._poll_errcodes(
            self.session.block_directions,
            LIBSSH2_SESSION_BLOCK_INBOUND,
            LIBSSH2_SESSION_BLOCK_OUTBOUND,
            timeout=timeout,
        )

    def _eagain_write(self, write_func, data, timeout=None):
        """Write data with given write_func for an ssh2-python session while
        handling EAGAIN and resuming writes from last written byte on each call to
        write_func.
        """
        return self._eagain_write_errcode(write_func,
                                          data,
                                          LIBSSH2_ERROR_EAGAIN,
                                          timeout=timeout)

    def eagain_write(self, write_func, data, timeout=None):
        return self._eagain_write(write_func, data, timeout=timeout)
示例#14
0
def security_investigate(ip):
    """Investigate the server at IP address.

    Use ssh-agent key to find the VM bridge interface and test its
    SSH authentication method.
    """
    session = get_session()
    nova = nova_client.Client(2, session=session)
    glance = glance_client.Client(2, session=session)
    neutron = neutron_client.Client(session=session)
    opts = {'all_tenants': True, 'ip': ip}
    instances = nova.servers.list(search_opts=opts, limit=1)
    if not instances:
        return
    instance = instances[0]

    target_mac_device = None
    target_host = instance._info['OS-EXT-SRV-ATTR:hypervisor_hostname']
    target_instance_name = instance._info['OS-EXT-SRV-ATTR:instance_name']
    # Augment the retrieved instance info
    if instance.image:
        image = try_assign(glance.images.get, instance.image['id'])
        if image:
            instance._info['image'] = image.name
    instance_flavor = nova.flavors.get(instance.flavor['id'])._info
    instance._info['flavor:name'] = instance_flavor['name']
    instance._info['flavor:vcpus'] = instance_flavor['vcpus']
    instance._info['flavor:ram'] = instance_flavor['ram']
    instance._info['os-extended-volumes:volumes_attached'] = \
        ', '.join(v['id']
                  for v in
                  instance._info['os-extended-volumes:volumes_attached'])
    for az in instance._info['addresses'].keys():
        network_name = "%s network" % az
        network = instance._info['addresses'][az]
        for net in network:
            # Retrieve the device mac to find out the correct tap interface
            if net['addr'] in ip:
                target_mac_device = net['OS-EXT-IPS-MAC:mac_addr']
                break
        output = ', '.join("%s" % net['addr'] for net in network)
        instance._info[network_name] = output
    # Render instance information
    _render_table_instance(instance)
    security_groups = generate_instance_sg_rules_info(neutron, instance.id)
    click.echo(_format_secgroups(security_groups))

    nmap_ports = []
    # Generate recommendation for nmap scan
    for sg in security_groups['security_groups']:
        for rule in sg['security_group_rules']:
            if (rule['direction'] in 'ingress' and rule['remote_ip_prefix']
                    and rule['remote_ip_prefix'] in '0.0.0.0/0'
                    and rule['protocol'] in ['tcp', 'udp']):
                if rule['port_range_min'] == rule['port_range_max']:
                    nmap_ports.append(str(rule['port_range_min']))
                else:
                    nmap_ports.append(
                        "%s-%s" %
                        (rule['port_range_min'], rule['port_range_max']))
    nmap_command = _recommend_nmap_command(nmap_ports, ip)

    ENABLED_PASSWORD_LOGIN = False
    # Probe the server ssh for password login
    if '22' in nmap_ports:
        vm_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        vm_sock.connect((ip, 22))
        ssh = Session()
        ssh.handshake(vm_sock)
        ssh_authlist = ssh.userauth_list('test')
        click.echo('SSH authentication method list: %s' % ssh_authlist)
        if 'password' in ssh_authlist:
            ENABLED_PASSWORD_LOGIN = True
    # Generate tcpdump
    host_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    host_sock.connect((target_host, 22))

    ssh = Session()
    ssh.handshake(host_sock)
    ssh.agent_auth('root')
    channel = ssh.open_session()
    channel.execute("virsh dumpxml %s | grep %s -A3 | grep bridge" %
                    (target_instance_name, target_mac_device))
    size, data = channel.read()
    target_bridge_interface = data.split("'")[1]

    tcpdump_command = "ssh %s 'tcpdump -l -q -i %s not arp and not icmp'" % \
                      (target_host,
                       target_bridge_interface)

    # Print out all recommendation
    click.echo('RECOMMENDATION:')
    if ENABLED_PASSWORD_LOGIN:
        click.echo('* RED FLAG: VM has password login enabled!')
    click.echo("* Discover VM running services by running: %s" % nmap_command)
    click.echo("* Discover VM network traffics by running: %s" %
               tcpdump_command)
示例#15
0
class SSHClient(object):
    """ssh2-python (libssh2) based non-blocking SSH client."""

    IDENTITIES = [
        os.path.expanduser('~/.ssh/id_rsa'),
        os.path.expanduser('~/.ssh/id_dsa'),
        os.path.expanduser('~/.ssh/identity')
    ]

    def __init__(self, host,
                 user=None, password=None, port=None,
                 pkey=None,
                 num_retries=DEFAULT_RETRIES,
                 retry_delay=RETRY_DELAY,
                 allow_agent=True, timeout=None,
                 forward_ssh_agent=True,
                 proxy_host=None,
                 _auth_thread_pool=True):
        """:param host: Host name or IP to connect to.
        :type host: str
        :param user: User to connect as. Defaults to logged in user.
        :type user: str
        :param password: Password to use for password authentication.
        :type password: str
        :param port: SSH port to connect to. Defaults to SSH default (22)
        :type port: int
        :param pkey: Private key file path to use for authentication.
          Note that the public key file
          pair *must* also exist in the same location with name ``<pkey>.pub``
        :type pkey: str
        :param num_retries: (Optional) Number of connection and authentication
          attempts before the client gives up. Defaults to 3.
        :type num_retries: int
        :param retry_delay: Number of seconds to wait between retries. Defaults
          to :py:class:`pssh.constants.RETRY_DELAY`
        :type retry_delay: int
        :param timeout: SSH session timeout setting in seconds. This controls
          timeout setting of authenticated SSH sessions.
        :type timeout: int
        :param allow_agent: (Optional) set to False to disable connecting to
          the system's SSH agent
        :type allow_agent: bool
        :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding -
          equivalent to `ssh -A` from the `ssh` command line utility.
          Defaults to True if not set.
        :type forward_ssh_agent: bool
        :param proxy_host: Connection to host is via provided proxy host
          and client should use self.proxy_host for connection attempts.
        :type proxy_host: str
        """
        self.host = host
        self.user = user if user else None
        if self.user is None and not WIN_PLATFORM:
            self.user = pwd.getpwuid(os.geteuid()).pw_name
        elif self.user is None and WIN_PLATFORM:
            raise ValueError("Must provide user parameter on Windows")
        self.password = password
        self.port = port if port else 22
        self.pkey = pkey
        self.num_retries = num_retries
        self.sock = None
        self.timeout = timeout * 1000 if timeout else None
        self.retry_delay = retry_delay
        self.allow_agent = allow_agent
        self.forward_ssh_agent = forward_ssh_agent
        self._forward_requested = False
        self.session = None
        self._host = proxy_host if proxy_host else host
        self._connect(self._host, self.port)
        if _auth_thread_pool:
            THREAD_POOL.apply(self._init)
        else:
            self._init()

    def disconnect(self):
        """Disconnect session, close socket if needed."""
        if self.session is not None:
            try:
                self._eagain(self.session.disconnect)
            except Exception:
                pass
        if not self.sock.closed:
            self.sock.close()

    def __del__(self):
        self.disconnect()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.disconnect()

    def _connect_init_retry(self, retries):
        retries += 1
        self.session = None
        if not self.sock.closed:
            self.sock.close()
        sleep(self.retry_delay)
        self._connect(self._host, self.port, retries=retries)
        return self._init(retries=retries)

    def _init(self, retries=1):
        self.session = Session()
        if self.timeout:
            self.session.set_timeout(self.timeout)
        try:
            self.session.handshake(self.sock)
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Error connecting to host %s:%s - %s"
            logger.error(msg, self.host, self.port, ex)
            if isinstance(ex, SSH2Timeout):
                raise Timeout(msg, self.host, self.port, ex)
            raise
        try:
            self.auth()
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Authentication error while connecting to %s:%s - %s"
            raise AuthenticationException(msg, self.host, self.port, ex)
        self.session.set_blocking(0)

    def _connect(self, host, port, retries=1):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        logger.debug("Connecting to %s:%s", host, port)
        try:
            self.sock.connect((host, port))
        except sock_gaierror as ex:
            logger.error("Could not resolve host '%s' - retry %s/%s",
                         host, retries, self.num_retries)
            while retries < self.num_retries:
                sleep(self.retry_delay)
                return self._connect(host, port, retries=retries+1)
            raise UnknownHostException("Unknown host %s - %s - retry %s/%s",
                                       host, str(ex.args[1]), retries,
                                       self.num_retries)
        except sock_error as ex:
            logger.error("Error connecting to host '%s:%s' - retry %s/%s",
                         host, port, retries, self.num_retries)
            while retries < self.num_retries:
                sleep(self.retry_delay)
                return self._connect(host, port, retries=retries+1)
            error_type = ex.args[1] if len(ex.args) > 1 else ex.args[0]
            raise ConnectionErrorException(
                "Error connecting to host '%s:%s' - %s - retry %s/%s",
                host, port, str(error_type), retries,
                self.num_retries,)

    def _pkey_auth(self):
        pub_file = "%s.pub" % self.pkey
        logger.debug("Attempting authentication with public key %s for user %s",
                     pub_file, self.user)
        self.session.userauth_publickey_fromfile(
            self.user,
            pub_file,
            self.pkey,
            self.password if self.password is not None else '')

    def _identity_auth(self):
        for identity_file in self.IDENTITIES:
            if not os.path.isfile(identity_file):
                continue
            pub_file = "%s.pub" % (identity_file)
            logger.debug(
                "Trying to authenticate with identity file %s",
                identity_file)
            try:
                self.session.userauth_publickey_fromfile(
                    self.user,
                    pub_file,
                    identity_file,
                    self.password if self.password is not None else '')
            except Exception:
                logger.debug("Authentication with identity file %s failed, "
                             "continuing with other identities",
                             identity_file)
                continue
            else:
                logger.debug("Authentication succeeded with identity file %s",
                             identity_file)
                return
        raise AuthenticationException("No authentication methods succeeded")

    def auth(self):
        if self.pkey is not None:
            logger.debug(
                "Proceeding with public key file authentication")
            return self._pkey_auth()
        if self.allow_agent:
            try:
                self.session.agent_auth(self.user)
            except Exception as ex:
                logger.debug("Agent auth failed with %s, "
                             "continuing with other authentication methods",
                             ex)
            else:
                logger.debug("Authentication with SSH Agent succeeded")
                return
        try:
            self._identity_auth()
        except AuthenticationException:
            if self.password is None:
                raise
            logger.debug("Public key auth failed, trying password")
            self._password_auth()

    def _password_auth(self):
        if self.session.userauth_password(self.user, self.password) != 0:
            raise AuthenticationException("Password authentication failed")

    def open_session(self):
        """Open new channel from session"""
        try:
            chan = self.session.open_session()
        except Exception as ex:
            raise SessionError(ex)
        while chan == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                chan = self.session.open_session()
            except Exception as ex:
                raise SessionError(ex)
        # Multiple forward requests result in ChannelRequestDenied
        # errors, flag is used to avoid this.
        if self.forward_ssh_agent and not self._forward_requested:
            self._eagain(chan.request_auth_agent)
            self._forward_requested = True
        return chan

    def execute(self, cmd, use_pty=False, channel=None):
        """Execute command on remote server.

        :param cmd: Command to execute.
        :type cmd: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param channel: Use provided channel for execute rather than creating
          a new one.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        channel = self.open_session() if channel is None else channel
        if use_pty:
            self._eagain(channel.pty)
        logger.debug("Executing command '%s'" % cmd)
        self._eagain(channel.execute, cmd)
        return channel

    def read_stderr(self, channel, timeout=None):
        """Read standard error buffer from channel.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        return _read_output(self.session, channel.read_stderr, timeout=timeout)

    def read_output(self, channel, timeout=None):
        """Read standard output buffer from channel.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        return _read_output(self.session, channel.read, timeout=timeout)

    def _select_timeout(self, func, timeout):
        ret = func()
        while ret == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session, timeout=timeout)
            ret = func()
            if ret == LIBSSH2_ERROR_EAGAIN and timeout is not None:
                raise Timeout

    def wait_finished(self, channel, timeout=None):
        """Wait for EOF from channel and close channel.

        Used to wait for remote command completion and be able to gather
        exit code.

        :param channel: The channel to use.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        if channel is None:
            return
        # If .eof() returns EAGAIN after a select with a timeout, it means
        # it reached timeout without EOF and _select_timeout will raise
        # timeout exception causing the channel to appropriately
        # not be closed as the command is still running.
        self._select_timeout(channel.wait_eof, timeout)
        # Close channel to indicate no more commands will be sent over it
        self.close_channel(channel)

    def close_channel(self, channel):
        logger.debug("Closing channel")
        self._eagain(channel.close)

    def _eagain(self, func, *args, **kwargs):
        ret = func(*args, **kwargs)
        while ret == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            ret = func(*args, **kwargs)
        return ret

    def read_output_buffer(self, output_buffer, prefix=None,
                           callback=None,
                           callback_args=None,
                           encoding='utf-8'):
        """Read from output buffers and log to ``host_logger``.

        :param output_buffer: Iterator containing buffer
        :type output_buffer: iterator
        :param prefix: String to prefix log output to ``host_logger`` with
        :type prefix: str
        :param callback: Function to call back once buffer is depleted:
        :type callback: function
        :param callback_args: Arguments for call back function
        :type callback_args: tuple
        """
        prefix = '' if prefix is None else prefix
        for line in output_buffer:
            output = line.decode(encoding)
            host_logger.info("[%s]%s\t%s", self.host, prefix, output)
            yield output
        if callback:
            callback(*callback_args)

    def run_command(self, command, sudo=False, user=None,
                    use_pty=False, shell=None,
                    encoding='utf-8', timeout=None):
        """Run remote command.

        :param command: Command to run.
        :type command: str
        :param sudo: Run command via sudo as super-user.
        :type sudo: bool
        :param user: Run command as user via sudo
        :type user: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param shell: (Optional) Override shell to use to run command with.
          Defaults to login user's defined shell. Use the shell's command
          syntax, eg `shell='bash -c'` or `shell='zsh -c'`.
        :type shell: str
        :param encoding: Encoding to use for output. Must be valid
          `Python codec <https://docs.python.org/2.7/library/codecs.html>`_
        :type encoding: str
        """
        # Fast path for no command substitution needed
        if not sudo and not user and not shell:
            _command = command
        else:
            _command = ''
            if sudo and not user:
                _command = 'sudo -S '
            elif user:
                _command = 'sudo -u %s -S ' % (user,)
            _shell = shell if shell else '$SHELL -c'
            _command += "%s '%s'" % (_shell, command,)
        channel = self.execute(_command, use_pty=use_pty)
        return channel, self.host, \
            self.read_output_buffer(
                self.read_output(channel, timeout=timeout),
                encoding=encoding), \
            self.read_output_buffer(
                self.read_stderr(channel, timeout=timeout), encoding=encoding,
                prefix='\t[err]'), channel

    def _make_sftp(self):
        """Make SFTP client from open transport"""
        try:
            sftp = self.session.sftp_init()
        except Exception as ex:
            raise SFTPError(ex)
        while sftp == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                sftp = self.session.sftp_init()
            except Exception as ex:
                raise SFTPError(ex)
        return sftp

    def _mkdir(self, sftp, directory):
        """Make directory via SFTP channel

        :param sftp: SFTP client object
        :type sftp: :py:class:`ssh2.sftp.SFTP`
        :param directory: Remote directory to create
        :type directory: str

        :raises: :py:class:`pssh.exceptions.SFTPIOError` on SFTP IO errors
        """
        mode = LIBSSH2_SFTP_S_IRUSR | \
            LIBSSH2_SFTP_S_IWUSR | \
            LIBSSH2_SFTP_S_IXUSR | \
            LIBSSH2_SFTP_S_IRGRP | \
            LIBSSH2_SFTP_S_IROTH | \
            LIBSSH2_SFTP_S_IXGRP | \
            LIBSSH2_SFTP_S_IXOTH
        try:
            self._eagain(sftp.mkdir, directory, mode)
        except SFTPProtocolError as error:
            msg = "Error occured creating directory %s on host %s - %s"
            logger.error(msg, directory, self.host, error)
            raise SFTPIOError(msg, directory, self.host, error)
        logger.debug("Created remote directory %s", directory)

    def copy_file(self, local_file, remote_file, recurse=False,
                  sftp=None, _dir=None):
        """Copy local file to host via SFTP.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        if os.path.isdir(local_file) and recurse:
            return self._copy_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be true if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self.sftp_put(sftp, local_file, remote_file)
        logger.info("Copied local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _sftp_put(self, remote_fh, local_file):
        with open(local_file, 'rb') as local_fh:
            for data in local_fh:
                self._eagain(remote_fh.write, data)

    def sftp_put(self, sftp, local_file, remote_file):
        mode = LIBSSH2_SFTP_S_IRUSR | \
               LIBSSH2_SFTP_S_IWUSR | \
               LIBSSH2_SFTP_S_IRGRP | \
               LIBSSH2_SFTP_S_IROTH
        f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC
        with self._sftp_openfh(
                sftp.open, remote_file, f_flags, mode) as remote_fh:
            try:
                self._sftp_put(remote_fh, local_file)
                # THREAD_POOL.apply(
                #     sftp_put, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error writing to remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def mkdir(self, sftp, directory, _parent_path=None):
        """Make directory via SFTP channel.

        Parent paths in the directory are created if they do not exist.

        :param sftp: SFTP client object
        :type sftp: :py:class:`paramiko.sftp_client.SFTPClient`
        :param directory: Remote directory to create
        :type directory: str

        Catches and logs at error level remote IOErrors on creating directory.
        """
        try:
            _dir, sub_dirs = directory.split('/', 1)
        except ValueError:
            _dir = directory.split('/', 1)[0]
            sub_dirs = None
        if not _dir and directory.startswith('/'):
            try:
                _dir, sub_dirs = sub_dirs.split(os.path.sep, 1)
            except ValueError:
                return True
        if _parent_path is not None:
            _dir = '/'.join((_parent_path, _dir))
        try:
            self._eagain(sftp.stat, _dir)
        except (SFTPHandleError, SFTPProtocolError) as ex:
            logger.debug("Stat for %s failed with %s", _dir, ex)
            self._mkdir(sftp, _dir)
        if sub_dirs is not None:
            if directory.startswith('/'):
                _dir = ''.join(('/', _dir))
            return self.mkdir(sftp, sub_dirs, _parent_path=_dir)

    def _copy_dir(self, local_dir, remote_dir, sftp):
        """Call copy_file on every file in the specified directory, copying
        them to the specified remote directory."""
        file_list = os.listdir(local_dir)
        for file_name in file_list:
            local_path = os.path.join(local_dir, file_name)
            remote_path = '/'.join([remote_dir, file_name])
            self.copy_file(local_path, remote_path, recurse=True,
                           sftp=sftp)

    def copy_remote_file(self, remote_file, local_file, recurse=False,
                         sftp=None, encoding='utf-8'):
        """Copy remote file to local host via SFTP.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths.
        :type encoding: str

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors reading
          from SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        try:
            self._eagain(sftp.stat, remote_file)
        except (SFTPHandleError, SFTPProtocolError):
            msg = "Remote file or directory %s does not exist"
            logger.error(msg, remote_file)
            raise SFTPIOError(msg, remote_file)
        try:
            dir_h = self._sftp_openfh(sftp.opendir, remote_file)
        except SFTPError:
            pass
        else:
            if not recurse:
                raise ValueError("Recurse must be true if remote_file is a "
                                 "directory.")
            file_list = self._sftp_readdir(dir_h)
            return self._copy_remote_dir(file_list, remote_file,
                                         local_file, sftp,
                                         encoding=encoding)
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self.sftp_get(sftp, remote_file, local_file)
        logger.info("Copied local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def scp_recv(self, remote_file, local_file, recurse=False, sftp=None,
                 encoding='utf-8'):
        """Copy remote file to local host via SCP.

        Note - Remote directory listings are gather via SFTP when
        ``recurse`` is enabled - SCP lacks directory list support.
        Enabling recursion therefore involves creating an extra SFTP channel
        and requires SFTP support on the server.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths when recursion is
          enabled.
        :type encoding: str

        :raises: :py:class:`pssh.exceptions.SCPError` when a directory is
          supplied to ``local_file`` and ``recurse`` is not set.
        :raises: :py:class:`pssh.exceptions.SCPError` on errors copying file.
        :raises: :py:class:`IOError` on local file IO errors.
        :raises: :py:class:`OSError` on local OS errors like permission denied.
        """
        sftp = self._make_sftp() if (sftp is None and recurse) else sftp
        if recurse:
            try:
                self._eagain(sftp.stat, remote_file)
            except (SFTPHandleError, SFTPProtocolError):
                msg = "Remote file or directory %s does not exist"
                logger.error(msg, remote_file)
                raise SCPError(msg, remote_file)
            try:
                dir_h = self._sftp_openfh(sftp.opendir, remote_file)
            except SFTPError:
                pass
            else:
                file_list = self._sftp_readdir(dir_h)
                return self._scp_recv_dir(file_list, remote_file,
                                          local_file, sftp,
                                          encoding=encoding)
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self._scp_recv(remote_file, local_file)
        logger.info("SCP local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_recv(self, remote_file, local_file):
        try:
            (file_chan, fileinfo) = self._eagain(
                self.session.scp_recv2, remote_file)
        except Exception as ex:
            msg = "Error copying file %s from host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        local_fh = open(local_file, 'wb')
        try:
            total = 0
            size, data = file_chan.read(size=fileinfo.st_size)
            while size == LIBSSH2_ERROR_EAGAIN:
                wait_select(self.session)
                size, data = file_chan.read(size=fileinfo.st_size)
            total += size
            local_fh.write(data)
            while total < fileinfo.st_size:
                size, data = file_chan.read(size=fileinfo.st_size - total)
                while size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                total += size
                local_fh.write(data)
            if total != fileinfo.st_size:
                msg = "Error copying data from remote file %s on host %s. " \
                      "Copied %s out of %s total bytes"
                raise SCPError(msg, remote_file, self.host, total,
                               fileinfo.st_size)
        finally:
            local_fh.close()

    def _scp_send_dir(self, local_dir, remote_dir, sftp):
        file_list = os.listdir(local_dir)
        for file_name in file_list:
            local_path = os.path.join(local_dir, file_name)
            remote_path = '/'.join([remote_dir, file_name])
            self.scp_send(local_path, remote_path, recurse=True,
                          sftp=sftp)

    def _scp_recv_dir(self, file_list, remote_dir, local_dir, sftp,
                      encoding='utf-8'):
        for file_name in file_list:
            file_name = file_name.decode(encoding)
            if file_name in ('.', '..'):
                continue
            remote_path = os.path.join(remote_dir, file_name)
            local_path = os.path.join(local_dir, file_name)
            logger.debug("Attempting recursive copy from %s:%s to %s",
                         self.host, remote_path, local_path)
            self.scp_recv(remote_path, local_path, sftp=sftp,
                          recurse=True)

    def scp_send(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SCP.

        Note - Directories are created via SFTP when ``recurse`` is enabled -
        SCP lacks directory create support. Enabling recursion therefore
        involves creating an extra SFTP channel and requires SFTP support on the
        server.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        if os.path.isdir(local_file) and recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            return self._scp_send_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be True if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            sftp = self._make_sftp() if sftp is None else sftp
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self._scp_send(local_file, remote_file)
        logger.info("SCP local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_send(self, local_file, remote_file):
        fileinfo = os.stat(local_file)
        try:
            chan = self._eagain(
                self.session.scp_send64,
                remote_file, fileinfo.st_mode & 777, fileinfo.st_size,
                fileinfo.st_mtime, fileinfo.st_atime)
        except Exception as ex:
            msg = "Error opening remote file %s for writing on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        try:
            with open(local_file, 'rb') as local_fh:
                for data in local_fh:
                    chan.write(data)
        except Exception as ex:
            msg = "Error writing to remote file %s on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)

    def _sftp_readdir(self, dir_h):
        for size, buf, attrs in dir_h.readdir():
            for line in buf.splitlines():
                yield line

    def _sftp_openfh(self, open_func, remote_file, *args):
        try:
            fh = open_func(remote_file, *args)
        except Exception as ex:
            raise SFTPError(ex)
        while fh == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session, timeout=0.1)
            try:
                fh = open_func(remote_file, *args)
            except Exception as ex:
                raise SFTPError(ex)
        return fh

    def _sftp_get(self, remote_fh, local_file):
        with open(local_file, 'wb') as local_fh:
            for size, data in remote_fh:
                if size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                local_fh.write(data)

    def sftp_get(self, sftp, remote_file, local_file):
        with self._sftp_openfh(
                sftp.open, remote_file,
                LIBSSH2_FXF_READ, LIBSSH2_SFTP_S_IRUSR) as remote_fh:
            try:
                self._sftp_get(remote_fh, local_file)
                # Running SFTP in a thread requires a new session
                # as session handles or any handles created by a session
                # cannot be used simultaneously in multiple threads.
                # THREAD_POOL.apply(
                #     sftp_get, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error reading from remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def _copy_remote_dir(self, file_list, remote_dir, local_dir, sftp,
                         encoding='utf-8'):
        for file_name in file_list:
            file_name = file_name.decode(encoding)
            if file_name in ('.', '..'):
                continue
            remote_path = os.path.join(remote_dir, file_name)
            local_path = os.path.join(local_dir, file_name)
            self.copy_remote_file(remote_path, local_path, sftp=sftp,
                                  recurse=True)

    def _make_local_dir(self, dirpath):
        if os.path.exists(dirpath):
            return
        try:
            os.makedirs(dirpath)
        except OSError:
            logger.error("Unable to create local directory structure for "
                         "directory %s", dirpath)
            raise

    def _remote_paths_split(self, file_path):
        _sep = file_path.rfind('/')
        if _sep > 0:
            return file_path[:_sep]
        return
示例#16
0
class SSHClient(object):
    """ssh2-python (libssh2) based non-blocking SSH client."""

    IDENTITIES = (
        os.path.expanduser('~/.ssh/id_rsa'),
        os.path.expanduser('~/.ssh/id_dsa'),
        os.path.expanduser('~/.ssh/identity'),
        os.path.expanduser('~/.ssh/id_ecdsa'),
    )

    def __init__(self, host,
                 user=None, password=None, port=None,
                 pkey=None,
                 num_retries=DEFAULT_RETRIES,
                 retry_delay=RETRY_DELAY,
                 allow_agent=True, timeout=None,
                 forward_ssh_agent=False,
                 proxy_host=None,
                 _auth_thread_pool=True, keepalive_seconds=60):
        """:param host: Host name or IP to connect to.
        :type host: str
        :param user: User to connect as. Defaults to logged in user.
        :type user: str
        :param password: Password to use for password authentication.
        :type password: str
        :param port: SSH port to connect to. Defaults to SSH default (22)
        :type port: int
        :param pkey: Private key file path to use for authentication. Path must
          be either absolute path or relative to user home directory
          like ``~/<path>``.
        :type pkey: str
        :param num_retries: (Optional) Number of connection and authentication
          attempts before the client gives up. Defaults to 3.
        :type num_retries: int
        :param retry_delay: Number of seconds to wait between retries. Defaults
          to :py:class:`pssh.constants.RETRY_DELAY`
        :type retry_delay: int
        :param timeout: SSH session timeout setting in seconds. This controls
          timeout setting of authenticated SSH sessions.
        :type timeout: int
        :param allow_agent: (Optional) set to False to disable connecting to
          the system's SSH agent
        :type allow_agent: bool
        :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding -
          equivalent to `ssh -A` from the `ssh` command line utility.
          Defaults to True if not set.
        :type forward_ssh_agent: bool
        :param proxy_host: Connection to host is via provided proxy host
          and client should use self.proxy_host for connection attempts.
        :type proxy_host: str
        :param keepalive_seconds: Interval of keep alive messages being sent to
          server. Set to ``0`` or ``False`` to disable.

        :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
          provided private key.
        """
        self.host = host
        self.user = user if user else None
        if self.user is None and not WIN_PLATFORM:
            self.user = pwd.getpwuid(os.geteuid()).pw_name
        elif self.user is None and WIN_PLATFORM:
            raise ValueError("Must provide user parameter on Windows")
        self.password = password
        self.port = port if port else 22
        self.num_retries = num_retries
        self.sock = None
        self.timeout = timeout
        self.retry_delay = retry_delay
        self.allow_agent = allow_agent
        self.forward_ssh_agent = forward_ssh_agent
        self._forward_requested = False
        self.session = None
        self.keepalive_seconds = keepalive_seconds
        self._keepalive_greenlet = None
        self._host = proxy_host if proxy_host else host
        self.pkey = _validate_pkey_path(pkey, self.host)
        self._connect(self._host, self.port)
        if _auth_thread_pool:
            THREAD_POOL.apply(self._init)
        else:
            self._init()

    def disconnect(self):
        """Disconnect session, close socket if needed."""
        if self.session is not None:
            try:
                self._eagain(self.session.disconnect)
            except Exception:
                pass
        if self.sock is not None and not self.sock.closed:
            self.sock.close()

    def __del__(self):
        self.disconnect()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.disconnect()

    def spawn_send_keepalive(self):
        """Spawns a new greenlet that sends keep alive messages every
        self.keepalive_seconds"""
        return spawn(self._send_keepalive)

    def _send_keepalive(self):
        while True:
            sleep(self._eagain(self.session.keepalive_send))

    def configure_keepalive(self):
        self.session.keepalive_config(False, self.keepalive_seconds)

    def _connect_init_retry(self, retries):
        retries += 1
        self.session = None
        if not self.sock.closed:
            self.sock.close()
        sleep(self.retry_delay)
        self._connect(self._host, self.port, retries=retries)
        return self._init(retries=retries)

    def _init(self, retries=1):
        self.session = Session()
        if self.timeout:
            # libssh2 timeout is in ms
            self.session.set_timeout(self.timeout * 1000)
        try:
            self.session.handshake(self.sock)
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Error connecting to host %s:%s - %s"
            logger.error(msg, self.host, self.port, ex)
            if isinstance(ex, SSH2Timeout):
                raise Timeout(msg, self.host, self.port, ex)
            raise
        try:
            self.auth()
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Authentication error while connecting to %s:%s - %s"
            raise AuthenticationException(msg, self.host, self.port, ex)
        self.session.set_blocking(0)
        if self.keepalive_seconds:
            self.configure_keepalive()
            self._keepalive_greenlet = self.spawn_send_keepalive()

    def _connect(self, host, port, retries=1):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        if self.timeout:
            self.sock.settimeout(self.timeout)
        logger.debug("Connecting to %s:%s", host, port)
        try:
            self.sock.connect((host, port))
        except sock_gaierror as ex:
            logger.error("Could not resolve host '%s' - retry %s/%s",
                         host, retries, self.num_retries)
            while retries < self.num_retries:
                sleep(self.retry_delay)
                return self._connect(host, port, retries=retries+1)
            raise UnknownHostException("Unknown host %s - %s - retry %s/%s",
                                       host, str(ex.args[1]), retries,
                                       self.num_retries)
        except sock_error as ex:
            logger.error("Error connecting to host '%s:%s' - retry %s/%s",
                         host, port, retries, self.num_retries)
            while retries < self.num_retries:
                sleep(self.retry_delay)
                return self._connect(host, port, retries=retries+1)
            error_type = ex.args[1] if len(ex.args) > 1 else ex.args[0]
            raise ConnectionErrorException(
                "Error connecting to host '%s:%s' - %s - retry %s/%s",
                host, port, str(error_type), retries,
                self.num_retries,)

    def _pkey_auth(self):
        self.session.userauth_publickey_fromfile(
            self.user,
            self.pkey,
            passphrase=self.password if self.password is not None else '')

    def _identity_auth(self):
        passphrase = self.password if self.password is not None else ''
        for identity_file in self.IDENTITIES:
            if not os.path.isfile(identity_file):
                continue
            logger.debug(
                "Trying to authenticate with identity file %s",
                identity_file)
            try:
                self.session.userauth_publickey_fromfile(
                    self.user,
                    identity_file,
                    passphrase=passphrase)
            except Exception:
                logger.debug("Authentication with identity file %s failed, "
                             "continuing with other identities",
                             identity_file)
                continue
            else:
                logger.debug("Authentication succeeded with identity file %s",
                             identity_file)
                return
        raise AuthenticationException("No authentication methods succeeded")

    def auth(self):
        if self.pkey is not None:
            logger.debug(
                "Proceeding with private key file authentication")
            return self._pkey_auth()
        if self.allow_agent:
            try:
                self.session.agent_auth(self.user)
            except Exception as ex:
                logger.debug("Agent auth failed with %s, "
                             "continuing with other authentication methods",
                             ex)
            else:
                logger.debug("Authentication with SSH Agent succeeded")
                return
        try:
            self._identity_auth()
        except AuthenticationException:
            if self.password is None:
                raise
            logger.debug("Private key auth failed, trying password")
            self._password_auth()

    def _password_auth(self):
        try:
            self.session.userauth_password(self.user, self.password)
        except Exception:
            raise AuthenticationException("Password authentication failed")

    def open_session(self):
        """Open new channel from session"""
        try:
            chan = self.session.open_session()
        except Exception as ex:
            raise SessionError(ex)
        while chan == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                chan = self.session.open_session()
            except Exception as ex:
                raise SessionError(ex)
        # Multiple forward requests result in ChannelRequestDenied
        # errors, flag is used to avoid this.
        if self.forward_ssh_agent and not self._forward_requested:
            if not hasattr(chan, 'request_auth_agent'):
                warn("Requested SSH Agent forwarding but libssh2 version used "
                     "does not support it - ignoring")
                return chan
            self._eagain(chan.request_auth_agent)
            self._forward_requested = True
        return chan

    def execute(self, cmd, use_pty=False, channel=None):
        """Execute command on remote server.

        :param cmd: Command to execute.
        :type cmd: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param channel: Use provided channel for execute rather than creating
          a new one.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        channel = self.open_session() if channel is None else channel
        if use_pty:
            self._eagain(channel.pty)
        logger.debug("Executing command '%s'" % cmd)
        self._eagain(channel.execute, cmd)
        return channel

    def read_stderr(self, channel, timeout=None):
        """Read standard error buffer from channel.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        return _read_output(self.session, channel.read_stderr, timeout=timeout)

    def read_output(self, channel, timeout=None):
        """Read standard output buffer from channel.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        return _read_output(self.session, channel.read, timeout=timeout)

    def _select_timeout(self, func, timeout):
        ret = func()
        while ret == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session, timeout=timeout)
            ret = func()
            if ret == LIBSSH2_ERROR_EAGAIN and timeout is not None:
                raise Timeout

    def wait_finished(self, channel, timeout=None):
        """Wait for EOF from channel and close channel.

        Used to wait for remote command completion and be able to gather
        exit code.

        :param channel: The channel to use.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        if channel is None:
            return
        # If .eof() returns EAGAIN after a select with a timeout, it means
        # it reached timeout without EOF and _select_timeout will raise
        # timeout exception causing the channel to appropriately
        # not be closed as the command is still running.
        self._select_timeout(channel.wait_eof, timeout)
        # Close channel to indicate no more commands will be sent over it
        self.close_channel(channel)

    def close_channel(self, channel):
        logger.debug("Closing channel")
        self._eagain(channel.close)

    def _eagain(self, func, *args, **kwargs):
        ret = func(*args, **kwargs)
        while ret == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            ret = func(*args, **kwargs)
        return ret

    def read_output_buffer(self, output_buffer, prefix=None,
                           callback=None,
                           callback_args=None,
                           encoding='utf-8'):
        """Read from output buffers and log to ``host_logger``.

        :param output_buffer: Iterator containing buffer
        :type output_buffer: iterator
        :param prefix: String to prefix log output to ``host_logger`` with
        :type prefix: str
        :param callback: Function to call back once buffer is depleted:
        :type callback: function
        :param callback_args: Arguments for call back function
        :type callback_args: tuple
        """
        prefix = '' if prefix is None else prefix
        for line in output_buffer:
            output = line.decode(encoding)
            host_logger.info("[%s]%s\t%s", self.host, prefix, output)
            yield output
        if callback:
            callback(*callback_args)

    def run_command(self, command, sudo=False, user=None,
                    use_pty=False, shell=None,
                    encoding='utf-8', timeout=None):
        """Run remote command.

        :param command: Command to run.
        :type command: str
        :param sudo: Run command via sudo as super-user.
        :type sudo: bool
        :param user: Run command as user via sudo
        :type user: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param shell: (Optional) Override shell to use to run command with.
          Defaults to login user's defined shell. Use the shell's command
          syntax, eg `shell='bash -c'` or `shell='zsh -c'`.
        :type shell: str
        :param encoding: Encoding to use for output. Must be valid
          `Python codec <https://docs.python.org/2.7/library/codecs.html>`_
        :type encoding: str

        :rtype: (channel, host, stdout, stderr, stdin) tuple.
        """
        # Fast path for no command substitution needed
        if not sudo and not user and not shell:
            _command = command
        else:
            _command = ''
            if sudo and not user:
                _command = 'sudo -S '
            elif user:
                _command = 'sudo -u %s -S ' % (user,)
            _shell = shell if shell else '$SHELL -c'
            _command += "%s '%s'" % (_shell, command,)
        channel = self.execute(_command, use_pty=use_pty)
        return channel, self.host, \
            self.read_output_buffer(
                self.read_output(channel, timeout=timeout),
                encoding=encoding), \
            self.read_output_buffer(
                self.read_stderr(channel, timeout=timeout), encoding=encoding,
                prefix='\t[err]'), channel

    def _make_sftp(self):
        """Make SFTP client from open transport"""
        try:
            sftp = self.session.sftp_init()
        except Exception as ex:
            raise SFTPError(ex)
        while sftp == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                sftp = self.session.sftp_init()
            except Exception as ex:
                raise SFTPError(ex)
        return sftp

    def _mkdir(self, sftp, directory):
        """Make directory via SFTP channel

        :param sftp: SFTP client object
        :type sftp: :py:class:`ssh2.sftp.SFTP`
        :param directory: Remote directory to create
        :type directory: str

        :raises: :py:class:`pssh.exceptions.SFTPIOError` on SFTP IO errors
        """
        mode = LIBSSH2_SFTP_S_IRUSR | \
            LIBSSH2_SFTP_S_IWUSR | \
            LIBSSH2_SFTP_S_IXUSR | \
            LIBSSH2_SFTP_S_IRGRP | \
            LIBSSH2_SFTP_S_IROTH | \
            LIBSSH2_SFTP_S_IXGRP | \
            LIBSSH2_SFTP_S_IXOTH
        try:
            self._eagain(sftp.mkdir, directory, mode)
        except SFTPProtocolError as error:
            msg = "Error occured creating directory %s on host %s - %s"
            logger.error(msg, directory, self.host, error)
            raise SFTPIOError(msg, directory, self.host, error)
        logger.debug("Created remote directory %s", directory)

    def copy_file(self, local_file, remote_file, recurse=False,
                  sftp=None, _dir=None):
        """Copy local file to host via SFTP.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        if os.path.isdir(local_file) and recurse:
            return self._copy_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be true if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self.sftp_put(sftp, local_file, remote_file)
        logger.info("Copied local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _sftp_put(self, remote_fh, local_file):
        with open(local_file, 'rb', 2097152) as local_fh:
            for data in local_fh:
                eagain_write(remote_fh.write, data, self.session)

    def sftp_put(self, sftp, local_file, remote_file):
        mode = LIBSSH2_SFTP_S_IRUSR | \
               LIBSSH2_SFTP_S_IWUSR | \
               LIBSSH2_SFTP_S_IRGRP | \
               LIBSSH2_SFTP_S_IROTH
        f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC
        with self._sftp_openfh(
                sftp.open, remote_file, f_flags, mode) as remote_fh:
            try:
                self._sftp_put(remote_fh, local_file)
                # THREAD_POOL.apply(
                #     sftp_put, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error writing to remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def mkdir(self, sftp, directory, _parent_path=None):
        """Make directory via SFTP channel.

        Parent paths in the directory are created if they do not exist.

        :param sftp: SFTP client object
        :type sftp: :py:class:`paramiko.sftp_client.SFTPClient`
        :param directory: Remote directory to create
        :type directory: str

        Catches and logs at error level remote IOErrors on creating directory.
        """
        try:
            _dir, sub_dirs = directory.split('/', 1)
        except ValueError:
            _dir = directory.split('/', 1)[0]
            sub_dirs = None
        if not _dir and directory.startswith('/'):
            try:
                _dir, sub_dirs = sub_dirs.split(os.path.sep, 1)
            except ValueError:
                return True
        if _parent_path is not None:
            _dir = '/'.join((_parent_path, _dir))
        try:
            self._eagain(sftp.stat, _dir)
        except (SFTPHandleError, SFTPProtocolError) as ex:
            logger.debug("Stat for %s failed with %s", _dir, ex)
            self._mkdir(sftp, _dir)
        if sub_dirs is not None:
            if directory.startswith('/'):
                _dir = ''.join(('/', _dir))
            return self.mkdir(sftp, sub_dirs, _parent_path=_dir)

    def _copy_dir(self, local_dir, remote_dir, sftp):
        """Call copy_file on every file in the specified directory, copying
        them to the specified remote directory."""
        file_list = os.listdir(local_dir)
        for file_name in file_list:
            local_path = os.path.join(local_dir, file_name)
            remote_path = '/'.join([remote_dir, file_name])
            self.copy_file(local_path, remote_path, recurse=True,
                           sftp=sftp)

    def copy_remote_file(self, remote_file, local_file, recurse=False,
                         sftp=None, encoding='utf-8'):
        """Copy remote file to local host via SFTP.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths.
        :type encoding: str

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors reading
          from SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        try:
            self._eagain(sftp.stat, remote_file)
        except (SFTPHandleError, SFTPProtocolError):
            msg = "Remote file or directory %s does not exist"
            logger.error(msg, remote_file)
            raise SFTPIOError(msg, remote_file)
        try:
            dir_h = self._sftp_openfh(sftp.opendir, remote_file)
        except SFTPError:
            pass
        else:
            if not recurse:
                raise ValueError("Recurse must be true if remote_file is a "
                                 "directory.")
            file_list = self._sftp_readdir(dir_h)
            return self._copy_remote_dir(file_list, remote_file,
                                         local_file, sftp,
                                         encoding=encoding)
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self.sftp_get(sftp, remote_file, local_file)
        logger.info("Copied local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def scp_recv(self, remote_file, local_file, recurse=False, sftp=None,
                 encoding='utf-8'):
        """Copy remote file to local host via SCP.

        Note - Remote directory listings are gather via SFTP when
        ``recurse`` is enabled - SCP lacks directory list support.
        Enabling recursion therefore involves creating an extra SFTP channel
        and requires SFTP support on the server.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths when recursion is
          enabled.
        :type encoding: str

        :raises: :py:class:`pssh.exceptions.SCPError` when a directory is
          supplied to ``local_file`` and ``recurse`` is not set.
        :raises: :py:class:`pssh.exceptions.SCPError` on errors copying file.
        :raises: :py:class:`IOError` on local file IO errors.
        :raises: :py:class:`OSError` on local OS errors like permission denied.
        """
        sftp = self._make_sftp() if (sftp is None and recurse) else sftp
        if recurse:
            try:
                self._eagain(sftp.stat, remote_file)
            except (SFTPHandleError, SFTPProtocolError):
                msg = "Remote file or directory %s does not exist"
                logger.error(msg, remote_file)
                raise SCPError(msg, remote_file)
            try:
                dir_h = self._sftp_openfh(sftp.opendir, remote_file)
            except SFTPError:
                pass
            else:
                file_list = self._sftp_readdir(dir_h)
                return self._scp_recv_dir(file_list, remote_file,
                                          local_file, sftp,
                                          encoding=encoding)
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self._scp_recv(remote_file, local_file)
        logger.info("SCP local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_recv(self, remote_file, local_file):
        try:
            (file_chan, fileinfo) = self._eagain(
                self.session.scp_recv2, remote_file)
        except Exception as ex:
            msg = "Error copying file %s from host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        local_fh = open(local_file, 'wb')
        try:
            total = 0
            size, data = file_chan.read(size=fileinfo.st_size)
            while size == LIBSSH2_ERROR_EAGAIN:
                wait_select(self.session)
                size, data = file_chan.read(size=fileinfo.st_size)
            total += size
            local_fh.write(data)
            while total < fileinfo.st_size:
                size, data = file_chan.read(size=fileinfo.st_size - total)
                while size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                total += size
                local_fh.write(data)
            if total != fileinfo.st_size:
                msg = "Error copying data from remote file %s on host %s. " \
                      "Copied %s out of %s total bytes"
                raise SCPError(msg, remote_file, self.host, total,
                               fileinfo.st_size)
        finally:
            local_fh.close()

    def _scp_send_dir(self, local_dir, remote_dir, sftp):
        file_list = os.listdir(local_dir)
        for file_name in file_list:
            local_path = os.path.join(local_dir, file_name)
            remote_path = '/'.join([remote_dir, file_name])
            self.scp_send(local_path, remote_path, recurse=True,
                          sftp=sftp)

    def _scp_recv_dir(self, file_list, remote_dir, local_dir, sftp,
                      encoding='utf-8'):
        for file_name in file_list:
            file_name = file_name.decode(encoding)
            if file_name in ('.', '..'):
                continue
            remote_path = os.path.join(remote_dir, file_name)
            local_path = os.path.join(local_dir, file_name)
            logger.debug("Attempting recursive copy from %s:%s to %s",
                         self.host, remote_path, local_path)
            self.scp_recv(remote_path, local_path, sftp=sftp,
                          recurse=True)

    def scp_send(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SCP.

        Note - Directories are created via SFTP when ``recurse`` is enabled -
        SCP lacks directory create support. Enabling recursion therefore
        involves creating an extra SFTP channel and requires SFTP support on the
        server.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        if os.path.isdir(local_file) and recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            return self._scp_send_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be True if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            sftp = self._make_sftp() if sftp is None else sftp
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self._scp_send(local_file, remote_file)
        logger.info("SCP local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_send(self, local_file, remote_file):
        fileinfo = os.stat(local_file)
        try:
            chan = self._eagain(
                self.session.scp_send64,
                remote_file, fileinfo.st_mode & 0o777, fileinfo.st_size,
                fileinfo.st_mtime, fileinfo.st_atime)
        except Exception as ex:
            msg = "Error opening remote file %s for writing on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        try:
            with open(local_file, 'rb', 2097152) as local_fh:
                for data in local_fh:
                    eagain_write(chan.write, data, self.session)
        except Exception as ex:
            msg = "Error writing to remote file %s on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)

    def _sftp_readdir(self, dir_h):
        for size, buf, attrs in dir_h.readdir():
            for line in buf.splitlines():
                yield line

    def _sftp_openfh(self, open_func, remote_file, *args):
        try:
            fh = open_func(remote_file, *args)
        except Exception as ex:
            raise SFTPError(ex)
        while fh == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session, timeout=0.1)
            try:
                fh = open_func(remote_file, *args)
            except Exception as ex:
                raise SFTPError(ex)
        return fh

    def _sftp_get(self, remote_fh, local_file):
        with open(local_file, 'wb') as local_fh:
            for size, data in remote_fh:
                if size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                local_fh.write(data)

    def sftp_get(self, sftp, remote_file, local_file):
        with self._sftp_openfh(
                sftp.open, remote_file,
                LIBSSH2_FXF_READ, LIBSSH2_SFTP_S_IRUSR) as remote_fh:
            try:
                self._sftp_get(remote_fh, local_file)
                # Running SFTP in a thread requires a new session
                # as session handles or any handles created by a session
                # cannot be used simultaneously in multiple threads.
                # THREAD_POOL.apply(
                #     sftp_get, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error reading from remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def _copy_remote_dir(self, file_list, remote_dir, local_dir, sftp,
                         encoding='utf-8'):
        for file_name in file_list:
            file_name = file_name.decode(encoding)
            if file_name in ('.', '..'):
                continue
            remote_path = os.path.join(remote_dir, file_name)
            local_path = os.path.join(local_dir, file_name)
            self.copy_remote_file(remote_path, local_path, sftp=sftp,
                                  recurse=True)

    def _make_local_dir(self, dirpath):
        if os.path.exists(dirpath):
            return
        try:
            os.makedirs(dirpath)
        except OSError:
            logger.error("Unable to create local directory structure for "
                         "directory %s", dirpath)
            raise

    def _remote_paths_split(self, file_path):
        _sep = file_path.rfind('/')
        if _sep > 0:
            return file_path[:_sep]
        return
示例#17
0
class SSHClient(BaseSSHClient):
    """ssh2-python (libssh2) based non-blocking SSH client."""

    def __init__(self, host,
                 user=None, password=None, port=None,
                 pkey=None,
                 num_retries=DEFAULT_RETRIES,
                 retry_delay=RETRY_DELAY,
                 allow_agent=True, timeout=None,
                 forward_ssh_agent=False,
                 proxy_host=None,
                 _auth_thread_pool=True, keepalive_seconds=60,
                 identity_auth=True,):
        """:param host: Host name or IP to connect to.
        :type host: str
        :param user: User to connect as. Defaults to logged in user.
        :type user: str
        :param password: Password to use for password authentication.
        :type password: str
        :param port: SSH port to connect to. Defaults to SSH default (22)
        :type port: int
        :param pkey: Private key file path to use for authentication. Path must
          be either absolute path or relative to user home directory
          like ``~/<path>``.
        :type pkey: str
        :param num_retries: (Optional) Number of connection and authentication
          attempts before the client gives up. Defaults to 3.
        :type num_retries: int
        :param retry_delay: Number of seconds to wait between retries. Defaults
          to :py:class:`pssh.constants.RETRY_DELAY`
        :type retry_delay: int
        :param timeout: SSH session timeout setting in seconds. This controls
          timeout setting of authenticated SSH sessions.
        :type timeout: int
        :param allow_agent: (Optional) set to False to disable connecting to
          the system's SSH agent
        :type allow_agent: bool
        :param identity_auth: (Optional) set to False to disable attempting to
          authenticate with default identity files from
          `pssh.clients.base_ssh_client.BaseSSHClient.IDENTITIES`
        :type identity_auth: bool
        :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding -
          equivalent to `ssh -A` from the `ssh` command line utility.
          Defaults to True if not set.
        :type forward_ssh_agent: bool
        :param proxy_host: Connection to host is via provided proxy host
          and client should use self.proxy_host for connection attempts.
        :type proxy_host: str
        :param keepalive_seconds: Interval of keep alive messages being sent to
          server. Set to ``0`` or ``False`` to disable.

        :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
          provided private key.
        """
        self.forward_ssh_agent = forward_ssh_agent
        self._forward_requested = False
        self.keepalive_seconds = keepalive_seconds
        self._keepalive_greenlet = None
        super(SSHClient, self).__init__(
            host, user=user, password=password, port=port, pkey=pkey,
            num_retries=num_retries, retry_delay=retry_delay,
            allow_agent=allow_agent, _auth_thread_pool=_auth_thread_pool,
            timeout=timeout,
            proxy_host=proxy_host, identity_auth=identity_auth)

    def disconnect(self):
        """Disconnect session, close socket if needed."""
        logger.debug("Disconnecting client for host %s", self.host)
        self._keepalive_greenlet = None
        if self.session is not None:
            try:
                self._eagain(self.session.disconnect)
            except Exception:
                pass
            self.session = None
        self.sock = None

    def spawn_send_keepalive(self):
        """Spawns a new greenlet that sends keep alive messages every
        self.keepalive_seconds"""
        return spawn(self._send_keepalive)

    def _send_keepalive(self):
        while True:
            sleep(self._eagain(self.session.keepalive_send))

    def configure_keepalive(self):
        self.session.keepalive_config(False, self.keepalive_seconds)

    def _init(self, retries=1):
        self.session = Session()
        if self.timeout:
            # libssh2 timeout is in ms
            self.session.set_timeout(self.timeout * 1000)
        try:
            self.session.handshake(self.sock)
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Error connecting to host %s:%s - %s"
            logger.error(msg, self.host, self.port, ex)
            if isinstance(ex, SSH2Timeout):
                raise Timeout(msg, self.host, self.port, ex)
            ex.host = self.host
            ex.port = self.port
            raise
        try:
            self.auth()
        except Exception as ex:
            while retries < self.num_retries:
                return self._connect_init_retry(retries)
            msg = "Authentication error while connecting to %s:%s - %s"
            raise AuthenticationException(msg, self.host, self.port, ex)
        self.session.set_blocking(0)
        if self.keepalive_seconds:
            self.configure_keepalive()
            self._keepalive_greenlet = self.spawn_send_keepalive()

    def auth(self):
        if self.pkey is not None:
            logger.debug(
                "Proceeding with private key file authentication")
            return self._pkey_auth(password=self.password)
        if self.allow_agent:
            try:
                self.session.agent_auth(self.user)
            except (AgentAuthenticationError, AgentConnectionError, AgentGetIdentityError,
                    AgentListIdentitiesError) as ex:
                logger.debug("Agent auth failed with %s"
                             "continuing with other authentication methods", ex)
            except Exception as ex:
                logger.error("Unknown error during agent authentication - %s", ex)
            else:
                logger.debug("Authentication with SSH Agent succeeded")
                return
        try:
            self._identity_auth()
        except AuthenticationException:
            if self.password is None:
                raise
            logger.debug("Private key auth failed, trying password")
            self._password_auth()

    def _pkey_auth(self, password=None):
        self.session.userauth_publickey_fromfile(
            self.user,
            self.pkey,
            passphrase=password if password is not None else '')

    def _password_auth(self):
        try:
            self.session.userauth_password(self.user, self.password)
        except Exception:
            raise AuthenticationException("Password authentication failed")

    def open_session(self):
        """Open new channel from session"""
        try:
            chan = self.session.open_session()
        except Exception as ex:
            raise SessionError(ex)
        while chan == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                chan = self.session.open_session()
            except Exception as ex:
                raise SessionError(ex)
        # Multiple forward requests result in ChannelRequestDenied
        # errors, flag is used to avoid this.
        if self.forward_ssh_agent and not self._forward_requested:
            if not hasattr(chan, 'request_auth_agent'):
                warn("Requested SSH Agent forwarding but libssh2 version used "
                     "does not support it - ignoring")
                return chan
            self._eagain(chan.request_auth_agent)
            self._forward_requested = True
        return chan

    def execute(self, cmd, use_pty=False, channel=None):
        """Execute command on remote server.

        :param cmd: Command to execute.
        :type cmd: str
        :param use_pty: Whether or not to obtain a PTY on the channel.
        :type use_pty: bool
        :param channel: Use provided channel for execute rather than creating
          a new one.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        channel = self.open_session() if channel is None else channel
        if use_pty:
            self._eagain(channel.pty)
        logger.debug("Executing command '%s'", cmd)
        self._eagain(channel.execute, cmd)
        return channel

    def read_stderr(self, channel, timeout=None):
        """Read standard error buffer from channel.
        Returns a generator of line by line output.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        :rtype: generator
        """
        return _read_output(self.session, channel.read_stderr, timeout=timeout)

    def read_output(self, channel, timeout=None):
        """Read standard output buffer from channel.
        Returns a generator of line by line output.

        :param channel: Channel to read output from.
        :type channel: :py:class:`ssh2.channel.Channel`
        :rtype: generator
        """
        return _read_output(self.session, channel.read, timeout=timeout)

    def wait_finished(self, channel, timeout=None):
        """Wait for EOF from channel and close channel.

        Used to wait for remote command completion and be able to gather
        exit code.

        :param channel: The channel to use.
        :type channel: :py:class:`ssh2.channel.Channel`
        """
        if channel is None:
            return
        # If wait_eof() returns EAGAIN after a select with a timeout, it means
        # it reached timeout without EOF and _select_timeout will raise
        # timeout exception causing the channel to appropriately
        # not be closed as the command is still running.
        self._eagain(channel.wait_eof)
        # Close channel to indicate no more commands will be sent over it
        self.close_channel(channel)

    def close_channel(self, channel):
        logger.debug("Closing channel")
        self._eagain(channel.close)

    def _eagain(self, func, *args, **kwargs):
        ret = func(*args, **kwargs)
        while ret == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            ret = func(*args, **kwargs)
        return ret

    def _make_sftp(self):
        """Make SFTP client from open transport"""
        try:
            sftp = self.session.sftp_init()
        except Exception as ex:
            raise SFTPError(ex)
        while sftp == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session)
            try:
                sftp = self.session.sftp_init()
            except Exception as ex:
                raise SFTPError(ex)
        return sftp

    def _mkdir(self, sftp, directory):
        """Make directory via SFTP channel

        :param sftp: SFTP client object
        :type sftp: :py:class:`ssh2.sftp.SFTP`
        :param directory: Remote directory to create
        :type directory: str

        :raises: :py:class:`pssh.exceptions.SFTPIOError` on SFTP IO errors
        """
        mode = LIBSSH2_SFTP_S_IRUSR | \
            LIBSSH2_SFTP_S_IWUSR | \
            LIBSSH2_SFTP_S_IXUSR | \
            LIBSSH2_SFTP_S_IRGRP | \
            LIBSSH2_SFTP_S_IROTH | \
            LIBSSH2_SFTP_S_IXGRP | \
            LIBSSH2_SFTP_S_IXOTH
        try:
            self._eagain(sftp.mkdir, directory, mode)
        except SFTPProtocolError as error:
            msg = "Error occured creating directory %s on host %s - %s"
            logger.error(msg, directory, self.host, error)
            raise SFTPIOError(msg, directory, self.host, error)
        logger.debug("Created remote directory %s", directory)

    def copy_file(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SFTP.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        if os.path.isdir(local_file) and recurse:
            return self._copy_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be true if local_file is a "
                             "directory.")
        destination = self._remote_paths_split(remote_file)
        if destination is not None:
            try:
                self._eagain(sftp.stat, destination)
            except (SFTPHandleError, SFTPProtocolError):
                self.mkdir(sftp, destination)
        self.sftp_put(sftp, local_file, remote_file)
        logger.info("Copied local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _sftp_put(self, remote_fh, local_file):
        with open(local_file, 'rb', 2097152) as local_fh:
            for data in local_fh:
                eagain_write(remote_fh.write, data, self.session)

    def sftp_put(self, sftp, local_file, remote_file):
        mode = LIBSSH2_SFTP_S_IRUSR | \
               LIBSSH2_SFTP_S_IWUSR | \
               LIBSSH2_SFTP_S_IRGRP | \
               LIBSSH2_SFTP_S_IROTH
        f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC
        with self._sftp_openfh(
                sftp.open, remote_file, f_flags, mode) as remote_fh:
            try:
                self._sftp_put(remote_fh, local_file)
                # THREAD_POOL.apply(
                #     sftp_put, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error writing to remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def mkdir(self, sftp, directory):
        """Make directory via SFTP channel.

        Parent paths in the directory are created if they do not exist.

        :param sftp: SFTP client object
        :type sftp: :py:class:`paramiko.sftp_client.SFTPClient`
        :param directory: Remote directory to create
        :type directory: str

        Catches and logs at error level remote IOErrors on creating directory.
        """
        _paths_to_create = deque()
        for d in directory.split('/'):
            if not d:
                continue
            _paths_to_create.append(d)
        cwd = '' if directory.startswith('/') else '.'
        while _paths_to_create:
            cur_dir = _paths_to_create.popleft()
            cwd = '/'.join([cwd, cur_dir])
            try:
                self._eagain(sftp.stat, cwd)
            except (SFTPHandleError, SFTPProtocolError) as ex:
                logger.debug("Stat for %s failed with %s", cwd, ex)
                self._mkdir(sftp, cwd)

    def copy_remote_file(self, remote_file, local_file, recurse=False,
                         sftp=None, encoding='utf-8'):
        """Copy remote file to local host via SFTP.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths.
        :type encoding: str

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors reading
          from SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        sftp = self._make_sftp() if sftp is None else sftp
        try:
            self._eagain(sftp.stat, remote_file)
        except (SFTPHandleError, SFTPProtocolError):
            msg = "Remote file or directory %s does not exist"
            logger.error(msg, remote_file)
            raise SFTPIOError(msg, remote_file)
        try:
            dir_h = self._sftp_openfh(sftp.opendir, remote_file)
        except SFTPError:
            pass
        else:
            if not recurse:
                raise ValueError("Recurse must be true if remote_file is a "
                                 "directory.")
            file_list = self._sftp_readdir(dir_h)
            return self._copy_remote_dir(file_list, remote_file,
                                         local_file, sftp,
                                         encoding=encoding)
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self.sftp_get(sftp, remote_file, local_file)
        logger.info("Copied local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def scp_recv(self, remote_file, local_file, recurse=False, sftp=None,
                 encoding='utf-8'):
        """Copy remote file to local host via SCP.

        Note - Remote directory listings are gathered via SFTP when
        ``recurse`` is enabled - SCP lacks directory list support.
        Enabling recursion therefore involves creating an extra SFTP channel
        and requires SFTP support on the server.

        :param remote_file: Remote filepath to copy from
        :type remote_file: str
        :param local_file: Local filepath where file(s) will be copied to
        :type local_file: str
        :param recurse: Whether or not to recursively copy directories
        :type recurse: bool
        :param encoding: Encoding to use for file paths when recursion is
          enabled.
        :type encoding: str

        :raises: :py:class:`pssh.exceptions.SCPError` when a directory is
          supplied to ``local_file`` and ``recurse`` is not set.
        :raises: :py:class:`pssh.exceptions.SCPError` on errors copying file.
        :raises: :py:class:`IOError` on local file IO errors.
        :raises: :py:class:`OSError` on local OS errors like permission denied.
        """
        if recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            try:
                self._eagain(sftp.stat, remote_file)
            except (SFTPHandleError, SFTPProtocolError):
                msg = "Remote file or directory %s does not exist"
                logger.error(msg, remote_file)
                raise SCPError(msg, remote_file)
            try:
                dir_h = self._sftp_openfh(sftp.opendir, remote_file)
            except SFTPError:
                pass
            else:
                try:
                    os.makedirs(local_file)
                except OSError:
                    pass
                file_list = self._sftp_readdir(dir_h)
                return self._scp_recv_dir(file_list, remote_file,
                                          local_file, sftp,
                                          encoding=encoding)
        elif local_file.endswith('/'):
            remote_filename = remote_file.rsplit('/')[-1]
            local_file += remote_filename
        destination = os.path.join(os.path.sep, os.path.sep.join(
            [_dir for _dir in local_file.split('/')
             if _dir][:-1]))
        self._make_local_dir(destination)
        self._scp_recv(remote_file, local_file)
        logger.info("SCP local file %s from remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_recv(self, remote_file, local_file):
        try:
            (file_chan, fileinfo) = self._eagain(
                self.session.scp_recv2, remote_file)
        except Exception as ex:
            msg = "Error copying file %s from host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        local_fh = open(local_file, 'wb')
        try:
            total = 0
            while total < fileinfo.st_size:
                size, data = file_chan.read(size=fileinfo.st_size - total)
                if size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                total += size
                local_fh.write(data)
            if total != fileinfo.st_size:
                msg = "Error copying data from remote file %s on host %s. " \
                      "Copied %s out of %s total bytes"
                raise SCPError(msg, remote_file, self.host, total,
                               fileinfo.st_size)
        finally:
            local_fh.close()

    def scp_send(self, local_file, remote_file, recurse=False, sftp=None):
        """Copy local file to host via SCP.

        Note - Directories are created via SFTP when ``recurse`` is enabled -
        SCP lacks directory create support. Enabling recursion therefore
        involves creating an extra SFTP channel and requires SFTP support on the
        server.

        :param local_file: Local filepath to copy to remote host
        :type local_file: str
        :param remote_file: Remote filepath on remote host to copy file to
        :type remote_file: str
        :param recurse: Whether or not to descend into directories recursively.
        :type recurse: bool

        :raises: :py:class:`ValueError` when a directory is supplied to
          ``local_file`` and ``recurse`` is not set
        :raises: :py:class:`pss.exceptions.SFTPError` on SFTP initialisation
          errors
        :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
          via SFTP
        :raises: :py:class:`IOError` on local file IO errors
        :raises: :py:class:`OSError` on local OS errors like permission denied
        """
        if os.path.isdir(local_file) and recurse:
            sftp = self._make_sftp() if sftp is None else sftp
            return self._scp_send_dir(local_file, remote_file, sftp)
        elif os.path.isdir(local_file) and not recurse:
            raise ValueError("Recurse must be True if local_file is a "
                             "directory.")
        if recurse:
            destination = self._remote_paths_split(remote_file)
            if destination is not None:
                sftp = self._make_sftp() if sftp is None else sftp
                try:
                    self._eagain(sftp.stat, destination)
                except (SFTPHandleError, SFTPProtocolError):
                    self.mkdir(sftp, destination)
        elif remote_file.endswith('/'):
            local_filename = local_file.rsplit('/')[-1]
            remote_file += local_filename
        self._scp_send(local_file, remote_file)
        logger.info("SCP local file %s to remote destination %s:%s",
                    local_file, self.host, remote_file)

    def _scp_send(self, local_file, remote_file):
        fileinfo = os.stat(local_file)
        try:
            chan = self._eagain(
                self.session.scp_send64,
                remote_file, fileinfo.st_mode & 0o777, fileinfo.st_size,
                fileinfo.st_mtime, fileinfo.st_atime)
        except Exception as ex:
            msg = "Error opening remote file %s for writing on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)
        try:
            with open(local_file, 'rb', 2097152) as local_fh:
                for data in local_fh:
                    eagain_write(chan.write, data, self.session)
        except Exception as ex:
            msg = "Error writing to remote file %s on host %s - %s"
            logger.error(msg, remote_file, self.host, ex)
            raise SCPError(msg, remote_file, self.host, ex)

    def _sftp_openfh(self, open_func, remote_file, *args):
        try:
            fh = open_func(remote_file, *args)
        except Exception as ex:
            raise SFTPError(ex)
        while fh == LIBSSH2_ERROR_EAGAIN:
            wait_select(self.session, timeout=0.1)
            try:
                fh = open_func(remote_file, *args)
            except Exception as ex:
                raise SFTPError(ex)
        return fh

    def _sftp_get(self, remote_fh, local_file):
        with open(local_file, 'wb') as local_fh:
            for size, data in remote_fh:
                if size == LIBSSH2_ERROR_EAGAIN:
                    wait_select(self.session)
                    continue
                local_fh.write(data)

    def sftp_get(self, sftp, remote_file, local_file):
        with self._sftp_openfh(
                sftp.open, remote_file,
                LIBSSH2_FXF_READ, LIBSSH2_SFTP_S_IRUSR) as remote_fh:
            try:
                self._sftp_get(remote_fh, local_file)
                # Running SFTP in a thread requires a new session
                # as session handles or any handles created by a session
                # cannot be used simultaneously in multiple threads.
                # THREAD_POOL.apply(
                #     sftp_get, args=(self.session, remote_fh, local_file))
            except SFTPProtocolError as ex:
                msg = "Error reading from remote file %s - %s"
                logger.error(msg, remote_file, ex)
                raise SFTPIOError(msg, remote_file, ex)

    def get_exit_status(self, channel):
        if not channel.eof():
            return
        return channel.get_exit_status()

    def finished(self, channel):
        """Checks if remote command has finished - has server sent client
        EOF.

        :rtype: bool
        """
        if channel is None:
            return
        return channel.eof()
示例#18
0
host = 'localhost'
user = os.getlogin()

# Make socket, connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 22))

# Initialise
session = Session()
session.handshake(sock)

# List available authentication methods
print(session.userauth_list(user))

# Convenience function for agent based authentication
session.agent_auth(user)

# Agent capabilities
agent = session.agent_init()
agent.connect()
identities = agent.get_identities()
print(identities)
print(identities[0].magic)
del agent

# Public key blob available as identities[0].blob

# Channel initialise, exec and wait for end
channel = session.open_session()
channel.execute('echo me')
channel.wait_eof()