pythonshellsshparamikointeractive

Implement an interactive shell over ssh in Python using Paramiko?


I want to write a program (in Python 3.x on Windows 7) that executes multiple commands on a remote shell via ssh. After looking at paramikos' exec_command() function, I realized it's not suitable for my use case (because the channel gets closed after the command is executed), as the commands depend on environment variables (set by prior commands) and can't be concatenated into one exec_command() call as they are to be executed at different times in the program.

Thus, I want to execute commands in the same channel. The next option I looked into was implementing an interactive shell using paramikos' invoke_shell() function:

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(host, username=user, password=psw, port=22)

channel = ssh.invoke_shell()

out = channel.recv(9999)

channel.send('cd mivne_final\n')
channel.send('ls\n')

while not channel.recv_ready():
    time.sleep(3)

out = channel.recv(9999)
print(out.decode("ascii"))

channel.send('cd ..\n')
channel.send('cd or_fail\n')
channel.send('ls\n')

while not channel.recv_ready():
    time.sleep(3)

out = channel.recv(9999)
print(out.decode("ascii"))

channel.send('cd ..\n')
channel.send('cd simulator\n')
channel.send('ls\n')

while not channel.recv_ready():
    time.sleep(3)

out = channel.recv(9999)
print(out.decode("ascii"))

ssh.close() 

There are some problems with this code:

  1. The first print doesn't always print the ls output (sometimes it is only printed on the second print).
  2. The first cd and ls commands are always present in the output (I get them via the recv command, as part of the output), while all the following cd and ls commands are printed sometimes, and sometimes they aren't.
  3. The second and third cd and ls commands (when printed) always appear before the first ls output.

I'm confused with this "non-determinism" and would very much appreciate your help.


Solution

  • import paramiko
    import re
    
    
    class ShellHandler:
    
        def __init__(self, host, user, psw):
            self.ssh = paramiko.SSHClient()
            self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            self.ssh.connect(host, username=user, password=psw, port=22)
    
            channel = self.ssh.invoke_shell()
            self.stdin = channel.makefile('wb')
            self.stdout = channel.makefile('r')
    
        def __del__(self):
            self.ssh.close()
    
        def execute(self, cmd):
            """
    
            :param cmd: the command to be executed on the remote computer
            :examples:  execute('ls')
                        execute('finger')
                        execute('cd folder_name')
            """
            cmd = cmd.strip('\n')
            self.stdin.write(cmd + '\n')
            finish = 'end of stdOUT buffer. finished with exit status'
            echo_cmd = 'echo {} $?'.format(finish)
            self.stdin.write(echo_cmd + '\n')
            shin = self.stdin
            self.stdin.flush()
    
            shout = []
            sherr = []
            exit_status = 0
            for line in self.stdout:
                if str(line).startswith(cmd) or str(line).startswith(echo_cmd):
                    # up for now filled with shell junk from stdin
                    shout = []
                elif str(line).startswith(finish):
                    # our finish command ends with the exit status
                    exit_status = int(str(line).rsplit(maxsplit=1)[1])
                    if exit_status:
                        # stderr is combined with stdout.
                        # thus, swap sherr with shout in a case of failure.
                        sherr = shout
                        shout = []
                    break
                else:
                    # get rid of 'coloring and formatting' special characters
                    shout.append(re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]').sub('', line).
                                 replace('\b', '').replace('\r', ''))
    
            # first and last lines of shout/sherr contain a prompt
            if shout and echo_cmd in shout[-1]:
                shout.pop()
            if shout and cmd in shout[0]:
                shout.pop(0)
            if sherr and echo_cmd in sherr[-1]:
                sherr.pop()
            if sherr and cmd in sherr[0]:
                sherr.pop(0)
    
            return shin, shout, sherr