107 ■ ■ ■ CHAPTER 17 AutomatingUserInputwithexpect T he expect utility’s name suggests precisely what it does: “expect” some output from an interactive program, and send the program some input in response. expect has much more functionality than I cover in this chapter, but this chapter provides a good example of how it can be used. To find more complete information, you can consult the expect manual page. You may find that when you try to automate a task, the utilities or tools you are using don’t lend themselves well to scripting. In the past, use of the format or fdisk command (along with many others) was difficult to automate. Today we have versions of these utilities, such as sfdisk, that are much easier to use within a script. A more modern use of expect might include logging into specialized hardware to gather information or to customize settings, as is required when administering network routers, switches, and firewalls. This chapter presents a pair of scripts for automating the control of a serial terminal server. This is a type of network-accessible hardware that looks very much like a network hub or a switch with multiple RJ45 ports. Each physical port can be connected to serial devices, such as serial consoles. Once consoles are attached to the terminal server, you can telnet to a specific network port on the terminal server and establish a connection with the attached console. The first example in this chapter is a shell script that processes user-provided command-line switches that specify what commands to send to the terminal server. The second script, which is called by the first, is an expect script that performs all the manual labor. expect is an extension of the Tcl scripting language. expect was designed to communicate with an interactive program, and it works well with ssh, telnet, ftp, and other interactive utilities. 108 CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT A Shell Script to Customize Parameters for an expect Script The first script obtains the userinput necessary to connect to the desired terminal server(s) and perform the intended tasks. It displays usage instructions and allows the user to specify a specific terminal server or to provide a file containing node names if there are multiple terminal servers with which the user wants to communicate in the same way and at the same time. First we need to define a few variables: #!/bin/sh NODE="" CMDS="" NODEFILE="" AUTO="" USAGE="There is a problem with the command, type $0 -h for syntax" The variables are initialized to null strings, except for the USAGE variable, which contains a message that is displayed whenever the script finds a problem with the command-line call the user provided. The script gets the information it needs from the user on the command line, so we check that switches have been passed. if [ $# -eq 0 ] then echo $USAGE exit 1 fi If no switches are passed to the script, the script displays the usage statement and quits with a nonzero return code (here, 1). The next section is where the command-line switches are handled. while getopts idhlc:f:n: opt do case $opt in i) CMDS="$CMDS \"sho ip\"" ;; The code uses the getopts construct, which is explained in greater detail in Chapter 5. The -i switch indicates that the terminal server’s IP settings should be displayed. It causes the command sho ip to be appended to the CMDS variable, which holds the commands that will be sent to the terminal server. Next we account for customized commands. c) CUSTOM_CMD=$OPTARG CMDS="$CMDS \"$CUSTOM_CMD\"" ;; CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT 109 The -c switch is for user-provided terminal-server commands that aren’t hard-coded in the script. The user can provide as many such commands as desired when invoking the shell script, as long as a -c option precedes each command and the command itself is double-quoted; most commands interpreted by the terminal server contain multiple words that are space-delimited and so need to be tied together with quotes when the shell script is called. The OPTARG variable used in handling the -c switch is part of the getopts construct. Note that this switch is followed by a colon in the getopts specification. When a colon follows a switch in the getopts command, getopts will expect some type of argument to follow that switch whenever it is used. OPTARG is the variable that receives the additional argument to the switch. For example, if you had a script that takes a command-line parameter to specify an optional input file, the invocation might look something like this: sample_script -f input_file. The corresponding getopts line would look like this: while getopts f:<other switches> opt, and OPTARG would be set to the string "input_file". The -h switch causes the script to display its usage information. h) cat << EOT Usage: $0 [-idhl] [-c "custom command"] [-f node_file] [-n node] where: -i Sends the "sho ip" command to the Xyplex terminal server -d Logs in and drops you to the command prompt -h Displays this information -l Logs out ports 1-15 -c Takes a custom command and sends it to the terminal server. Double quotes are required. You can have as many of these as you like. -f Defines a file with a list of terminal servers to apply the commands to. -n Defines a specific node to apply the command to. EOT exit 0 ;; Note that the cat command is used here to format the output, instead of multiple echo commands. Chapter 28 contains more discussion of free-format output using cat. The -d switch in the following code indicates that the terminal-server session is not automated, and that the user simply wants to be left at a prompt after logging in: d) AUTO="no" ;; The presence of this switch causes the AUTO variable to be set to no. The expect script examines this variable, and if it is set to no, the expect script leaves the user at the com- mand prompt of the terminal server’s shell after logging in, and performs any commands specified via the other options before logging out automatically. (See the following sec- tion, “An expect Script to Automate telnet.”) If the AUTO variable is left undefined, the 110 CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT script will perform any specified tasks in an automated fashion without any user interaction. The -l switch adds a command to tell the terminal server to log out all of its serial ports. l) CMDS="$CMDS \"logout por 1-15\"" ;; On occasion, a terminal server will have a hung and unresponsive serial port. A command to log it out resets the port and it becomes usable again. The preceding CMDS variable assignment is an example of a command that performs an action on managed hardware. This command is specific to the hardware involved. The -f switch specifies a file containing a node list (that is, a list of terminal servers). f) NODEFILE=$OPTARG ;; The script loops through the list of terminal servers and performs the specified com- mand(s) against each one. The -n switch indicates that a specific terminal-server node is the target, rather than those in a list of nodes, as specified using the previous switch. n) NODE=$OPTARG ;; The following are two alternatives for robustness: ?) echo $USAGE exit 1 ;; *) echo $USAGE exit 1 ;; esac done If anything besides the anticipated options were provided in the invocation of the script, the script should echo the contents of the USAGE variable to the screen, and exit. Finally, after processing the switches and building the command list, the script calls the expect script to contact the terminal server. If a NODEFILE was specified using the -f switch, it validates the file and then iterates through it, calling the expect script once for each terminal server with the parameters the user supplied. If a NODEFILE was not speci- fied by the user via the -f switch, the script validates that an individual terminal server was specified with the -n switch and that the NODE variable is not null. If the NODE variable is null, the expect script is called with the appropriate parameters; otherwise it displays the usage string. if [ "$NODEFILE" != "" ] then CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT 111 if [ -s $NODEFILE ] then for node in `cat $NODEFILE | grep -v '^#'` do eval ./xyp_connect $NODE $AUTO $LOGNAME $CMDS done else echo There is a problem with $NODEFILE fi else if [ "$NODE" != "" ] then eval ./xyp_connect $NODE $AUTO $LOGNAME $CMDS else echo $USAGE fi fi The eval command is used here to evaluate the variables on that line of code once before the code is executed. This is because the CMDS variable may contain terminal- server commands that are, as a result of the processing of the switches, surrounded by backslash-escaped double quotes; these escaped characters must be replaced with unmodified quotes or else the multiple commands will be read incorrectly as one long command. This is also where the call to the xyp_connect expect script that performs the interactive functions takes place. An expect Script to Automate telnet The xyp_connect script, an expect script, performs the communication with the interac- tive program used to connect to the terminal server, in this case telnet. The script starts out by initializing some variables to hold the parameters that the shell script passed to it. These parameters are accessed by their positions in the argument vector, argv[], of the expect script’s process. The -f switch in the first line of the following code is used so the script will accept additional command-line options. #!/usr/bin/expect -f set TERMSERV [lindex $argv 0] set AUTO [lindex $argv 1] set USER [lindex $argv 2] The first parameter is the terminal server to which the expect script will attach. The second parameter defines if this will be an automated session in which the expect script performs the work, or an interactive one in which the script simply logs you in and leaves you at the terminal-server shell prompt. The third parameter is the user who is to be logged in. 112 CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT The next line of the expect script initiates an interactive telnet session with the termi- nal server. catch {spawn -noecho telnet $TERMSERV 2000} The spawn command starts by trying to establish a telnet connection at the specified port (2000). Port 2000 is being used because of the way this vendor has designed its equip- ment. Other manufacturers will likely be configured differently. The noecho switch tells expect to avoid echoing on the user’s console the command that is being spawned. Finally, a catch command surrounds the whole spawn command. It catches the output that is generated by the spawned telnet so that the script can use it later when determining how the telnet command responded. Once the telnet connection has begun, a timeout should be set to check that the com- mand completes within a reasonable amount of time. set timeout 10 expect { timeout { send_user "Telnet timed out waiting for $TERMSERV\n" ; exit } "onnection refused" { send_user "Connection was refused to $TERMSERV\n" ; exit } "nknown host" { send_user "System $TERMSERV is unknown\n" ; exit} "Escape character is '^]'." } send "\r" Here we set the timeout period to 10 seconds; following the setting of the timeout is the first true expect command. A single expect command can handle multiple events, per- forming the appropriate task based on which one is detected. In this case, a number of responses may be received from an attempted telnet connection. A timeout, connection refused, or actual connection are three possibilities. For each type, the code needs to determine the appropriate response to make. This first expect command handles the three error events that may arise from the telnet attempt. The first event is the timeout. Once 10 seconds have passed with no response, the script displays an error message and exits. The next two events are repre- sented by patterns matching the error messages that may be caught from the telnet invocation in case of failure: “Connection refused” and “Unknown host,” respectively. Because the error message may or may not be initial-capped, depending on the telnet server, and we want to handle both possibilities, the first character is not included in the pattern used to match against the caught output. In each event, we use a send_user com- mand to echo the appropriate error output to the user and exit the expect script. If none of these error conditions occur, then we have successfully begun a telnet ses- sion with the terminal server. The previous expect command then has no effect, and the script falls through to the next statement, send "\r". But the terminal server does not yet know this. Once we are attached to the terminal server, there is no further reply from it CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT 113 until it receives a single carriage return from us. This send command delivers that carriage return, at which point both parties know that we have arrived via telnet at the point just prior to login. Now comes the interaction for the actual login to the terminal server. If expect succeeds in establishing a telnet connection, the caught output consists of the success string, which for our terminal server is the pound or hash sign, #. When the script detects this response, it proceeds withexpect commands, implementing the login dialogue. Our particular terminal-server hardware will by default take anything for the initial username and not require a password. The expect script here assumes these factory defaults. You may need to change this dialogue to match your environment. (For exam- ple, it would be fairly simple to add another switch to the shell script allowing the password to be given from the command line, so that the login/password would not be hard-coded in an unencrypted text file.) expect "#" { send "access\r" } expect "username>" { send "$USER\r" } expect ">" { send "set priv\r" } expect "Password>" { send "system\r" } expect ">>" In our case, the basic login is complete when the > character is received in reply for the username; however, to perform administrative tasks on the terminal server, we must upgrade privileges via a set priv command. As shown in the preceding code, the default password for this level of access is system, and once you’re logged in at the privileged level, you receive a >> prompt. Next we check whether the AUTO variable is set to no. Recall that the value of this vari- able was passed to the expect script as a parameter, and allows the script to determine whether the user wants to perform a command or a set of commands on the terminal server, or simply wants to be left logged in to perform her own administration. if { "$AUTO" == "no" } { send_user "Script ended: You have been dropped to the command line\n" send "\r" interact exit } If AUTO is set to no, a message is sent to the user that the script has completed its run and control of the terminal server session will now be handed over to the user. The next-to-last interact command in this part of the script carries out this handover before exiting. If the script reaches this point, then AUTO has not been set to no, and there may be terminal-server commands that were intended for the expect script that were included in the shell script’s command line as described earlier. Next we determine the number of these parameters and assign that value to argc. 114 CHAPTER 17 ■ AUTOMATINGUSERINPUTWITHEXPECT set argc [llength $argv] for {set i 3} {$i<$argc} {incr i} { send "[lindex $argv $i]\r" expect ">>" } This code lets us know when to stop looking in the expect script’s argument vector argv for terminal-server commands. Each time through the for loop, a terminal-server com- mand is sent; after the command finishes running, a >> prompt should be received before the next command is issued. (The loop starts at 3 because the first few parameters, at index positions 0, 1, and 2, are those that were used earlier by the expect script: AUTO, TERMSERV, and USER.) When the list of commands has been processed and all commands have been sent, we perform the telnet logout dialog. send "^]" expect "telnet>" send "quit\r" send_user "\n" The first send command in this code segment contains a single special character—not a caret followed by a right square bracket, but rather a Ctrl+] character. To enter the spe- cial character in vi’s insert mode, you would press Ctrl+v and then Ctrl+]. The Ctrl+v command tells vi to insert the following key sequence as a Ctrl character sequence, with- out attempting to interpret it. (Another example of this type of vi editing maneuver might be to replace Ctrl+] with Enter, which would specify a carriage return sequence and be displayed as ^M.) Sending the ^] special character causes the script to break out of the active telnet con- nection and drops you to the telnet’s interactive prompt. At this point the script sends a quit command to the terminal server and the telnet session closes. After the telnet port connection closes with the quit command, expect sends the user a final carriage return, \n, to ensure that when the script finishes cleanly, the user will be back at her usual shell prompt. . 107 ■ ■ ■ CHAPTER 17 Automating User Input with expect T he expect utility’s name suggests precisely what it does: expect some output from an. 17 ■ AUTOMATING USER INPUT WITH EXPECT 109 The -c switch is for user- provided terminal-server commands that aren’t hard-coded in the script. The user can