24

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.

6
  • 1
    you'll get more help if replace the tag with the fewest followers with a python tag, assuming that this is really python code. good luck. Commented Mar 6, 2016 at 1:38
  • Do you have to use paramiko? I found it much easier to work with fabric. You just set up env variables like user, password and host_string and then you can do various stuff like use: get to download files from remote host, put to send files and run to issue commands. You can chain commands like this for example: run('cd .. && cd simulator && ls'). Commented Mar 6, 2016 at 10:51
  • @kchomski unfortunately fabric is not compatible with python 3.x so it's not an option. Anyway, from what i saw, Fabric is just a wrapper to paramiko and doesn't let me run 'non-chained' commands in the same channel. There is a lot of logic that i ultimately want to run between the shell commands. Commented Mar 6, 2016 at 12:12
  • @misha: sorry, I overlooked that you're working with Python 3.x Commented Mar 6, 2016 at 12:40
  • 1
    check out netmiko It's specialized for network devices, but you can also use it with Linux. It works on Python 3 and is built on Paramiko, but handles a lot of the buffering for you Commented May 3, 2017 at 19:59

3 Answers 3

33
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
Sign up to request clarification or add additional context in comments.

9 Comments

How can I send multiple commands to the execute()? I've tried to do a for loop : for command in commands: object.execute(command) for a list of commands but it only executes 2 commands then I have to re-start the shell.
What if I my command produces both stdout and stderr, and I want them as separate files?
@YaroslavBulatov I didn't try it, but I think you could declare self.stderr = channel.makefile_stderr('r'), in a similar fashion to how stdin and stdout are declared (pay attention to the makefile_stderr method). Then, supposedly you could access stderr as the file-like object should be associated with the stderr of this channel.
you can avoid must of the stdout cleanup by: - removing the command prompt by sending cmd "export PS1="\n"" - avoiding echoing stdin by sending cmd "stty -echo"
Classy!, I added self.stdin.write("sudo su " + '\n') below cmd.strip('\n') to change user to root. Thanks
|
3

I needed this for a Cisco router, which is rather different from a Linux machine. Credit to @misha for identifying the big challenge when running multiple commands during a single session, namely detecting the end of one command output so you know when to stop reading from the router's stdout object. If you don't detect that, the read loop just hangs. Sending two commands every time and using the second command as a sentinel is kind of a clever hack, so I copied it! This uses a well-known error response from the IOS command prompt as a sentinel.

import logging
import paramiko
import socket

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def inter_handler(
        title: str,
        instructions: str,
        prompts: List[Tuple[str, bool]]) -> List[str]:
    """
    Proprietary details omitted. Here is the relevant doc from
    paramiko/transport.py, function `auth_interactive`:

        The handler is expected to be a callable that will handle calls of the
        form: ``handler(title, instructions, prompt_list)``.  The ``title`` is
        meant to be a dialog-window title, and the ``instructions`` are user
        instructions (both are strings).  ``prompt_list`` will be a list of
        prompts, each prompt being a tuple of ``(str, bool)``.  The string is
        the prompt and the boolean indicates whether the user text should be
        echoed.

        A sample call would thus be:
        ``handler('title', 'instructions', [('Password:', False)])``.

        The handler should return a list or tuple of answers to the server's
        questions.
    """
    pass


def ssh_run_cmds(
        host: str,
        port: int,
        user: str,
        commands: list) -> None:
    """
    Connect to the router, authenticate by computing a challenge
    response, and run commands. A major challenge is detecting
    the end of the command output, to know when to stop reading
    from the router session. This code uses an ugly hack of
    sending an invalid command and checking for a well-known
    error message.
    """
    # Create a socket and connect it to port 22 on the remote host
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # the argument must be a tuple
    sock.connect((host, port))
    # Wrap the socket in a paramiko Transport object
    ts = paramiko.Transport(sock)
    # Tell Paramiko that the Transport is going to be used as a client
    ts.start_client(timeout=10)
    # Authenticate the specified user via the handler
    ts.auth_interactive(user, inter_handler)
    # Open a channel
    chan = ts.open_channel(kind='session', timeout=10)
    # Associate a pseudo tty
    chan.get_pty()
    # Request an interactive shell session
    chan.invoke_shell()
    # Create writer/reader file-like objects
    stdin = chan.makefile('wb')
    stdout = chan.makefile('r')
    # Use the output from this invalid command as a sentinel
    bogus_cmd = 'show bogus'
    for cmd in commands:
        # Send the command AND a bogus command to detect end of output
        cmds = f'{cmd}\n{bogus_cmd}\n'
        logger.debug('Send commands: %s', cmds)
        stdin.write(cmds)
        stdin.flush()

        # Read the response
        for line in stdout:
            line = line.strip()
            logger.debug('Output line: %s', line)
            # the response from the bogus command is the last line
            if line.startswith("% Invalid input detected at '^' marker."):
                break
        # for line
    # for cmd

    stdin.close()
    stdout.close()
    chan.close()
    ts.close()
    sock.close()

3 Comments

This is fairly useless without knowing what inter_handler does. Can you share any other details on how to create this?
@Dave I added a stub inter_handler function. With that said, I think the real answer here is to use Netmiko, because it does so much more than Paramiko. I brought my custom interactive auth handler to Netmiko and the combination works for me well.
I deleted my last comment, netmiko works fine with Palo Alto (what we were using it for). Problem was I was not using it properly. I agree with you to use Netmiko!
0

I tried the answer above, and it didn't work because ECHO command returned error in Python CLI, which I was using with SSH.

So I wrote another code that is applicable for Python CLI, assuming that the output is in one line.

And I also think something like f"print('{finish}')" can do same thing (terminator??) as ECHO in the answer above. But I didn't make use of it because my output always has to be in one line.

class MusicPlayer:
def __init__(self, host='', username='pi', password=''):
    self.ssh = paramiko.SSHClient()
    self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    self.ssh.connect(host, username=username, password=password)
    channel = self.ssh.invoke_shell()
    self.stdin = channel.makefile('wb')
    self.stdout = channel.makefile('r')
    self.in_history = []
    self.out_history = []
    self.init_vlc()
    self.print()
    # atexit.register(self.__del__)

def __del__(self):
    self.ssh.close()

def execute(self, cmd):
    self.in_history.append(cmd)
    self.stdin.write(cmd + '\n')

def print(self, lines=1):
    for line in self.stdout:
        lined = line.strip()
        print(lined)
        self.out_history.append(lined)
        if self.in_history[-1] in lined:
            next_one = self.stdout.__next__().strip()
            print(next_one)
            self.out_history.append(next_one)
            return next_one

def init_vlc(self):
    for command in ['python', 'import vlc', 'import time', 'media_player = vlc.MediaPlayer()']:
        self.execute(command)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.