Conditional statament inside expect command called from a bash script - bash

I've been trying to automate some configuration backups on my cisco devices, i've already managed to do the script that accomplishes the task but I'm trying to improve it to handle errors too.
I think that's necessary to catch the errors on two steps, first just after the 'send \"$pass\r\"' to get login errors (access denied messages) and at the 'expect \": end\"' line, to be sure that the commands issued were able to pull the configuration from the device.
I've seen some ways to do it if you work on a expect script, but i want to use a bash script to be able to supply a list of devices from a .txt file.
#!/bin/bash
data=$(date +%d-%m-%Y)
dataOntem=$(date +%d-%m-%Y -d "-1 day")
hora=$(date +%d-%m-%Y-%H:%M:%S)
log=/firewall/log/bkpCisco.$data.log
user=MYUSER
pass=MYPASS
for firewall in `cat /firewall/script/firewall.cisco`
do
VAR=$(expect -c "
spawn ssh $user#$firewall
expect \"assword:\"
send \"$pass\r\"
expect \">\"
send \"ena\r\"
expect \"assword:\"
send \"$pass\r\"
expect \"#\"
send \"conf t\r\"
expect \"conf\"
send \"no pager\r\"
send \"sh run\r\"
log_file -noappend /firewall/backup/$firewall.$data.cfg.tmp
expect \": end\"
log_file
send \"pager 24\r\"
send \"exit\r\"
send \"exit\r\"
")
echo "$VAR"
done

You need alternative patterns in the expect statements where you want to catch errors. If you're looking for a specific error message you can specify that, alternatively just specify a timeout handler which will eventually trigger when the normal output fails to appear.
Eg. after send \"$pass\r\" instead of expect \">\" try:
expect \">\" {} timeout {puts stderr {Could not log in}; exit}
ie. if the expected output arrives before the timeout (default 10 sec) do nothing and continue, otherwise complain and exit from expect. You might also need an eof pattern to match the case where your ssh session ends.
Note that since you don't do any variable substitution in expect, you don't need \"\" around your strings, you can use {} or even nothing when it's one word, eg. expect conf and send {no pager}.
BTW I agree with bstpierre that this would be cleaner if you dropped bash and did the whole thing in expect, but if bash does the job that's ok.

If you don't use single quotes (expect -c '...'), then all the $variables will be substituted by bash not expect. May be easier to put the expect code in a separate file, or maybe a heredoc.

Related

Quoting issue with expect script

I have a script to automatically connect to a VPN, because I often have to connect and disconnect several times a day. (Certain things like ScreenHero and GoToMeeting fail on the VPN, and Mail servers and other things are blocked while on the VPN, but I can't connect to the Git server unless I'm on the VPN, and the app I'm working on can't reach certain back-end services unless I'm on the VPN, so development is limited when disconnected.)
The script lessens the hassle, but I have a $ in my password (which is available in the environment through the environment variable VPN_PASSWORD1), and the password value gets interpolated before it is passed to the VPN program by expect.
#!/bin/bash
{
/usr/bin/expect << END_OF_LOGIN_SESSION
set timeout 30
spawn /opt/cisco/anyconnect/bin/vpn -s connect blah.blahblah.com
expect "Username:*"
send "\r"
expect "Password:*"
send "$VPN_PASSWORD\r"
expect "accept? \[y/n\]:*"
send "y\r"
expect eof
END_OF_LOGIN_SESSION
}
How do I pass a password literally, without letting it be subject to string interpolation?
Changing my password is a cop-out. Saving it with a \ is also a cop-out (and horrible UX, since I have to enter it every time I source my .project file for this project2).
It seems like expect must have a solution for this.
Yes I know someone who steals my laptop could try to log in, but (a) they'd have to get into my account first, and (b) they'd fail without my PIN, entered by phone, anyway.
No, the password is not saved to disk. I enter it once per shell session related to this project.
You should have got the can't read "VPN_PASSWORD": no such variable at the first place unless you have defined any variable with that name.
Environmental variables can be accessed via the Tcl's special array variable env.
You can straightaway use env(VPN_PASSWORD) to access the same.
send "$env(VPN_PASSWORD)\r"
Reference : env
You have the shell variable $VPN_PASSWORD in an unquoted heredoc, so the shell will substitute the value. Suppose VPN_PASSWORD='foo$bar' => Then expect will see: send "foo$bar\r" and you'll get can't read "bar": no such variable. The solution is to use {braces} instead of double quotes, so that expect will not attempt to expand the "inner" variable.
send {$VPN_PASSWORD}; send "\r"
You need a separate send "\r" because putting \r inside the braces will remove its special meaning, and Tcl won't let you do send {$VPN_PASSWORD}"\r"
Here's a demo:
$ VPN_PASSWORD='foo$bar'
$ expect <<END
send_user "$VPN_PASSWORD\n"
END
can't read "bar": no such variable
while executing
"send_user "foo$bar\n""
$ expect <<END
send_user {$VPN_PASSWORD}; send_user "\n"
END
foo$bar
In Tcl, braces act like single quotes in the shell: everything inside them are literal characters.
It might be cleaner to use the environment to pass the values. Here it is implemented as a shell function
vpnconnect() {
expect <<'END'
set timeout 30
spawn /opt/cisco/anyconnect/bin/vpn -s connect $env(VPN_HOST)
expect "Username:*"
send "\r"
expect "Password:*"
send "$env(VPN_PASSWORD)\r"
expect {accept? [y/n]:*}
send "y\r"
expect eof
END
}

How to suppress expect send output?

I have an expect script which I'd like to behave as a fancy ssh program that hops several machines and sets up the environment on target machine before running the commands.
I can use log_user 0/1 to turn off / on output from expect and that helps with password prompts and login banners, and commands to setup environment.
But, like ssh, once my script starts to issue commands, I don't want to see the issued command. That is I don't want to see "command" after send "command\n". All I want to see is the results of command.
How do I suppress the send output, but not the results?
Here's a snippet of the expect script:
log_user 1
foreach daline [lrange \$argv 0 end] {
send "\$daline\r"
set buffer1
}
So prior to this loop, I send password, and setup environment. Then in this loop, I run each bash command that was fed to the expect as an argument.
thanks.
Many programs echo their input. For example, if you send the date command to the shell, you will see the string date followed by a date. More precisely, you will see everything that you would ordinarily see at a terminal. This includes formatting, too.
send "date\r"
expect -re $prompt
The command above ends with expect_out (buffer) set to date\r\nFri Nov 7 20:47:32 IST 2014\r\n. More importantly, the string date has been echoed. Also, each line ends with a \r\n, including the one you sent with a \r. The echoing of date has nothing to do with the send command.
To put this another way, there is no way to send the string and have send not echo it because send is not echoing it in the first place. The spawned process is.
In many cases, the spawned process actually delegates the task of echoing to the terminal driver, but the result is the same-you see your input to the process as output from the process.
Often, echoed input can be handled by using log_user only (which you have used in different place). As an example, suppose a connection to a remote host has been spawned and you want to get the remote date, but without seeing the date command itself echoed. A common error is to write:
log_user 0 ;# WRONG
send "date\r" ;# WRONG
log_user 1 ;# WRONG
expect -re .*\n ;# WRONG
When run, the log_user command has no effect because expect does not read the echoed "date" until the expect command. The correct way to solve this problem is as follows:
send "date\r"
log_user 0
expect -re "\n(\[^\r]*)\r" ;# match actual date
log_user 1
puts "$expect_out(l,string)" ;# print actual date only
If you are sending a lot of commands to a remote shell it may be more convenient to just disable all echoing in the first place. You can spawn a shell and then send the command stty -echo, after which your commands will no longer be echoed. stty echo re enables echoing.
spawn ssh <host>
stty -echo; # Disable 'echo' here
expect something
#Your further code here
stty echo # Enable 'echo' here
#Close of connection
Reference : Exploring Expect

capture expect ssh output to variable

Hey am new to bash scripts and was wondering how would I capture the output of the ssh command into a bash variable? I looked around and cant seem to get it right. I have tried puts $expect_out(buffer) but when echo it says variable does not exist
I know the response should be just one line and if I want to save that into a variable response and then echo it how would I do that?
A generic idea can be something like as below.
spawn the ssh session
make proper login
Send each commands with send
Wait for desired output with expect
Example:
spawn ssh $user#$domain
expect "password" { send "$pwd\r"}
expect "#"; # This '#' is nothing but the terminal prompt
send "$cmd\r"
expect "#"
puts $expect_out(buffer); #Will print the output of the 'cmd' output now.
The word to wait for after executing the command can vary based on your system. It can be # or $ or > or :; So, make sure you are giving the correct one. Or, you can provide a generalized pattern for the prompt as such
set prompt "#|>|:|\\\$"; # We escaped the `$` symbol with backslash to match literal '$'
While using the expect after sending the commands, it can be used as
expect -re $prompt; #Using regex to match the pattern`

expect script, puting name of file in command

I have problems with expect script. Well when I grep this file I need to put it to line under and it should look like :
/opt/ericsson/arne/bin/import.sh -f bla_bla_bla.xml -val:rall
but I don't know how to put this file in beetween this line. Because when I have put grep command in beetween in didn't work, maybe problem is -val:rall that I have after?
If someone know's how could I put name of file in File1
#!/usr/local/bin/expect --
set env(TERM) vt100
set env(SHELL) /bin/sh
set env(HOME) /usr/local/bin
set PASSWORD ter
set DUL [lindex $argv 0]
set VAR _cus_ipsec
match_max 1000
spawn ssh mashost
expect {
"assword" {send "$PASSWORD\r"}
}
expect "ran#rn23"
send -- "cd /tih/opt/bla/tih/ \r"
expect "ran#rn23"
send -- "grep -il $DUL * \r*"
expect "ran#rn23"
send -- "/opt/bl/arne/bin/imp.sh -f File1 -val:rall\r"
expect "ran#rn03"
send -- "/opt/bl/arne/b/imp.sh -f File1 -import\r"
expect "ran#rn23"
interact
Ok, thanks for the clarification, I believe I do understand what you're trying to do now.
What you need to do is change the expect statement you have after you send the grep command to one that will capture your filename. And you will probably benefit from using the regexp mode of the expect command (-re), and possibly using parenthesis to capture the filename (not used in my sample below). I do not know what are the possible filenames that you can get from your grep, so you will probably need to tweak the below quite a bit, but assuming your grep will give you a single .xml file beginning with "NAME", you could do something like the following:
send -- "grep -il $DUL * \r*"
expect -re "NAME.*\.xml"
send -- "/opt/bl/arne/bin/imp.sh -f $expect_out(0,string) -val:rall\r"
As a suggestion, you should really include some timeout options for your expect statements, and some error checking, otherwise this script will not stop if anything does not go as expected. E.g. only send if you have found what you expected, etc.
Your regexp probably will be more complicated than the one I showed you, but you can get the idea. Also, include exp_internal 1 somewhere near the top of your script to get good, solid debugging info on what your script is matching (or not matching). It will be very useful as you test it.
Let me know how that goes.

Expect exits too soon

I have the following bash script (script.sh):
#!/bin/bash
read -p "Remove? (y|n): " answer
echo "You answered '$answer'."
and I would like to drive it using expect. I have the following script (expect.exp, in the same directory):
#!/usr/bin/expect -f
set timeout -1
spawn -noecho ./script.sh
expect "^Remove"
send "y\r"
but it doesn't work as expected (pun intended). The result is:
~/Playground$ ./expect.exp
Remove? (y|n): ~/Playground$
So, the expect script somehow fails on the first 'expect "^Remove"' line and exits immediately, and the rest of script.sh does not execute. What am I doing wrong here?
I have been following the basic tutorials found online (the ones with the ftp examples). I am using expect 5.45 on Kubuntu 12.10.
Edit
So it changes if I add either 'interact' or 'expect eof' at the very end. But I have no idea what happens and why. Any help?
Two things I see:
"^Remove" is a regular expression, but by default expect uses glob patterns. Try
expect -re "^Remove"
while developing your program, add exp_internal 1 to the top of the script. Then expect will show you what's happening.
Ah, I see that expect adds special meaning to ^ beyond Tcl's glob patterns.
However, because expect is not line oriented, these characters (^ and $) match the beginning and end of the data (as opposed to lines) currently in the expect matching buffer
So what you see is that you send y\r and then you expect script exits as it has nothing more to do. When your script exits, the spawned child process will be killed. Hence the need to wait for the spawned child to end first: expect eof
Problem
You are not matching any text after the shell script's prompt, so the buffer for your spawned process never gets printed. The script finishes, the spawned process closes, and that's the end of the story.
Solution
Make sure you expect a specific response or your shell script's EOF, and then print your buffer. For example:
#!/usr/bin/expect -f
spawn -noecho "./script.sh"
expect "Remove" { send "y\r" }
expect EOF { send_user $expect_out(buffer) }

Resources