misha misha - 7 months ago 605
Bash Question

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 don'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.

Answer

After a long research I finally found an answer and implemented a working interactive shell using paramiko:

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

channel = ssh.invoke_shell()

finish = 'end of stdOUT buffer. remote execution finished with exit status'

stdin = channel.makefile('wb')
stdout = channel.makefile('r')


def print_exec_out(cmd, out_buf, err_buf, exit_status):
    print('command executed: {}'.format(cmd))
    print('STDOUT:')
    for line in out_buf:
        print(line, end="")
    print('end of STDOUT')
    print('STDERR:')
    for line in err_buf:
        print(line, end="")
    print('end of STDERR')
    print('finished with exit status: {}'.format(exit_status))


def execute(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')
    stdin.write(cmd + '\n')
    echo_cmd = 'echo {} $?'.format(finish)
    stdin.write(echo_cmd + '\n')
    shin = stdin
    stdin.flush()

    shout = []
    sherr = []
    exit_status = 0
    for line in stdout:
        if str(line).startswith(cmd) or str(line).startswith(echo_cmd):
            # clean out buffer, up for now filled with shell junk from stdin
            shout = []
        elif str(line).startswith(finish):
            # our finish command ends with the exit status of remote executed command
            exit_status = int(str(line).rsplit(maxsplit=1)[1])
            if exit_status:
                # in interactive shell mode, stderr is combined with stdout.
                # thus, swap sherr with shout in a case of remote command execution failure.
                sherr = shout
                shout = []
            break
        else:
            # get rid of 'coloring and formatting' special characters before appending line to shout
            shout.append(re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]').sub('', line))

    # first and last lines of shout/sherr contain a prompt with the entered commands. get rid of them.
    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)

    print_exec_out(cmd=cmd, out_buf=shout, err_buf=sherr, exit_status=exit_status)
    return shin, shout, sherr