Connecting to switches using SSH and Expect - ssh

I'm a newbie to expect (lot's of shell programming though).
I'm trying to connect to a switch using expect to be able to dump it's running config. But somehow my send commands appear not be send to the switch.
Script:
#!/usr/bin/expect -f
proc connect {passw} {
expect {
"assword:" {
send "$passw\r"
expect {
"Prompt>" {
return 0
}
}
}
}
# timed out
return 1
}
set user [lindex $argv 0]
set passw [lindex $argv 1]
set host [lindex $argv 2]
#check if all were provided
if { $user == "" || $passw == "" || $host == "" } {
puts "Usage: <user> <passw> <host>\n"
exit 1
}
spawn ssh -oStrictHostKeyChecking=no -oCheckHostIP=no $user#$host
set rez [connect $passw]
if { $rez == 0 } {
send {enable\r}
send {show running\r}
exit 0
}
puts "\nError connecting to switch: $host, user: $user!\n"
exit 1
Alas when I run the script (trigged from a shell script) I can see it login to the switch and see the 'Prompt', but after that the script seems to exit, because I don't see the 'enable' and 'show running' commands being executed.
Anyone can show me how to fix this?
Richard.

You need add further expect statement after sending the commands using send.
spawn ssh -oStrictHostKeyChecking=no -oCheckHostIP=no $user#$host
set rez [connect $passw]
set prompt "#"
if { $rez == 0 } {
send {enable\r}
expect $prompt; # Waiting for 'prompt' to appear
send {show running\r}
expect $prompt; # Waiting for the prompt to appear
exit 0
}
Without the expect command after using send, then expect has nothing to expect from the ssh process and it will simply send the commands in a faster way which is why you are not able to get any output.
One more update :
You are using a separate proc for login purpose. If you are using a separate proc, then you are recommended to pass the spawn handle. Else, the script may fail. The reason is, instead of send/expect from the process, it will send/expect from the stdin. So, you should have used it like this,
proc connect {passw handle} {
expect {
-i $handle
"assword:" {
send "$passw\r"
expect {
-i $handle
"Prompt>" {
return 0
}
}
}
}
# timed out
return 1
}
set handle [ spawn ssh -oStrictHostKeyChecking=no -oCheckHostIP=no $user#$host ]
set rez [connect $passw $handle ]
set prompt "#"
if { $rez == 0 } {
send -i $handle "enable\r"
expect -i $handle $prompt
send -i $handle "show running\r"
expect -i $handle $prompt
exit 0
}
We are saving the ssh spawn handle into the variable 'handle' and it is being used with send and expect and the flag -i is used to make them to wait for the corresponding process

#Dinesh has the main answer. A couple of other notes:
send {enable\r} -- since you use braces instead of quotes, you are sending the characters \ and r and not a carriage return.
you return 0 and 1 from connect like you're a shell programmer (not an insult, I am one too). Use Tcl's boolean values instead.
A quick rewrite:
#!/usr/bin/expect -f
set prompt "Prompt>"
proc connect {passw} {
expect "assword:"
send "$passw\r"
expect {
$::prompt {return true}
timeout {return false}
}
}
lassign $argv user passw host
#check if all were provided
if { $user == "" || $passw == "" || $host == "" } {
puts "Usage: [info script] <user> <passw> <host>"
exit 1
}
spawn ssh -oStrictHostKeyChecking=no -oCheckHostIP=no $user#$host
if {[connect $passw]} {
send "enable\r"
expect $::prompt
send "show running\r"
expect $::prompt
send "exit\r" # neatly close the session
expect eof
} else {
puts "\nError connecting to switch: $host, user: $user!\n"
}

Related

expect script reading empty line

I have a list of IPs that I am looping through to ssh into each of them and capture some logs. Currently, it will loop through all the IPs and do what I want, the issue occurs when it hits the last IP, after it is done with the last line, it attempts to spawn another empty line resulting in an error. (spawn ssh root#)
How can I prevent this error from happening?
myexpect.sh
set user user
set pass pass
set timeout 600
# Get the list of hosts, one per line #####
set f [open "/my/ip/list.txt"]
set hosts [split [read $f] "\n"]
close $f
# Iterate over the hosts
foreach host $hosts {
spawn ssh $user#$host
expect {
"connecting (yes/no)? " {send "yes\r"; exp_continue}
"assword: " {send "$pass\r"}
}
expect "# "
send "myscript.sh -x\r"
expect "# "
send "exit\r"
expect eof
}
myiplist.txt
172.17.255.255
172.17.255.254
...
error:
[root#172.17.255.255: ]# exit //last ip in the list
Connection to 172.17.255.255 closed.
spawn ssh root#
ssh: Could not resolve hostname : Name or service not known
expect: spawn id exp5 not open
Text files end with a newline
first line\n
...
last line\n
So when you read the whole file into a variable, then split on newlines, your list looks like this:
{first line} {...} {last line} {}
because there is an empty string after the last newline.
The idiomatic way to iterate over the lines of a file in Tcl/expect is this:
set f [open file r]
while {[gets $f host] != -1} {
do something with $host
}
close $f
Or, use the -nonewline option of the read command
set f [open file]
set hosts [split [read -nonewline $f] \n]
close $f
foreach host $hosts {...}

Capture exit code of Expect script

I have an expect script that will transfer the file from one server to another. This expect script is being kicked off by the shell script. The expect script can transfer the file if it is available in source and it will give a zero exit code. However, when the file is not available in source, the script will not transfer the file but it will still give a zero exit code.
How can I capture the correct exit code of the expect script.
test.sh:
#!/bin/sh
DT=`date +%Y%m%d`
RXHOME=/tmp'
x_server=A1234
x_file="test_${DT}.dat"
cd $RXHOME/scripts
./test.exp ${x_server} ${x_file}
if [ $? -ne 0 ]
then
echo "ERROR : Expect script encountered an error."
exit 1
fi
test.exp:
#!/usr/local/bin/expect -f
set timeout -1
set XSERVER [lindex $argv 0]
set XFILE [lindex $argv 1]
set uid "user"
set pwd "password"
set srcDir "/tmp/src"
set trgDir "/tmp/trg"
spawn /usr/ldir/bin/sftp ${uid}#${XSERVER}
expect -re "Are you sure you want to continue connecting (yes/no)?" {
send -- "yes\r"
expect -re "assword: "
send -- "$pwd\r"
} -re "assword: " {
send -- "$pwd\r"
}
expect -re "sftp> "
send -- "put ${srcDir}/${XFILE} ${trgDir}/test_file.txt\r"
expect "sftp> "
send -- "bye\r"
expect eof
catch wait result
exit [lindex $result 3]
Sample Output when there is no file available in source:
+ + date +%Y%m%d
DT=20160919
+ RXHOME=/tmp
+ x_server=A1234
+ x_file=test_20160919.dat
+ cd /tmp/scripts
+ ./test.exp A1234 test_20160919.dat
spawn /usr/ldir/bin/sftp user#A1234
Connecting to A1234...
user#A1234's password:
sftp> put /tmp/src/test_20160919.dat /tmp/trg/test_file.txt
File "/tmp/src/test_20160919.dat" not found.
sftp> bye
+ [ 0 -ne 0 ]
$
As you can see, even if there is no file transferred to target since it's not available in source, the script still exited with zero exit code.
You have to handle the error by yourself. Try like this:
set err 0
send -- "put ${srcDir}/${XFILE} ${trgDir}/test_file.txt\r"
expect {
-re "File .* not found" {
set err 1
exp_continue
}
"sftp> "
}
send -- "bye\r"
expect eof
exit $err
Why don't you just check that the file exists before spawning sftp?
set srcFile [file join $srcDir $XFILE]
if {! [file exists $srcFile]} {
puts stderr "File not found in source: $XFILE"
exit 1
}
spawn /usr/ldir/bin/sftp ${uid}#${XSERVER}
...

Ignore ORA-28011 in sqlplus in bash script

I am using sqlplus in a shell script and I am using WHENEVER SQLERROR EXIT 8 and WHENEVER OSERROR EXIT 9 so that I can catch errors using $?.
I will be putting this code on a server that I know gets the password expiry warning/error 'ORA-28011'.
My question is, will my script catch on 'ORA-28011' even though it isn't really an error? If so, how would I go about ignoring it?
My (simplified) code, if it helps:
[...]
CONNECTION_STRING=$USER/$PASS#$TNS
RESULT=$(sqlplus -s /nolog <<-EOF
WHENEVER OSERROR EXIT 9;
WHENEVER SQLERROR EXIT 8;
$OPTIONS
CONNECT $CONNECTION_STRING
$DB_SQL
COMMIT;
EOF)
RETURN_CODE=$?
echo "db_exec: Result -> $RETURN_CODE\n$RESULT"
if [ $RETURN_CODE -eq 0 ]
then
echo "$RESULT"
return 0
else
echo "db_exec: Failed"
return 1
fi
In case anyone is interested, I solved this problem.
ORA-28011 and ORA-28002 do not cause SQL*Plus to exit when using WHENEVER SQLERROR EXIT # or WHENEVER OSERROR EXIT # but will appear in the result. Therefore the code in my question will work, but I need to remove these errors. My updated code is below:
# Run the SQL with the options specified
RESULT=$(sqlplus -s /nolog <<-EOF
WHENEVER SQLERROR EXIT 4;
WHENEVER OSERROR EXIT 5;
SPOOL $TEMP_FILE;
$DB_OPTIONS
$DB_CONNECT
$DB_SQL
COMMIT;
EOF)
# Save the return code
RETURN_CODE=$?
# Log the result
echo "Result -> Code: $RETURN_CODE\n$RESULT" 1>&2
if [ $( grep -cE '^ORA-28002:|^ORA-28011:' $TEMP_FILE) -ge 1 ]
then
echo "Warning -> Password Expiry \n$(grep '^ORA-' $TEMP_FILE)" 1>&2
fi
# Check the return code and catch any SQL*Plus (SP2-) errors that might not have presented an error code
if [ $RETURN_CODE -eq 0 ] && [ $(grep -c '^SP2-[0-9][0-9][0-9][0-9]' $TEMP_FILE) -eq 0 ]
then
# Echo the result, but remove any lines regarding password expiry
echo "$RESULT" | grep -v "^ERROR:" | grep -v "^ORA-[0-9][0-9][0-9][0-9][0-9]"
rm $TEMP_FILE
echo "Success" 1>&2
return 0
elif [ $RETURN_CODE -eq 4 ]
then
echo "Failed -> SQL Error \n$(grep '^ORA-' $TEMP_FILE) $(grep '^SP2-' $TEMP_FILE)" 1>&2
return 4
elif [ $RETURN_CODE -eq 5 ]
then
echo "Failed -> OS Error \n$(grep '^ORA-' $TEMP_FILE) $(grep '^SP2-' $TEMP_FILE)" 1>&2
return 5
elif [ $(grep -c "^SP2-[0-9][0-9][0-9][0-9]" $TEMP_FILE) -ne 0 ]
then
echo "Failed -> SQL*Plus Error \n$(grep '^SP2-' $TEMP_FILE)" 1>&2
return 6
else
echo "Unknown error -> $RETURN_CODE\n$RESULT" 1>&2
return 3
fi

sending "ping" output to a variable

I am trying to ping some ip addresses in my router. I use this code:
for {set n 0} {$n < 10} {incr n} {puts [exec "ping 199.99.$n.1]}
but this will show the output.
the issue is that I don't want to see the output. I would like to send that output into another variable and the search the content of variable with "regexp" and get the result, and do the rest of the story.
but I don't know how I can do that.
Use the set command. The puts command prints it's argument.
set pingOutput [exec ping "199.99.$n.1"]
Or append if you want all IP's results in one variable.
set allPingOutput ""
for {set n 0} {$n < 10} {incr n} {
append allPingOutput [exec ping "199.99.$n.1"]
}
Try calling the ping with the -c flag:
ping -c 1 10.0.1.1
Not sure how to do it in tcl but in php for example:
It is very important to use ping -c1 <IP address> , otherwise the script will never end as the ping process never ends :)
My code uses an array of results of every IP
for {set i 2 } {$i < 10} {incr i} {
catch {if {[regexp {bytes from} [exec ping -c1 192.168.12.$i]]} {
set flag "reachable"
} else { set flag "not reachable"}
set result(192.168.12.$i) $flag
}
}
parray result
OUTPUT :
result(192.168.12.2) = reachable
result(192.168.12.3) = reachable
result(192.168.12.5) = reachable
result(192.168.12.6) = reachable
result(192.168.12.7) = reachable
result(192.168.12.9) = reachable
Instead of storing and manipulating , I used regexp .

Calling fgets() on popen() of 'ssh' is flushing the beginning of stdin of the calling process (ptty issue)

I have now whittled this down to a minimal test case. Thus far I have been able to determine that this is an issue related to pseudo-terminals which come about with the pipe of ssh. Adding the '-t -t' to the ssh call improved things, in that now, it takes a second call to fgets() to cause the issue. I suspect that the stderr output of the ssh command somehow works into the issue, for now I have redirected stderr to stdout in the ssh code to execute. I do wonder if the "tcgetattr: Invalid argument" error is part of the problem, but am not sure how to get rid of that. It seems to come from the -t -t being present. I believe the -t -t is moving in the right direction, but I have to set up the pseudo terminal for stderr somehow and perhaps the test will work properly?
The Makefile:
test:
gcc -g -DBUILD_MACHINE='"$(shell hostname)"' -c -o test.o test.c
gcc -g -o test test.o
.PHONY: clean
clean:
rm -rf test.o test
The test.c source file:
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int
main(int argc, char *argv[])
{
const unsigned int bufSize = 32;
char buf1[bufSize];
char buf2[bufSize];
int ssh = argv[1][0] == 'y';
const char *cmd = ssh ? "ssh -t -t " BUILD_MACHINE " \"ls\" 2>&1" : "ls";
FILE *fPtr = popen(cmd, "r");
if (fPtr == NULL) {
fprintf(stderr,"Unable to spawn command.\n");
perror("popen(3)");
exit(1);
}
printf("Command: %s\n", cmd);
if (feof(fPtr) == 0 && fgets(buf2, bufSize, fPtr) != NULL) {
printf("First result: %s\n", buf2);
if (feof(fPtr) == 0 && fgets(buf2, bufSize, fPtr) != NULL) {
printf("Second result: %s\n", buf2);
int nRead = read(fileno(stdin), buf1, bufSize);
if (nRead == 0) {
printf("???? popen() of ssh consumed the beginning of stdin ????\n");
} else if (nRead > 0) {
if (strncmp("The quick brown fox jumped", buf1, 26) != 0) {
printf("??? Failed ???\n");
} else {
printf("!!!!!!! Without ssh popen() did not consume stdin !!!!!!!\n");
}
}
}
}
}
This shows it running the passing way:
> echo "The quick brown fox jumped" | ./test n
Command: ls
First result: ARCH.linux_26_i86
Second result: Makefile
!!!!!!! Without ssh popen() did not consume stdin !!!!!!!
This shows it running the failing way:
> echo "The quick brown fox jumped" | ./test y
Command: ssh -t -t hostname "ls" 2>&1
First result: tcgetattr: Invalid argument
Second result: %backup%~ gmon.out
???? popen() of ssh consumed the beginning of stdin ????
Okay, I have got this working finally. The secret was to supply /dev/null as the input to my ssh command as follows from the test case above:
const char *cmd
= ssh ? "ssh -t -t " BUILD_MACHINE " \"ls\" 2>&1 < /dev/null" : "ls";
However, while the code works correctly, I get a nasty message which apparently I can ignore for my purposes (although I'd like to make the message go away):
tcgetattr: Inappropriate ioctl for device