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.
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 <firstname.lastname@example.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 == user: password = auth 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 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 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.