Welcome, guest | Sign In | My Account | Store | Cart

System administrators sometimes need to automate commands which prompt for a password (or any other single prompt) before they execute. This recipe demonstrates using Pexpect and the built-in netrc module to automate these commands easily and relatively securely.

Python, 119 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# ewp.py - John Landahl <john@landahl.org> [16 May 2008]

import sys, re, os, optparse, pexpect, netrc

def password_from_netrc(machine, user):
    '''Given a machine name and user name, return the associated password in
    ~/.netrc. Returns None if not found or if .netrc does not exist.'''
    password = None
    try:
        auth = netrc.netrc().authenticators(machine)
        if auth and auth[0] == user:
            password = auth[2]
    except IOError:
        pass
    return password

def execute_with_prompt(command, prompt, response, timeout=-1):
    '''Execute a system command which expects a single prompt (e.g. for a
    password) to be answered by the user before running.

    A timeout may be specified in seconds, where a value of -1 (a Pexpect
    convention) indicates that the default timeout (30 seconds) should be
    used.

    The result is a generator which produces each line of output as it is
    received from the command. This is especially advantageous for
    long-running commands or commands with a great deal of output, since the
    caller does not have to wait for the command to finish before receiving
    output, and the output will not take up memory as it would if collected
    into a list.

    Trailing CRLFs (included with each line coming from a pty) are stripped
    from each line.

    A simple example:

        for line in execute_with_prompt('sudo cat /etc/shadow', 'Password:', 'secret'):
            print line.split(':')[0:2]
    '''
    child = pexpect.spawn(command)
    child.expect(prompt, timeout=timeout)
    child.sendline(response)

    # compile a regular expression to find trailing CRLFs. note that we can't
    # simply use .rstrip() because it would strip -all- whitespace from the
    # end of each line, which could be inappropriate in some use cases.
    trailing_crlf = re.compile('\r\n$')

    # return a generator (via a generator expression) which will produce the
    # next line of output with each iteration.
    return (trailing_crlf.sub('', line) for line in child)

def main():
    '''
    Provide a generic wrapper around the password_from_netrc() and
    execute_with_prompt() functions, with some options for setting the
    machine, user, and prompt. The command to run should be the first argument
    and will most likely need to be put in quotes.

    Suppose our username is "foo" and we have a ~/.netrc file as follows:

      default login foo password secret
      default login bar password anothersecret
      machine blah login foo password y-a-secret

    If this script is saved as ewp.py, the default should allow for a simple
    command line:

      ewp.py "ssh somewhere ls /etc"

    This will provide ssh with the password 'secret' when prompted.

    If we need to login as as 'bar', the code here can determine this from the
    use of the @ symbol:

      ewp.py "ssh bar@somewhere ls /etc"

    This will provide ssh with the password 'anothersecret' when prompted.

    Finally, logging into the machine 'blah':

      ewp.py -m blah "ssh blah ls /etc"

    This will provide ssh with the password 'y-a-secret' when prompted. We had
    to use the '-m' flag here because the machine name cannot be determined
    automatically (there must be an @ symbol for the present code to work).
    '''
    opt = optparse.OptionParser()
    opt.add_option('--machine', '-m', dest='machine')
    opt.add_option('--user', '-u',    dest='user')
    opt.add_option('--prompt', '-p',  dest='prompt', default='ssword:')
    opts, args = opt.parse_args()

    command = args[0]
    machine = opts.machine
    user = opts.user

    # if machine or user were not specified, try to determine their values
    # from the command line
    if machine is None or user is None:
        match = re.search(r'(\w[\w\-.]+)@(\w[\w\-.]+)', command)
        if match:
            if user is None: user = match.group(1)
            if machine is None: machine = match.group(2)

        # fall-through case: use login name
        if user is None:
            user = os.getlogin()

    password = password_from_netrc(machine, user)

    # run an arbitrary command passed in as the first command line argument
    # (surround the full command with quotes).
    command = args[0]
    for line in execute_with_prompt(command, opts.prompt, password):
        print line

if __name__ == '__main__':
    main()

The Pexpect library is a pure-Python implementation of most of the features of Expect, a TCL-based library for automating interactive programs. This recipe shows how it can be used in conjunction with a ~/.netrc file in order to answer a password prompt in a relatively secure way. The permissions of a .netrc file should be set so that only the owner can read it, which protects its contents from all but the superuser (which is why call it as "relatively secure"). Though not ideal, this method is far better than keeping the password in the script itself where it will be visible to all. While the script could also be secured similarly to the .netrc file, it is far more likely to get unsecured at some point, especially if it ever needs to be shared or ends up in a version control system.

The password_from_netrc() function is a thin wrapper for the builtin netrc.netrc() method, returning None if there is no matching entry or if the .netrc file itself is missing. Note that .netrc files may have a "default" entry which applies to any machine for a given username.

The execute_with_prompt() function uses spawn(), expect(), and sendline() from Pexpect to run a command, wait for the given prompt, and send the given response. It then returns a generator (created with a generator expression) which produces a new line from the child process with each iteration. As the docstring explains, this technique is very useful for long-running commands and commands with a large amount of output.

One potential problem with the existing code is that any other prompt from the command will not be seen. Because execute_with_prompt() iterates through the child process to get each line of output, and because this is implemented in Pexpect by looking for newlines, another prompt (such as one which doesn't match the one we're looking for, or one that comes after the one already responded to) will stall execution. This code could be extended to either look for additional prompts (complicated to get right), or to detect a stalled program (perhaps through a periodic timeout and a check to see if output has remained the same), after which Pexpect's interact() method would allow the user to respond to the prompt.