Python pexpect - TIMEOUT falls into traceback and exits

27,037

Solution 1

A note about automating ssh logins: If you're thinking of using (p)expect for automating ssh logins, consider using ssh keys instead. As a bonus, ssh keys are more secure than passwords. They are far more secure than storing plain text passwords on your file system, which many users of expect do. If you use an ssh agent, you can type your password once to unlock your key locally, then automatically use the key for multiple logins on multiple hosts. As an added convenience, you may set or disable the time limit for password expiry.

First off, you should import traceback, so if there is an unexpected error in the program, traceback.print_exc() will work.

import getpass, os, traceback

I also created a die() function so that ssh gets closed when the program quits.

def die(child, errstr):
    print errstr
    print child.before, child.after
    child.terminate()
    exit(1)

Delete this line, since it was probably left there by mistake after adding the correct command on the next line:

        child.expect ('password: ')

The correct line (which handles timeouts) is:

        i = child.expect([pexpect.TIMEOUT, 'password: '])

then replace this:

    child.expect(pexpect.EOF)
    print child.before

with this:

    i = child.expect([pexpect.TIMEOUT, 'Permission denied', pexpect.EOF])
    if i == 0:
        die(child, 'ERROR!\nSSH timed out. Here is what SSH said:')
    elif i == 1:
        die(child, 'ERROR!\nIncorrect password Here is what SSH said:')
    elif i == 2:
        print child.before

This change will tell the program to detect a bad password, or to timeout gracefully if there is unknown input, instead of raising an exception. Note that if your command runs longer than the default timeout duration, it will be aborted before it finishes. You need to set something like timeout=60 as an argument to expect() in order to change that.

Also, returning None after a timeout isn't useful. Instead, call die() which will call exit(1):

        die(child, 'error message')

This is the final code:

#!/usr/bin/env python

"""This runs 'ls -l' on a remote host using SSH. At the prompts enter hostname,
user, and password.

$Id$
"""

import pexpect
import getpass, os, traceback

def ssh_command (user, host, password, command):

    """This runs a command on the remote host. This could also be done with the
pxssh class, but this demonstrates what that class does at a simpler level.
This returns a pexpect.spawn object. This handles the case when you try to
connect to a new host and ssh asks you if you want to accept the public key
fingerprint and continue connecting. """

    ssh_newkey = 'Are you sure you want to continue connecting'
    child = pexpect.spawn('ssh -l %s %s %s'%(user, host, command))
    i = child.expect([pexpect.TIMEOUT, ssh_newkey, 'password: '])
    if i == 0: # Timeout
        die(child, 'ERROR!\nSSH could not login. Here is what SSH said:')
    if i == 1: # SSH does not have the public key. Just accept it.
        child.sendline ('yes')
        i = child.expect([pexpect.TIMEOUT, 'password: '])
        if i == 0: # Timeout
            die(child, 'ERROR!\nSSH could not login. Here is what SSH said:')
    child.sendline(password)
    return child

def die(child, errstr):
    print errstr
    print child.before, child.after
    child.terminate()
    exit(1)

def main ():

    host = raw_input('Hostname: ')
    user = raw_input('User: ')
    password = getpass.getpass('Password: ')
    child = ssh_command(user, host, password, '/bin/ls -l')

    i = child.expect([pexpect.TIMEOUT, 'Permission denied', pexpect.EOF])
    if i == 0:
        die(child, 'ERROR!\nSSH timed out. Here is what SSH said:')
    elif i == 1:
        die(child, 'ERROR!\nIncorrect password Here is what SSH said:')
    elif i == 2:
        print child.before

if __name__ == '__main__':
    try:
        main()
    except Exception, e:
        print str(e)
        traceback.print_exc()
        os._exit(1)

Solution 2

I would recommend trying the pxssh module/class that comes with pexpect to automate ssh sessions. This would cut down on your code by handling those common situations.

Here is some example code copied from another StackOverflow https://stackoverflow.com/a/6189676/570450

