from ssh.session import Session from ssh import options # Linux only USERNAME = pwd.getpwuid(os.geteuid()).pw_name HOST = 'localhost' PORT = 22 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) s = Session() s.options_set(options.HOST, HOST) s.options_set(options.USER, USERNAME) s.options_set_port(PORT) s.set_socket(sock) s.connect() # Authenticate with agent s.userauth_agent(USERNAME) chan = s.channel_new() chan.open_session() chan.request_exec('echo me') size, data = chan.read() while size > 0: print(data.strip()) size, data = chan.read() chan.close()
class SSHClient(BaseSSHClient): """ssh-python based non-blocking client.""" def __init__(self, host, user=None, password=None, port=None, pkey=None, cert_file=None, num_retries=DEFAULT_RETRIES, retry_delay=RETRY_DELAY, allow_agent=True, timeout=None, identity_auth=True, gssapi_auth=False, gssapi_server_identity=None, gssapi_client_identity=None, gssapi_delegate_credentials=False, _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. Path must be either absolute path or relative to user home directory like ``~/<path>``. :type pkey: str :param cert_file: Public key signed certificate file to use for authentication. The corresponding private key must also be provided via ``pkey`` parameter. For example ``pkey='id_rsa',cert_file='id_rsa-cert.pub'`` for RSA signed certificate. Path must be absolute or relative to user home directory. :type cert_file: 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: (Optional) If provided, all commands will timeout after <timeout> number of seconds. :type timeout: int :param allow_agent: (Optional) set to False to disable connecting to the system's SSH agent. Currently unused. :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 gssapi_server_identity: Enable GSS-API authentication. Uses GSS-MIC key exchange. Enabled if either gssapi_server_identity or gssapi_client_identity are provided. :type gssapi_auth: bool :type gssapi_server_identity: str :param gssapi_server_identity: Set GSSAPI server identity. :type gssapi_server_identity: str :param gssapi_client_identity: Set GSSAPI client identity. :type gssapi_client_identity: str :param gssapi_delegate_credentials: Enable/disable server credentials delegation. :type gssapi_delegate_credentials: bool :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding provided private key. """ self.cert_file = _validate_pkey_path(cert_file, host) self.gssapi_auth = gssapi_auth self.gssapi_server_identity = gssapi_server_identity self.gssapi_client_identity = gssapi_client_identity self.gssapi_delegate_credentials = gssapi_delegate_credentials 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, identity_auth=identity_auth) def disconnect(self): """Close socket if needed.""" if self.sock is not None and not self.sock.closed: self.sock.close() def _agent_auth(self): self.session.userauth_agent(self.user) def _keepalive(self): pass def _init_session(self, retries=1): logger.debug("Starting new session for %s@%s:%s", self.user, self.host, self.port) self.session = Session() self.session.options_set(options.USER, self.user) self.session.options_set(options.HOST, self.host) self.session.options_set_port(self.port) if self.gssapi_server_identity: self.session.options_set(options.GSSAPI_SERVER_IDENTITY, self.gssapi_server_identity) if self.gssapi_client_identity: self.session.options_set(options.GSSAPI_CLIENT_IDENTITY, self.gssapi_client_identity) if self.gssapi_client_identity or self.gssapi_server_identity: self.session.options_set_gssapi_delegate_credentials( self.gssapi_delegate_credentials) self.session.set_socket(self.sock) logger.debug("Session started, connecting with existing socket") try: self._session_connect() except Exception as ex: if retries < self.num_retries: 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) ex.host = self.host ex.port = self.port raise ex def _session_connect(self): self.session.connect() def auth(self): if self.gssapi_auth or (self.gssapi_server_identity or self.gssapi_client_identity): try: return self.session.userauth_gssapi() except Exception as ex: logger.error( "GSSAPI authentication with server id %s and client id %s failed - %s", self.gssapi_server_identity, self.gssapi_client_identity, ex) return super(SSHClient, self).auth() def _password_auth(self): self.session.userauth_password(self.user, self.password) def _pkey_auth(self, pkey_file, password=None): pkey = import_privkey_file( pkey_file, passphrase=password if password is not None else '') if self.cert_file is not None: logger.debug( "Certificate file set - trying certificate authentication") self._import_cert_file(pkey) self.session.userauth_publickey(pkey) def _import_cert_file(self, pkey): cert_key = import_cert_file(self.cert_file) self.session.userauth_try_publickey(cert_key) copy_cert_to_privkey(cert_key, pkey) logger.debug("Imported certificate file %s for pkey %s", self.cert_file, self.pkey) def _shell(self, channel): return self._eagain(channel.request_shell) def _open_session(self): channel = self.session.channel_new() channel.set_blocking(0) self._eagain(channel.open_session) return channel def open_session(self): """Open new channel from session.""" logger.debug("Opening new channel on %s", self.host) try: channel = self._open_session() except Exception as ex: raise SessionError(ex) return channel def _make_output_readers(self, channel, stdout_buffer, stderr_buffer): _stdout_reader = spawn(self._read_output_to_buffer, channel, stdout_buffer) _stderr_reader = spawn(self._read_output_to_buffer, channel, stderr_buffer, is_stderr=True) return _stdout_reader, _stderr_reader def execute(self, cmd, use_pty=False, channel=None): """Execute command on remote host. :param cmd: The command string to execute. :type cmd: str :param use_pty: Whether or not to request a PTY on the channel executing command. :type use_pty: bool :param channel: Channel to use. New channel is created if not provided. :type channel: :py:class:`ssh.channel.Channel`""" channel = self.open_session() if not channel else channel if use_pty: self._eagain(channel.request_pty, timeout=self.timeout) logger.debug("Executing command '%s'", cmd) self._eagain(channel.request_exec, cmd, timeout=self.timeout) return channel def _read_output_to_buffer(self, channel, _buffer, is_stderr=False): while True: self.poll(timeout=self.timeout) try: size, data = channel.read_nonblocking(is_stderr=is_stderr) except EOF: _buffer.eof.set() sleep(.1) return if size > 0: _buffer.write(data) else: # Yield event loop to other greenlets if we have no data to # send back, meaning the generator does not yield and can there # for block other generators/greenlets from running. sleep(.1) 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 set. """ 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 logger.debug("Sending EOF on channel %s", channel) self._eagain(channel.send_eof, timeout=self.timeout) logger.debug("Waiting for readers, timeout %s", timeout) with GTimeout(seconds=timeout, exception=Timeout): joinall((host_output.buffers.stdout.reader, host_output.buffers.stderr.reader)) logger.debug("Readers finished, closing channel") self.close_channel(channel) def finished(self, channel): """Checks if remote command has finished - has server sent client EOF. :rtype: bool """ if channel is None: return return channel.is_eof() 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:`ssh.channel.Channel` :rtype: int or ``None`` """ if not channel.is_eof(): return return channel.get_exit_status() def close_channel(self, channel): """Close channel. :param channel: The channel to close. :type channel: :py:class:`ssh.channel.Channel` """ logger.debug("Closing channel") self._eagain(channel.close, timeout=self.timeout) def poll(self, timeout=None): """ssh-python based co-operative gevent poll on session socket.""" self._poll_errcodes( self.session.get_poll_flags, SSH_READ_PENDING, SSH_WRITE_PENDING, timeout=timeout, ) def _eagain(self, func, *args, **kwargs): """Run function given and handle EAGAIN for an ssh-python session""" return self._eagain_errcode(func, SSH_AGAIN, *args, **kwargs) def _eagain_write(self, write_func, data, timeout=None): return self._eagain_write_errcode(write_func, data, SSH_AGAIN, timeout=timeout)
class SSHClient(BaseSSHClient): """ssh-python based non-blocking 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, identity_auth=True, gssapi_auth=False, gssapi_server_identity=None, gssapi_client_identity=None, gssapi_delegate_credentials=False, _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. 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: (Optional) If provided, all commands will timeout after <timeout> number of seconds. :type timeout: int :param allow_agent: (Optional) set to False to disable connecting to the system's SSH agent. Currently unused. :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 gssapi_server_identity: Enable GSS-API authentication. Uses GSS-MIC key exchange. Enabled if either gssapi_server_identity or gssapi_client_identity are provided. :type gssapi_auth: bool :type gssapi_server_identity: str :param gssapi_server_identity: Set GSSAPI server identity. :type gssapi_server_identity: str :param gssapi_client_identity: Set GSSAPI client identity. :type gssapi_client_identity: str :param gssapi_delegate_credentials: Enable/disable server credentials delegation. :type gssapi_delegate_credentials: bool :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding provided private key. """ self.gssapi_auth = gssapi_auth self.gssapi_server_identity = gssapi_server_identity self.gssapi_client_identity = gssapi_client_identity self.gssapi_delegate_credentials = gssapi_delegate_credentials 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, identity_auth=identity_auth) self._stdout_buffer = BytesIO() self._stderr_buffer = BytesIO() self._stdout_reader = None self._stderr_reader = None self._stdout_read = False self._stderr_read = False def disconnect(self): """Close socket if needed.""" if self.sock is not None and not self.sock.closed: logger.debug("Closing socket") self.sock.close() def _init(self, retries=1): logger.debug("Starting new session for %s@%s:%s", self.user, self.host, self.port) self.session = Session() self.session.options_set(options.USER, self.user) self.session.options_set(options.HOST, self.host) self.session.options_set_port(self.port) if self.gssapi_server_identity: self.session.options_set(options.GSSAPI_SERVER_IDENTITY, self.gssapi_server_identity) if self.gssapi_client_identity: self.session.options_set(options.GSSAPI_CLIENT_IDENTITY, self.gssapi_client_identity) if self.gssapi_client_identity or self.gssapi_server_identity: self.session.options_set_gssapi_delegate_credentials( self.gssapi_delegate_credentials) self.session.set_socket(self.sock) logger.debug("Session started, connecting with existing socket") try: self.session.connect() 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) ex.host = self.host ex.port = self.port raise ex 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" ex = AuthenticationException(msg, self.host, self.port, ex) ex.host = self.host ex.port = self.port raise ex logger.debug("Authentication completed successfully - " "setting session to non-blocking mode") self.session.set_blocking(0) def auth(self): if self.pkey is not None: logger.debug("Proceeding with private key file authentication") return self._pkey_auth(self.pkey, self.password) if self.allow_agent: try: self.session.userauth_agent(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 if self.gssapi_auth or (self.gssapi_server_identity or self.gssapi_client_identity): try: self.session.userauth_gssapi() except Exception as ex: logger.error( "GSSAPI authentication with server id %s and client id %s failed - %s", self.gssapi_server_identity, self.gssapi_client_identity, ex) if self.identity_auth: 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): if not self.password: raise AuthenticationException("All authentication methods failed") try: self.session.userauth_password(self.password) except Exception as ex: raise AuthenticationException( "Password authentication failed - %s", ex) def _pkey_auth(self, pkey, password=None): password = b'' if not password else password pkey = import_privkey_file(pkey, passphrase=password) self.session.userauth_publickey(pkey) def open_session(self): """Open new channel from session.""" logger.debug("Opening new channel on %s", self.host) try: channel = self.session.channel_new() while channel == SSH_AGAIN: wait_select(self.session, timeout=self.timeout) channel = self.session.channel_new() logger.debug("Channel %s created, opening session", channel) channel.set_blocking(0) while channel.open_session() == SSH_AGAIN: logger.debug( "Channel open session blocked, waiting on socket..") wait_select(self.session, timeout=self.timeout) # Select on open session can dead lock without # yielding event loop sleep(.1) except Exception as ex: raise SessionError(ex) return channel def execute(self, cmd, use_pty=False, channel=None): """Execute command on remote host. :param cmd: The command string to execute. :type cmd: str :param use_pty: Whether or not to request a PTY on the channel executing command. :type use_pty: bool :param channel: Channel to use. New channel is created if not provided. :type channel: :py:class:`ssh.channel.Channel`""" channel = self.open_session() if not channel else channel if use_pty: eagain(self.session, channel.request_pty, timeout=self.timeout) eagain(self.session, channel.request_exec, cmd, timeout=self.timeout) self._stderr_read = False self._stdout_read = False self._stdout_buffer = BytesIO() self._stderr_buffer = BytesIO() self._stdout_reader = spawn(self._read_output_to_buffer, channel) self._stderr_reader = spawn(self._read_output_to_buffer, channel, is_stderr=True) self._stdout_reader.start() self._stderr_reader.start() 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 """ _buffer_name = 'stderr' _buffer = self._stderr_buffer _flag = self._stderr_read _reader = self._stderr_reader return self._read_output(_buffer, _buffer_name, _flag, _reader, channel, timeout=timeout, is_stderr=True) def read_output(self, channel, timeout=None, is_stderr=False): """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 """ _buffer_name = 'stdout' _buffer = self._stdout_buffer _flag = self._stdout_read _reader = self._stdout_reader return self._read_output(_buffer, _buffer_name, _flag, _reader, channel, timeout=timeout) def _read_output(self, _buffer, _buffer_name, _flag, _reader, channel, timeout=None, is_stderr=False): if _flag is True: logger.debug("Output for %s has already been read", _buffer_name) raise StopIteration logger.debug("Waiting for %s reader", _buffer_name) timeout = timeout if timeout else self.timeout try: _reader.get(timeout=timeout) except GeventTimeout as ex: raise Timeout(ex) if _buffer.getvalue() == '': logger.debug("Reader finished and output empty for %s", _buffer_name) raise StopIteration logger.debug("Reading from %s buffer", _buffer_name) for line in _buffer.getvalue().splitlines(): yield line if is_stderr: self._stderr_read = True else: self._stdout_read = True def _read_output_to_buffer(self, channel, is_stderr=False): _buffer_name = 'stderr' if is_stderr else 'stdout' _buffer = self._stderr_buffer if is_stderr else self._stdout_buffer logger.debug("Starting output generator on channel %s for %s", channel, _buffer_name) while True: wait_select(self.session, timeout=self.timeout) try: size, data = channel.read_nonblocking(is_stderr=is_stderr) except EOF: logger.debug( "Channel is at EOF trying to read %s - " "reader exiting", _buffer_name) sleep(.1) return if size > 0: logger.debug("Writing %s bytes to %s buffer", size, _buffer_name) _buffer.write(data) else: # Yield event loop to other greenlets if we have no data to # send back, meaning the generator does not yield and can there # for block other generators/greenlets from running. logger.debug("No data for %s, waiting", _buffer_name) sleep(.1) 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:`ssh.channel.Channel` """ if channel is None: return timeout = timeout if timeout else self.timeout logger.debug("Sending EOF on channel %s", channel) eagain(self.session, channel.send_eof, timeout=timeout) try: self._stdout_reader.get(timeout=timeout) self._stderr_reader.get(timeout=timeout) except GeventTimeout as ex: logger.debug("Timed out waiting for readers..") raise Timeout(ex) else: logger.debug("Readers finished, closing channel") # Close channel self.close_channel(channel) def finished(self, channel): """Checks if remote command has finished - has server sent client EOF. :rtype: bool """ if channel is None: return return channel.is_eof() def get_exit_status(self, channel): """Get exit status from channel if ready else return `None`. :rtype: int or `None` """ if not channel.is_eof(): return return channel.get_exit_status() def close_channel(self, channel): """Close channel. :param channel: The channel to close. :type channel: :py:class:`ssh.channel.Channel` """ logger.debug("Closing channel") eagain(self.session, channel.close, timeout=self.timeout)