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()
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
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)
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!")
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()
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, ))
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
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))
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, ))
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())
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
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)
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)
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
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
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()
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()