Python pexpect - TIMEOUT falls into traceback and exits

Tester315 picture Tester315 · May 5, 2012 · Viewed 25.9k times · Source

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)

Answer

sudoman picture sudoman · May 13, 2012

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)