get the result of remote command in Expect - tcl

I have Expect script invoked from a bash script. The Expect script spawns
a ssh session and runs a remote command. I want to have a return code
of the remote command available in my shell script.
# !/bin/sh
expect -f remote.exp 192.168.1.100 user pass 10.10.10.1
# here I want to analyze exit status of remotely invoked command
The expect script is below, here is my ugly workaround:
#!/usr/bin/expect
set timeout 20
set box [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
set target [lindex $argv 3]
spawn ssh -l $user $box
expect "*password:"
send "$password\r\n"
expect "*#"
send "ping -c1 $target\r\n"
expect "*#"
send "echo $?\r"
expect {
"0\r" { puts "Test passed."; }
timeout { puts "Test failed."; }
}
expect "*#"
PS. I read that expect_out may help me, but I failed to get it work. Any help
will be greatly appreciated! Thanks.

I have tried the following and it works on a Mac:
#!/usr/bin/expect
# Process command parameters
foreach {host user password target} $argv {break}
set command "ping -c 1 $target"
# Log in
spawn ssh -l $user $host
expect -nocase "password:"
send "$password\r"
# Send the command
expect "$ "
send "$command\r"
# Echo the return code, the key here is to use the format code=X instead
# just a single digit. This way, Expect can find it
expect "$ "
send "echo code=$?\r"
# Analyze the result
set testResult "Test failed."
expect {
"code=0" { set testResult "Test passed." }
}
puts "$testResult"
# Done, exit
expect "$ "
send exit
Note a few differences:
The shell prompt on my system is "$ "
I only send \r, not \r\n. I don't know if it matters
I exit the ssh session by sending "exit"

Related

Expect script failed with "spawn id not open" when calling "expect eof"

I am trying to call an expect script from bash and have it return exit code in case of e.g. Connection Timeout.
Basically the bash script is calling expect like this:
if [[ -f $1 ]]; then
do something
else
script.exp $device
# here I want to evaluate the exit code but it is always 0
fi
I found some previous questions about it and that the right way to catch exit codes is:
expect eof
catch wait result
exit [lindex \$result 3].
The expect script:
#!/usr/bin/expect
exp_internal 1
log_user 1
set timeout -1
set hostname [lindex $argv 0]
set prompt "sftp>";
if { $hostname == "blah" } {
set username "user1"
set password "pass1"
} else {
set username "user2"
set password "pass2"
}
spawn sftp $username#$hostname "-o ConnectTimeout=3"
expect {
"timeout" {
puts "A timeout occured"
exp_continue
}
"password:" {
send "$password\r"
exp_continue
}
"sftp>" {
send "cd cfg\n"
expect $prompt
send "get * /some/dir/$hostname\n"
expect $prompt
send "get running-config /some/dir/$hostname-config\n"
expect $prompt
send "exit\r";
exp_continue
}
}
expect eof
catch wait result
exit [lindex \$result 3]
When I just test the expect script to see what happens, I get this error:
expect: spawn id exp4 not open
while executing
"expect eof"
I've tried moving expect eof around but it does not change the behavior. It states that the spawned process id is not open, which I guess is correct as it has exited with Timed Out/Connection closed?
Remove exp_continue after send "exit\r", otherwise EOF will be consumed by the expect { ... } block and another expect eof would fail.
Change lindex \$result 3 to lindex $result 3 as in the comments.
(Incorporating the previous comments)
Add it into the previous expect command, but with no body:
expect {
timeout {...}
password: {...}
sftp> {...}
eof
}
wait result
if {[lindex $result 2] == 0} {
exit [lindex $result 3]
} else {
error "system error: errno [lindex $result 3]"
}

Automating xterm using Expect

I am trying to automate xterm window using Expect (though I already knew Expect cant control such GUI applications, but there is a tweaked mechanism explained in Exploring Expect)
package require Expect
spawn -pty
stty raw -echo < $spawn_out(slave,name)
regexp ".*(.)(.)" $spawn_out(slave,name) dummy c1 c2
if {[string compare $c1 "/"] == 0} {
set c1 "0"
}
set xterm_pid [exec xterm -S$c1$c2$spawn_out(slave,fd) &]
close -slave
expect "\n" ;# match and discard X window id
set xterm $spawn_id
spawn $env(SHELL)
Don Libes mentioned that from this point, xterm can be automated and he has given example to use xterm with interact command as follows,
interact -u $xterm "X" {
send -i $xterm "Press return to go away: "
set timeout -1
expect -i $xterm "\r" {
send -i $xterm "Thanks!\r\n"
exec kill $xterm_pid
exit
}
}
But, my expectation is send and expect commands to/from xterm. I have tried the following,
send -i $xterm "ls -l\r"; # Prints commands on xterm
expect -i $xterm "\\\$" ; # Trying to match the prompt
But it didn't workout. This example mainly relies on the xterm's command line option -Sccn.
-Sccn
This option allows xterm to be used as an input and output channel for
an existing program and is sometimes used in specialized applications.
The option value specifies the last few letters of the name of a
pseudo-terminal to use in slave mode, plus the number of the inherited
file descriptor. If the option contains a "/" character, that delimits
the characters used for the pseudo-terminal name from the file
descriptor. Otherwise, exactly two characters are used from the option
for the pseudo-terminal name, the remainder is the file descriptor.
Examples:
-S123/45
-Sab34
Note that xterm does not close any file descriptor which it did not open for its own use. It is possible (though probably not
portable) to have an application which passes an open file descriptor
down to xterm past the initialization or the -S option to a process
running in the xterm.
Where am I making the mistake ?
Here I have you a view from my code I used. It is extracted from a complex part.
# create pty for xterm
set spawn(PTTY,PID) [spawn -noecho -pty]
set spawn(PTTY,DEVICE) $spawn_out(slave,name)
set spawn(PTTY) $spawn_id
stty raw -echo < $spawn(PTTY,DEVICE)
regexp ".*(.)(.)" $spawn_out(slave,name) dummy c1 c2
if {[string compare $c1 "/"] == 0} { set c1 0 }
# Start XTERM (using -into can place the xterm in a TK widget)
set pid(XTERM) [::exec xterm -S$c1$c2$spawn_out(slave,fd) {*}$addidtionlXtermOptions &]
close -slave
# Link
set spawn(SHELL,PID) [spawn -noecho {*}$commandInXterm]
set spawn(SHELL) $spawn_id
set spawn(SHELL,DEVICE) $spawn_out(slave,name)
# ...
# send a key or string into the xterm
exp_send -raw -i $spawn(SHELL) -- $key
exp_send -raw -i $spawn(SHELL) -- "$str\r
As Mr.Thomas Dickey pointed out here, I started exploring on the multixterm
and finally able to make a standalone version where the commands are sent to xterm directly.
The part which mainly I have missed in my code is expect_background which actually does the linking in the background. Hope it helps to all those who all wanted to automate the xterm. All credits to Mr.Thomas Dickey and Mr.Don Libes!!!
#!/usr/bin/tclsh
package require Expect
set ::xtermStarted 0
set xtermCmd $env(SHELL)
set xtermArgs ""
# set up verbose mechanism early
set verbose 0
proc verbose {msg} {
if {$::verbose} {
if {[info level] > 1} {
set proc [lindex [info level -1] 0]
} else {
set proc main
}
puts "$proc: $msg"
}
}
# ::xtermSid is an array of xterm spawn ids indexed by process spawn ids.
# ::xtermPid is an array of xterm pids indexed by process spawn id.
######################################################################
# create an xterm and establish connections
######################################################################
proc xtermStart {cmd name} {
verbose "starting new xterm running $cmd with name $name"
######################################################################
# create pty for xterm
######################################################################
set pid [spawn -noecho -pty]
verbose "spawn -pty: pid = $pid, spawn_id = $spawn_id"
set ::sidXterm $spawn_id
stty raw -echo < $spawn_out(slave,name)
regexp ".*(.)(.)" $spawn_out(slave,name) dummy c1 c2
if {[string compare $c1 "/"] == 0} {
set c1 0
}
######################################################################
# start new xterm
######################################################################
set xtermpid [eval exec xterm -name dinesh -S$c1$c2$spawn_out(slave,fd) $::xtermArgs &]
verbose "xterm: pid = $xtermpid"
close -slave
# xterm first sends back window id, save in environment so it can be
# passed on to the new process
log_user 0
expect {
eof {wait;return}
-re (.*)\n {
# convert hex to decimal
# note quotes must be used here to avoid diagnostic from expr
set ::env(WINDOWID) [expr "0x$expect_out(1,string)"]
}
}
######################################################################
# start new process
######################################################################
set pid [eval spawn -noecho $cmd]
verbose "$cmd: pid = $pid, spawn_id = $spawn_id"
set ::sidCmd $spawn_id
######################################################################
# link everything back to spawn id of new process
######################################################################
set ::xtermSid($::sidCmd) $::sidXterm
set ::xtermPid($::sidCmd) $xtermpid
######################################################################
# connect proc output to xterm output
# connect xterm input to proc input
######################################################################
expect_background {
-i $::sidCmd
-re ".+" {
if {!$::xtermStarted} {set ::xtermStarted 1}
sendTo $::sidXterm
}
eof [list xtermKill $::sidCmd]
-i $::sidXterm
-re ".+" {
if {!$::xtermStarted} {set ::xtermStarted 1}
sendTo $::sidCmd
}
eof [list xtermKill $::sidCmd]
}
vwait ::xtermStarted
}
######################################################################
# connect main window keystrokes to all xterms
######################################################################
proc xtermSend {A} {
exp_send -raw -i $::sidCmd -- $A
}
proc sendTo {to} {
exp_send -raw -i $to -- $::expect_out(buffer)
}
######################################################################
# clean up an individual process death or xterm death
######################################################################
proc xtermKill {s} {
verbose "killing xterm $s"
if {![info exists ::xtermPid($s)]} {
verbose "too late, already dead"
return
}
catch {exec /bin/kill -9 $::xtermPid($s)}
unset ::xtermPid($s)
# remove sid from activeList
verbose "removing $s from active array"
catch {unset ::activeArray($s)}
verbose "removing from background handler $s"
catch {expect_background -i $s}
verbose "removing from background handler $::xtermSid($s)"
catch {expect_background -i $::xtermSid($s)}
verbose "closing proc"
catch {close -i $s}
verbose "closing xterm"
catch {close -i $::xtermSid($s)}
verbose "waiting on proc"
wait -i $s
wait -i $::xtermSid($s)
verbose "done waiting"
unset ::xtermSid($s)
set ::forever NO
}
######################################################################
# create windows
######################################################################
# xtermKillAll is not intended to be user-callable. It just kills
# the processes and that's it. A user-callable version would update
# the data structures, close the channels, etc.
proc xtermKillAll {} {
foreach sid [array names ::xtermPid] {
exec /bin/kill -9 $::xtermPid($sid)
}
}
rename exit _exit
proc exit {{x 0}} {xtermKillAll;_exit $x}
xtermStart $xtermCmd $xtermCmd
xtermSend "ls -l\r"
xtermSend "pwd\r"
vwait ::forever

Problems looping to prompt for another password

I need some help with an EXPECT script please....
I'm trying to automate a login, prior to accessing a load of hosts, and cater for when a user enters a password incorrectly. I am getting the username and password first, and then validating this against a particular host. If the password is invalid, I want to loop round and ask for the username and password again.
I am trying this :-
(preceding few irrelevant lines omitted)
while {1} {
send_user "login as:- "
expect -re "(.*)\n"
send_user "\n"
set user $expect_out(1,string)
stty -echo
send_user "password: "
expect -re "(.*)\n"
set password $expect_out(1,string)
stty echo
set host "some-box.here.there.co.uk"
set hostname "some-box"
set host_unknown 0
spawn ssh $user#$host
while {1} {
expect {
"Password:" {send $password\n
break}
"(yes/no)?" {send "yes\n"}
"Name or service not known" {set host_unknown 1
break}
}
}
if {$host_unknown < 1} {
expect {
"$hostname#" {send "exit\r"
break
}
"Password:" {send \003
expect eof
close $spawn_id
puts "Invalid Username or Password - try again..."
}
}
} elseif {$host_unknown > 0} {
exit 0}
}
puts "dropped out of loop"
And now I can go off and do lots of stuff to lots of boxes .....
This works fine when I enter a valid username or password, and my script goes off and does all the other stuff I want, but when I enter an invalid password I get this :-
Fred#Aserver:~$ ./Ex_Test.sh ALL
login as:- MyID
password: spawn ssh MyID#some-box.here.there.co.uk
Password:
Password:
Invalid Username or Password - try again...
login as:- cannot find channel named "exp6"
while executing "expect -re "(.*)\n""
invoked from within "if {[lindex $argv 1] != ""} {
puts "Too many arguments"
puts "Usage is:- Ex_Test.sh host|ALL"
} elseif {[lindex $argv 0] != ""} {
while {1} {
..."
(file "./Ex_Test.sh" line 3)
Its the line "can not find channel named "exp6" which is really bugging me.
What am I doing wrong? I am reading Exploring Expect (Don Lines) but getting nowhere....
Whenever expect is supposed to wait for some word, it will save the spawn_id for that expect process into expect_out(spawn_id).
As per your code, expect's spawn_id is generated when it encounters
expect -re "(.*)\n"
When user typed something and pressed enter key, it will save the expect's spawn_id. If you have used expect with debugging, you might have seen the following in the debugging output
expect does "" (spawn_id exp0) match regular expression "(.*)\n"
Lets say user typed 'Simon', then the debugging output will be
expect: does "Simon\n" (spawn_id exp0) match regular expression "(.*)\n"? Gate "*\n"? gate=yes re=yes
expect: set expect_out(0,string) "Simon\n"
expect: set expect_out(1,string) "Simon"
expect: set expect_out(spawn_id) "exp0"
expect: set expect_out(buffer) "Simon\n"
As you can see, the expect_out(spawn_id) holds the spawn_id from which it has to expect for values. In this case, the term exp0 pointing the standard input.
If spawn command is used, then as you know, the tcl variable spawn_id holds the reference to the process handle which is known as the spawn handle. We can play around with spawn_id by explicitly setting the process handle and save it for future reference. This is one good part.
As per your code, you are closing the ssh connection when wrong password given with the following code
close $spawn_id
By taking advantage of spawn_id, you are doing this and what you are missing is that setting the expect's process handle back to it's original reference handle. i.e.
While {1} {
###Initial state. Nothing present in spawn_id variable ######
expect "something here"; #### Now exp0 will be created
###some code here ####
##Spawning a process now###
spawn ssh xyz ##At this moment, spawn_id updated
###doing some operations###
###closing ssh with some conditions###
close $spawn_id
##Loop is about to end and still spawn_id has the reference to ssh process
###If anything present in that, expect will assume that might be current process
###so, it will try to expect from that process
}
When the loop executes for the 2nd time, expect will try to expect commands from the spawn_id handle which is nothing but ssh process which is why you are getting the error
can not find channel named "exp6"
Note that the "exp6" is nothing but the spawn handle for the ssh process.
Update :
If some process handle is available in the
spawn_id, then expect will always expect commands from that
process only.
Perhaps you can try something like the following to avoid these.
#Some reference variable
set expect_init_spawn_id 0
while {1} {
if { $expect_spawn_id !=0 } {
#when the loop enters from 2nd iteration,
#spawn_id is explicitly set to initial 'exp0' handle
set spawn_id $expect_init_spawn_id
}
expect -re "(.*)\n"
#Saving the init spawn id of expect process
#And it will have the value as 'exp0'
set expect_init_spawn_id $expect_out(spawn_id)
spawn ssh xyz
##Manipulations here
#closing ssh now
close $spawn_id
}
This is my opinion and it may not be the efficient approach. You can also think of your own logic to handle these problems.
You simply need to store the $spawn_id as a temp variable before a nested expect command, then set the $spawn_id to the temp variable after a nested expect command.
Also, get rid of the while {1} loops. They are not needed because expect behaves like a loop provided you use exp_continue whenever you don't wish to exit. You don't need expect eof nor do you need close $spawn_id. I don't use them in the following example:
#!/usr/bin/expect
set domain [lindex $argv 0];
set timeout 300
spawn ./certbot-add.sh $domain
expect {
"*replace the certificate*" {
send "2\r";
exp_continue;
}
"*_acme-challenge*" {
puts [open output.txt w] $expect_out(buffer)
spawn ./acme-add.sh $domain
set tmp_spawn_id $spawn_id
expect {
"$ "
}
set spawn_id $tmp_spawn_id
send "\r";
exp_continue;
}
"*certificate expires on*" {
puts "Certificate Added!"
}
}

eof not recognized in Expect Script

i have got some problems in my expect script.
I am spawning a scp file upload but the eof is not recognized.
The expect rans into the timeout before the script goes on.
What am i doing wrong??
Here is the script
function ssh_upload_file () {
test_log INFO "Uploading File $3 to Device $2 with a $1 seconds timeout" "ssh_upload_file"
last_log_entry_to_detail
expect <<- DONE
set outfilename "$DETAIL_LOG"
log_file "$DETAIL_LOG";
set timeout $1
set lantime_ip $2
system "touch ~/.ssh/known_hosts"
system "ssh-keygen -R $2"
spawn /bin/bash
sleep 3
send_user "Uploading File via SSH...\n"
send "scp $2 $3:$4 || echo FWTEST_SSH_RESULT=\$?\n"
expect "assword:" {
after 1000
send "$DUT_PWD\r"
#set timeout 5
#expect "y/n"
#send "n\n"
#set timeout $1
#expect "#"
#send "no_shell_timeout\n"
}
expect eof
send_user "Done."
log_file
exit 0
expect eof
DONE
LAST_ERROR=$?
return $LAST_ERROR
}
You spawn a bash shell, and send an scp command. When scp completes, you're sitting at a bash prompt. You need to send bash an exit command before you'll see eof.
Alternatively, you're not doing anything in that bash session except capture the output of the scp command which you can do like this:
spawn scp $2 $3:$4
expect "assword:"
send "$DUT_PWD\r"
expect eof
set info [wait]
puts "FWTEST_SSH_RESULT=[lindex [set info] 3]"
Addressing Donal's comment:
spawn scp $2 $3:$4
expect {
"assword:" {
send "$DUT_PWD\r"
exp_continue
}
eof
}
set info [wait]
puts "FWTEST_SSH_RESULT=[lindex [set info] 3]"

Expect is not behaving as expected when I send a command that is too long to fit on a single line

I am trying to create an expect script which will grep a file and return the line which contains the string I am looking for, in this case the string will be a terminal ID. For example I have a file called terminal_list.txt with the following contents:
0x400 192.168.62.133 10006
0x420 192.168.62.133 10021
0x440 192.168.62.133 10022
and I want the line returned which starts with 0x420
My code is as follows:
#!/usr/bin/expect -f
set terminal_list "/home/linkway/terminal_list.txt"
set termid "0x400"
spawn /bin/bash
expect "] "
# Get ip and port of terminal
# Check if termid exists in terminal_list file
set command "grep -q '$termid' '$terminal_list' && echo 'true' || echo 'false'"
send "$command\r"
expect "$command\r\n"
expect -re "(.*)\r\n.*] "
set val $expect_out(1,string)
# If terminal_list does not exist print error and exit
if { [string first "No such file or directory" $val] >= 0 } {
puts "$terminal_list does not exist. Script Failed"
exit
# If terminal_list does not contain termid print error and continue
} elseif { [string match "false" $val] } {
puts "\nTerminal $termid does not exist in ${terminal_list}. Cannot update bootfile.\n"
# If termid is found in terminal_list, get the ip and port of the terminal
} else {
set command "grep '$termid' '$terminal_list'"
send "$command\r"
expect "$command\r\n"
expect -re "(.*)\r\n.*] "
set val $expect_out(1,string)
set ip_port [string range $val 6 end]
}
This works perfectly when I ssh to the RHEL server via putty and run the script in a maximized putty window. HOWEVER when I shrink the window length so that the grep command no longer fits on a single line my code breaks! Can anyone please help me come up with a solution to this? I have been struggling with the processing of expect_out and could really use some guidance.
EDIT: I found what was causing the bug. It turns out that when the grep command is split over multiple lines, a \r is added to the command where the line break is. Here is some debugging info from exp_internal 1. You can see how the \r is added into the grep command where the command ran onto the next line:
expect: does "grep -q '0x400' '/home/linkway/term \rinal_list.txt'
&& echo 'true' || echo 'false'\r\n" (spawn_id exp6)
match glob pattern "grep -q '0x400' '/home/linkway/terminal_list.txt'
&& echo 'true' || echo 'false'\r\n"? no
Why is this happening and what is the proper way to get just the output of the grep command? I find it very strange that expect would behave differently base on how the output of a command is displayed on screen. Any help is greatly appreciated.
I was able to find a cleaner solution to my problem by making my script more expect-like. Here's what it looks like:
set command "grep -q '$termid' '$terminal_list' && echo 'true' || echo 'false'"
send "$command\r"
expect {
-re ".*\r\ntrue\r\n.*] " {
send "grep '$termid' '$terminal_list'\r"
expect ".*\r\n"
expect -re "(.*)\r\n.*] "
set val $expect_out(1,string)
set ip_port [string range $val 6 end]
puts "\nUpdating $termid bootfile"
updatebootfile $ip_port $boot_data $termid
}
-re ".*\r\nfalse\r\n.*] " {
puts "\nTerminal $termid does not exist in ${terminal_list}. Cannot update bootfile.\n"
}
-re "No such file or directory.*] " {
puts "$terminal_list does not exist. Script Failed"
exit 1
}
timeout {
puts "expect timeout when searching for $termid in $terminal_list"
}
}