The 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.
The first script obtains the user input 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""
;;
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 command 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 section, "An expect
Script to Automate telnet
.") If the AUTO
variable is left undefined, the 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 command(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 specified 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
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.
The xyp_connect
script, an expect
script, performs the communication with the interactive 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.
The next line of the expect
script initiates an interactive telnet
session with the terminal 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 equipment. 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 command completes within a reasonable amount of time.
set timeout 10
expect {
timeout { send_user "Telnet timed out waiting for $TERMSERV
" ; exit }
"onnection refused" { send_user "Connection was refused to $TERMSERV
" ; exit }
"nknown host" { send_user "System $TERMSERV is unknown
" ; exit}
"Escape character is '^]'."
}
send "
"
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, performing 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 represented 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
command 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
session with the terminal server. The previous expect
command then has no effect, and the script falls through to the next statement, send "
"
. But the terminal server does not yet know this. Once we are attached to the terminal server, there is no further reply from it 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 with expect
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 example, 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
" }
expect "username>" { send "$USER
" }
expect ">" { send "set priv
" }
expect "Password>" { send "system
" }
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 variable 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
"
send "
"
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
.
set argc [llength $argv]
for {set i 3} {$i<$argc} {incr i} {
send "[lindex $argv $i]
"
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 command 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
"
send_user "
"
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 special 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, without 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
connection 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,
, to ensure that when the script finishes cleanly, the user will be back at her usual shell prompt.
18.189.185.251