import pxssh
s = pxssh.pxssh()
if not s.login ('localhost', 'myusername', 'mypassword'):
    print "SSH session failed on login."
    print str(s)
else:
    print "SSH session login successful"
    s.sendline ('ls -l')
    s.prompt()         # match the prompt
    print s.before     # print everything before the prompt.
    s.logout()
Share:
27,037
Admin
Author by

Admin

Updated on July 05, 2022

Comments

  • Admin
    Admin almost 2 years

    I'm new to python-pexpect. In Tcl/expect when I hit a timeout - I would respond with message and exit the function. I have tried to experiment with similar response using sample code posted http://pexpect.svn.sourceforge.net/viewvc/pexpect/trunk/pexpect/examples/sshls.py?revision=489&view=markup

    I based on this code above - if I give a bogus password, I would expect this to just timeout, print "ERROR!", and exit program. But when I run it - goes into a 'Traceback output (see below), can someone help me to get the program to print "ERROR" and exit program gracefully.

    test@ubuntu:~/scripts$ ./tmout.py 
    Hostname: 192.168.26.84
    User: root
    Password: 
    Timeout exceeded in read_nonblocking().
    <pexpect.spawn object at 0xb77309cc>
    version: 2.3 ($Revision: 399 $)
    command: /usr/bin/ssh
    args: ['/usr/bin/ssh', '-l', 'root', '192.168.26.84', '/bin/ls', '-l']
    searcher: searcher_re:
        0: EOF
    buffer (last 100 chars): 
    Permission denied, please try again.
    [email protected]'s password: 
    before (last 100 chars): 
    Permission denied, please try again.
    [email protected]'s password: 
    after: <class 'pexpect.TIMEOUT'>
    match: None
    match_index: None
    exitstatus: None
    flag_eof: False
    pid: 14997
    child_fd: 3
    closed: False
    timeout: 30
    delimiter: <class 'pexpect.EOF'>
    logfile: None
    logfile_read: None
    logfile_send: None
    maxread: 2000
    ignorecase: False
    searchwindowsize: None
    delaybeforesend: 0.05
    delayafterclose: 0.1
    delayafterterminate: 0.1
    Traceback (most recent call last):
      File "./tmout.py", line 54, in <module>
        traceback.print_exc()
    NameError: name 'traceback' is not defined
    test@ubuntu:~/scripts$ 
    

    Source Code:

    #!/usr/bin/env python
    
    """This runs 'ls -l' on a remote host using SSH. At the prompts enter hostname,
    user, and password.
    
    $Id$
    """
    
    import pexpect
    import getpass, os
    
    def ssh_command (user, host, password, command):
    
        """This runs a command on the remote host. This could also be done with the
    pxssh class, but this demonstrates what that class does at a simpler level.
    This returns a pexpect.spawn object. This handles the case when you try to
    connect to a new host and ssh asks you if you want to accept the public key
    fingerprint and continue connecting. """
    
        ssh_newkey = 'Are you sure you want to continue connecting'
        child = pexpect.spawn('ssh -l %s %s %s'%(user, host, command))
        i = child.expect([pexpect.TIMEOUT, ssh_newkey, 'password: '])
        if i == 0: # Timeout
            print 'ERROR!'
            print 'SSH could not login. Here is what SSH said:'
            print child.before, child.after
            return None
        if i == 1: # SSH does not have the public key. Just accept it.
            child.sendline ('yes')
            child.expect ('password: ')
            i = child.expect([pexpect.TIMEOUT, 'password: '])
            if i == 0: # Timeout
                print 'ERROR!'
                print 'SSH could not login. Here is what SSH said:'
                print child.before, child.after
                return None       
        child.sendline(password)
        return child
    
    def main ():
    
        host = raw_input('Hostname: ')
        user = raw_input('User: ')
        password = getpass.getpass('Password: ')
        child = ssh_command (user, host, password, '/bin/ls -l')
        child.expect(pexpect.EOF)
        print child.before
    
    if __name__ == '__main__':
        try:
            main()
        except Exception, e:
            print str(e)
            traceback.print_exc()
            os._exit(1)