What is Expect / AutoExpect
In this tutorial we are going to use Expect tool to automate backup of Cisco switches into an FTP server using either Telnet or SSH.
Expect is a natural and intuitive automation scripting language that operates in much the same way humans do when interacting with a system. You type in commands and expect a certain response to your command. When you receive the expected response, you enter another command and so on. Expect works in the same way, except you have to provide the script with commands and expected responses to those commands. Basically, you have to script out the entire two-way “conversation.”
You can think of an Expect script as a dialog script written for two actors: a sender and a receiver. One of the more popular activities to automate is an SSH session between two hosts, in which one host is the sender (local host) and the other is the receiver (remote host). Being able to emulate every keystroke and create a true interactive session between two systems via a script is an exciting proposition.
For straightforward scripts, autoexpect saves substantial time over writing scripts by hand. Even if you are an Expect expert, you will find it convenient to use autoexpect to automate the more mindless parts of interactions. It is much easier to cut/paste hunks of autoexpect scripts together than to write them from scratch. And if you are a beginner, you may be able to get away with learning nothing more about Expect than how to call autoexpect.
However, it is important to understand that autoexpect does not guarantee a working script because it necessarily has to guess about certain things – and occasionally it guesses wrong. However, it is usually very easy to identify and fix these problems. For instance, if you use commands whose output differs from run to run, the generated scripts are not going to be correct. For example, the “date” command always produces different output. So, using the “date command” while running “autoexpect” produces a script that requires editing in order for it to work.
Expect Setup
Most Linux distributions include Expect as part of the available and installable software packages. In other words, you won’t have to download and install from source code. Use your system’s package manager to download and install Expect and any required dependencies or associated packages. For example:
$ sudo yum install expect
Once you have Expect installed, you can begin writing scripts.
Configuring TFTP Server
Here we are going to go over configuring a TFTP server on CentOS 6. This fits the typical example of needing a TFTP server to backup configuration files for networking devices to.
Login as root, and issue the following command:
$yum install tftp-server
Add an account to access the tftp server
$adduser tftp
Set ownership appropriately for the tftp directory
$chown tftp:tftp /var/lib/tftpboot/
Set the service to run on startup, as well as start the service:
$chkconfig xinetd on
$service xinetd start
Now edit the tftp configuration file:
$vi /etc/xinetd.d/tftp
Change disable to « no » :
disable = no
Your final configuration file should look as follows; make sure to edit the server_args line as well:
service tftp
{
socket_type = dgram
protocol = udp
wait = yes
user = root
server = /usr/sbin/in.tftpd
server_args = -c –p –u tftp –U 117 –s /var/lib/tftpboot
disable = no
per_source = 11
cps = 100 2
flags = IPv4
}
Cisco Configuration Backup Script
Requirements
- Ubuntu or another Linux Flavor with Autoexpect installed
- Any Cisco Router or Switch (ssh version 2 enable)
- TFTP Server
- Package “sshpass” installed
Cisco Backup Script
Let’s start with generating a script that logs on to a Cisco device, retrieves its startup configuration file, and save it on the TFTP server:
Issue the command:
autoexpect -f cisco_backup_conf
You will be able to see the message:
"autoexpect started, file is <cisco_backup_conf>
- Log into the cisco device :
sshpass -p $password ssh $username@$ip
- Enter the enable password
- Type the following command to back up the configuration to the tftp server:
wr mem
copy startup-config tftp:
- Hit Enter and the type the tftp server IP: 192.168.100.6
- Hit Enter again to confirm the file name.
For now, you will be able to see the log for the configuration file transferred
Type: Exit to close the SSH session and then to stop the Autoexpect script hit CTRL + d
You will be able to see the message
"autoexpect done. file is <your filename>"
The scripts introduced in the coming sections include a part the renames the backed up configuration files and move them to a specific directory.
In order to run the script, go to the directory where the script is saved and then simply type:
./<your script name> <arg[0]> <arg[1]> <arg[2]> <arg[3]>
Note: In order to use the command “sshpass” you should have SSHPASS package already installed
$yum install sshpass
Testing Router’s Reachability
We will assume that you have a number of routers whose configuration files are to be backed up regularly. For this purpose, we’ll consider a file which contains in each of its lines, an ip address of a router, followed by its credentials. The format used is as follows:
ip username password enable_password
The file name used is “list.txt”. This file name is used in the below script which fetches the file and pings every listed ip address to see if it’s reachable. If yes; the router’s configuration will be backed up. Otherwise, we will be notified that the router is not reachable and that the backup process has failed.
input=list.txt
cmd=./cisco_ssh_en2 #The script to be called to perform the backup
cd /root/HB #The location of the backup script
while read line
do
ip=$(echo $line | cut -d' ' -f1)
user=$(echo $line | cut -d' ' -f2)
psw=$(echo $line | cut -d' ' -f3)
enpsd=$(echo $line | cut -d' ' -f4)
ping -w 2 $ip > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
$cmd $ip $user $psw $enpsd > /dev/null 2>&1
echo "The configuration of the router whose ip is $ip has been backed up successfully."
else echo "The router whose ip is $ip is not reachable. Backup process failed."
fi
sleep 1
done < "$input"
Note that we could have used “cmd=./cisco_telnet_en2” in the 2nd line if we want to reach the router via telnet. Both scripts to reach the routers (via SSH or Telnet) are presented in the following sections.
Optimized Script using SSH
The below script uses SSH to log into the Cisco device. There are 4 defined arguments that should be provided when running the script: the ip of the device, username, password, and the enable password.
Note that the script assumes that the tftp server directory is /var/lib/tftpboot. It also assumes that there are two pre-created directories: /var/lib/tftpboot/tmp and /var/lib/tftpboot/backups.
Once the script logs into the Cisco device and save its startup configuration file in /var/lib/tftpboot, it moves the configuration file into /var/lib/tftpboot/tmp where it’s renamed by a “hostname-yyyy-mm-dd” stamp. After that, it is moved again to var/lib/tftpboot/backups. The ip of the tftp server used in this script is 192.168.100.6. Although it can be modified according to your need, you might find it more interesting to pass it through an argument.
set force_conservative 0 ;# set to 1 to force conservative mode even if
;# script wasn't run conservatively originally
if {$force_conservative} {
set send_slow {1 .1}
proc send {ignore arg} {
sleep .1
exp_send -s -- $arg
}
}
set timeout -1
set ip [lindex $argv 0]
set username [lindex $argv 1]
set password [lindex $argv 2]
set en_password [lindex $argv 3]
spawn $env(SHELL)
match_max 100000
send -- "sshpass -p $password ssh $username@$ip\r"
expect -exact "\r"
send -- "\r"
expect -exact "\r"
send -- "en\r"
expect -exact "en\r
Password: "
send -- "$en_password\r"
expect -exact "\r"
send -- "copy startup-config tftp:\r"
expect -exact ":\r
Address or name of remote host \[\]? "
send -- "192.168.100.6\r"
expect -exact "\r"
send -- "\r"
expect -exact "\r"
send -- "exit\r"
expect -exact "exit\r
Connection to $ip closed.\r\r"
send -- "cd /var/lib/tftpboot"
expect -exact "cd /var/lib/tftpboot"
send -- "\r"
expect -exact "\r"
send -- "mv /var/lib/tftpboot/*-confg /var/lib/tftpboot/tmp"
expect -exact "mv /var/lib/tftpboot/*-confg /var/lib/tftpboot/tmp"
send -- "\r"
expect -exact "\r"
send -- "cd /var/lib/tftpboot/tmp"
expect -exact "cd /var/lib/tftpboot/tmp"
send -- "\r"
expect -exact "\r"
send -- "for f in *; do mv -- \"\$f\" \"\$f-\$(stat -c %Y \"\$f\" | date +%F-)\"; done\r"
expect -exact "\r"
send -- "mv -v /var/lib/tftpboot/tmp/* /var/lib/tftpboot/backups"
expect -exact "mv -v /var/lib/tftpboot/tmp/* /var/lib/tftpboot/backups"
send -- "\r"
expect -exact "\r"
send -- "exit\r"
expect eof
Optimized Script Using TELNET
This script is similar to the previous one except that it uses TELNET to log into the device. Some lines are adjusted for this reason.
set force_conservative 0 ;# set to 1 to force conservative mode even if
;# script wasn't run conservatively originally
if {$force_conservative} {
set send_slow {1 .1}
proc send {ignore arg} {
sleep .1
exp_send -s -- $arg
}
}
set timeout -1
set ip [lindex $argv 0]
set username [lindex $argv 1]
set password [lindex $argv 2]
set en_password [lindex $argv 3]
spawn $env(SHELL)
match_max 100000
send -- "telnet $ip\r"
expect -exact "telnet $ip\r
Trying $ip...\r\r
Connected to $ip.\r\r
Escape character is '^\]'.\r\r
\r
\r
User Access Verification\r
\r
Username: "
send -- "$username\r"
expect -exact "$username\r"
send -- "$password\r"
expect -exact "\r"
send -- "en\r"
expect -exact "en\r
Password: "
send -- "$en_password\r"
expect -exact "\r"
send -- "copy startup-config tftp:\r"
expect -exact "\r"
send -- "192.168.100.6\r"
expect -exact "192.168.100.6\r"
send -- "\r"
expect -exact "\r"
send -- "exit\r"
expect -exact "exit\r
Connection closed by foreign host.\r\r"
send -- "cd /var/lib/tftpboot"
expect -exact "cd /var/lib/tftpboot"
send -- "\r"
expect -exact "\r"
send -- "mv /var/lib/tftpboot/*-confg /var/lib/tftpboot/tmp"
expect -exact "mv /var/lib/tftpboot/*-confg /var/lib/tftpboot/tmp"
send -- "\r"
expect -exact "\r"
send -- "cd /var/lib/tftpboot/tmp"
expect -exact "cd /var/lib/tftpboot/tmp"
send -- "\r"
expect -exact "\r"
send -- "for f in *; do mv -- \"\$f\" \"\$f-\$(stat -c %Y \"\$f\" | date +%F-)\"; done\r"
expect -exact "\r"
send -- "mv -v /var/lib/tftpboot/tmp/* /var/lib/tftpboot/backups"
expect -exact "mv -v /var/lib/tftpboot/tmp/* /var/lib/tftpboot/backups"
send -- "\r"
expect -exact "\r"
send -- "exit\r"
expect eof
Final AutoExpect Script
#!/usr/bin/expect -f
#
# This Expect script was generated by autoexpect on Friday
# Expect and autoexpect were both written by Don Libes, NIST.
#
# Note that autoexpect does not guarantee a working script. It
# necessarily has to guess about certain things. Two reasons a script
# might fail are:
#
# 1) timing - A surprising number of programs (rn, ksh, zsh, telnet,
# etc.) and devices discard or ignore keystrokes that arrive "too
# quickly" after prompts. If you find your new script hanging up at
# one spot, try adding a short sleep just before the previous send.
# Setting "force_conservative" to 1 (see below) makes Expect do this
# automatically - pausing briefly before sending each character. This
# pacifies every program I know of. The -c flag makes the script do
# this in the first place. The -C flag allows you to define a
# character to toggle this mode off and on.
set force_conservative 0 ;# set to 1 to force conservative mode even if
;# script wasn't run conservatively originally
if {$force_conservative} {
set send_slow {1 .1}
proc send {ignore arg} {
sleep .1
exp_send -s -- $arg
}
}
#
# 2) differing output - Some programs produce different output each time
# they run. The "date" command is an obvious example. Another is
# ftp, if it produces throughput statistics at the end of a file
# transfer. If this causes a problem, delete these patterns or replace
# them with wildcards. An alternative is to use the -p flag (for
# "prompt") which makes Expect only look for the last line of output
# (i.e., the prompt). The -P flag allows you to define a character to
# toggle this mode off and on.
#
# Read the man page for more info.
#
# -Don
set timeout -1
set ip [lindex $argv 0]
set username [lindex $argv 1]
set password [lindex $argv 2]
set en_password [lindex $argv 3]
spawn $env(SHELL)
match_max 100000
send -- "telnet $ip\r"
expect -exact "telnet $ip\r"
#Trying $ip...\r\r
#Connected to $ip.\r\r
#Escape character is '^\]'.\r\r
#\r
#\r
#User Access Verification\r
#\r
#Username: "
send -- "$username\r"
expect -exact "$username\r"
send -- "$password\r"
expect -exact "\r"
send -- "en\r"
expect -exact "en\r
Password: "
send -- "$en_password\r"
expect -exact "\r"
send -- "wr mem\r"
expect -exact "wr mem\r"
send -- "conf t\r"
expect -exact "conf t\r"
send -- "ip tftp source-interface loopback0\r"
expect -exact "ip tftp source-interface loopback0\r"
send -- "exit\r"
expect -exact "exit\r"
send -- "copy startup-config tftp:\r"
expect -exact "\r"
send -- "10.1.4.10\r"
expect -exact "10.1.4.10\r"
send -- "\r"
expect -exact "\r"
send -- "conf t\r"
expect -exact "conf t\r"
send -- "no ip tftp source-interface loopback0\r"
expect -exact "no ip tftp source-interface loopback0\r"
send -- "exit\r"
expect -exact "exit\r"
send -- "exit\r"
expect -exact "exit\r"
send -- "\r"
expect -exact "\r"
#Connection closed by foreign host.\r\r"
send -- "exit\r"
expect eof
Final Backup Telnet Script
#tftpboot Directory
TFTPBOOT=/var/lib/tftpboot
#Text file to read router parameters from (IP, Password...)
input=$TFTPBOOT/backup_scripts/lists.txt
#Autoexpect script needed to telnet to router and issue commands.
cmd1=$TFTPBOOT/backup_scripts/telnet_backup
#Empty file email.txt
> $TFTPBOOT/backup_scripts/email.txt
while read line
do
#Read router information from lists.txt file
ip=$(echo $line | cut -d' ' -f1)
user=$(echo $line | cut -d' ' -f2)
psw=$(echo $line | cut -d' ' -f3)
enpsd=$(echo $line | cut -d' ' -f4)
#Confirm router is reachable.
ping -w 4 $ip > /dev/null 2>&1
#If router is not reachable, exit from script and notify by mail that backup process failed.
if [[ $? -ne 0 ]]; then
echo "Backup of router \"$ip\" failed."
printf "[$(date +"%m-%d-%Y - %H:%M:%S")] : The router whose ip is $ip is not reachable. Backup process failed.\n" >> $TFTPBOOT/backup_scripts/email.txt
else
#Run the autoexpect script to telnet to router and get new configuration
$cmd1 $ip $user $psw $enpsd > $TFTPBOOT/backup_scripts/logs.txt
#Move the new configuration backup into /tmp folder
mv -v $TFTPBOOT/*-confg $TFTPBOOT/tmp > /dev/null 2>&1
#Append timestamp to the new configuration backup
for f in $TFTPBOOT/tmp/*; do mv -- "$f" "$f"-$(stat -c %Y "$f" | date +%F-%H:%M:%S); done;
#Store the name of the new configuration backup into variable "new_backup"
new_backup=$(ls $TFTPBOOT/tmp/ | grep conf)
#Store the hostname of the router into the variable "router_name"
router_name=$(echo "$new_backup" | sed 's/-confg.*//')
#Store the name of the old configuration backup into variable "old_backup"
old_backup=$(ls -t $TFTPBOOT/backups | grep -m 1 $router_name)
#Move the new configuration backup into the folder backups
mv -v $TFTPBOOT/tmp/* $TFTPBOOT/backups > /dev/null 2>&1
echo "Backup of router \"$ip\" completed successfully."
printf "[$(date +"%m-%d-%Y - %H:%M:%S")] : The configuration file of the router \"$router_name\" whose IP is \"$ip\" has been backed up successfully. Here are the differences between the last two versions:\n" >> $TFTPBOOT/backup_scripts/email.txt
#Output changes between the new and old backups to email.txt
diff $TFTPBOOT/backups/$new_backup $TFTPBOOT/backups/$old_backup >> $TFTPBOOT/backup_scripts/email.txt
printf "\n \n" >> $TFTPBOOT/backup_scripts/email.txt
fi
wait
done < "$input"
mail -s "Backing up Routers' Config Files" info@kloudvm.com < $TFTPBOOT/backup_scripts/email.txt
Adding Scripts to Cron Job
One of the most standard ways to run tasks in the background on Linux machines is with cron jobs. They’re useful for scheduling tasks and automating different jobs. “Cron” itself is a daemon (or program) that runs in the background. The schedule for the different jobs that are run is in a configuration file called “crontab.”
Almost all distros have a form of cron installed by default. However, if you’re using a system that doesn’t have it installed, you can install it with the following commands:
For Cent OS/Red Hat Linux:
sudo yum update
sudo yum install vixie-cron crontabs
sudo /sbin/chkconfig crond on
sudo /sbin/service crond start
Here is an example task we want to have run:
5 * * * * curl http://www.google.com
The syntax for the different jobs we’re going to place in the crontab might look intimidating. It’s actually a very succinct and easy-to-parse if you know how to read it. Every command is broken down into:
- Schedule
- Command
The command can be virtually any command you would normally run on the command line. The schedule component of the syntax is broken down into 5 different options for scheduling in the following order:
- minute
- hour
- day of the month
- month
- day of the week
Here is a list of examples for some common schedules you might encounter while configuring cron.
To run a command every minute:
* * * * *
To run a command every 12th minute on the hour:
12 * * * *
You can also use different options for each placeholder. To run a command every 15 minutes:
0,15,30,45 * * * *
Every day at 4:00am, you’d use:
0 4 * * *
Every Tuesday at 4:00am, you’d use:
0 4 * * 2
You can use division in your schedule. Instead of listing out 0,15,30,45, you could also use the following:
*/4 2-6 * * *
Notice the “2-6” range. This syntax will run the command between the hours of 2:00am and 6:00am.
Once you’ve settled on a schedule and you know the job you want to run, you’ll have to have a place to put it so your daemon will be able to read it. There are a few different places, but the most common is the user’s crontab. If you’ll recall, this is a file that holds the schedule of jobs cron will run. The files for each user are located at /var/spool/cron/crontab
, but they are not supposed to be edited directly. Instead, it’s best to use the crontab
command.
You can edit your crontab with the following command:
crontab -e
This will bring up a text editor where you can input your schedule with each job on a new line.
If you’d like to view your crontab, but not edit it, you can use the following command:
crontab -l
You can erase your crontab with the following command:
crontab -r
If you’re a privileged user, you can edit another user’s by specifying crontab -u <user> -e
For every cron job that gets executed, the user’s email address that’s associated with that user will get emailed the output unless it is directed into a log file or into /dev/null. The email address can be manually specified if you provide a “MAILTO” setting at the top of the crontab. You can also specify the shell you’d like run, the path where to search for the cron binary and the home directory with the following example:
First, let’s edit the crontab:
crontab -e
Then, we’ll edit it like so:
SHELL=/bin/bash
HOME=/
MAILTO=”info@kloudvm.com”
#This is a comment
0 12 * * 7 /backup_ssh.sh
To append to a log file, it’s as simple as:
0 12 * * 7 /backup_ssh.sh >> file.log
Note: “>>
” appends to a file. “>
” creates a new file.
If you want to pipe into an empty location, use /dev/null
. Here is the backup script that gets executed and runs in the background.
0 12 * * 7 /backup_ssh.sh > /dev/null 2>&1
Final Thoughts
Expect is a program that “talks” to other interactive programs according to a script. Following the script, Expect knows what can be expected from a program and what the correct response should be. An interpreted language provides branching and high-level control structures to direct the dialogue. In addition, the user can take control and interact directly when desired, afterward returning control to the script.
Now that you’ve learned how to use Expect to automate scripts, you might want to use it with our bash script to monitor CPU, memory and Disk usage on Linux
Leave a Reply