How to handle "no such file or directory" in expect script? - error-handling

I have script that uses #!/usr/bin/expect to connect/disconnect to VPN.
But not all of my PCs configured similarly so VPN command can be at PATH but it also can be not.
My question is: how to handle moment when there is no such command in PATH. Script just stops when he tries to execute command that is not presented in PATH.
Here is script that I'm wrote so far:
#!/usr/bin/expect -d
set timeout -1
spawn vpn status
expect {
"Disconnected" {
expect eof
set timeout -1
spawn vpn connect <vpnurl>
expect "Username:"
send "username\r"
expect "Password:"
send "password\r"
expect eof
}
"Connected" {
expect eof
set timeout -1
spawn vpn disconnect
expect eof
}
"*" {
set timeout -1
spawn /opt/cisco/anyconnect/bin/vpn status
expect {
"Disconnected" {
expect eof
set timeout -1
spawn /opt/cisco/anyconnect/bin/vpn connect <vpnurl>
expect "Username:"
send "username\r"
expect "Password:"
send "password\r"
expect eof
}
"Connected" {
expect eof
set timeout -1
spawn /opt/cisco/anyconnect/bin/vpn disconnect
expect eof
}
}
}
}
And error that I receive when command not in PATH:
spawn vpn status
couldn't execute "vpn": no such file or directory
while executing
"spawn vpn status"
I tried to use which vpn and command -v vpn to check if vpn is here but they just don`t produce any output.

expect is a tcl extension, and tcl has a few options for trapping and handling errors: try and catch. Something along the lines of:
if {[catch {spawn vpn status} msg]} {
puts "spawn vpn failed: $msg"
puts "Trying again with absolute path"
spawn /path/to/vpn status
}

You can do the existence test in expect/tcl before attempting to spawn it:
foreach vpn {vpn /opt/cisco/anyconnect/bin/vpn} {
set path [auto_execok $vpn]
if {$path ne "" && [file executable $path]} {
set vpn_exec $path
break
}
}
if {$vpn_exec eq ""} {
puts stderr "vpn not executable: is it installed?"
exit 1
}
spawn $vpn_exec status
...
See
file
auto_execok

Related

How do I ensure the pty associated with an ssh connection in expect is closed properly?

I have an expect script running on a raspberry pi, connecting via ssh to another box, running a few commands, and then logging out. This is supposed to then run a million times over a few days. The script however crashes consistantly after about 8000 iterations with the following error:
Connection no: 8303
Error message: The system has no more ptys. Ask your system administrator to create more.
Error info:
The system has no more ptys. Ask your system administrator to create more.
while executing
"spawn ssh testuser#$ip_addr"
(procedure "login_root" line 17)
invoked from within
"login_root "0.0.0.0" session1"
The script is as follows:
#!/usr/bin/expect -f
#run file using nohup scriptname > /dev/null & for background running
source "~/expect_scripts/expect_commands.sh"
for {set i 0} {$i < 1000000} {incr i} {
if {[catch {
send_user "******************************************$i\n"
set ret [login_root "0.0.0.0" session1 pty1]
set ret [report_folder_size "/tmp"]
send_cmd "logout\"
close $session1
#close -slave pty1
} errmsg]} {
#handle error messages here
set outFileId [open $outputFilename "a"]
puts -nonewline $outFileId "Connection no: $i \nError message: $errmsg\n"
puts -nonewline $outFileId "Error info: \n\n$errorInfo\n\n"
}
}
And the relevant login_root function from commandss.sh:
proc login_root { ip_addr session_id pty_id {tout 30}} {
global spawn_id
global connection_stack
global root_prompt
global root_password
global testuser_password
global testuser
global test_prompt
upvar $session_id sess_id
upvar $pty_id ptyID
set timeout $tout
if {[info exists spawn_id]} {
set current_spawn_id $spawn_id
send_user "\n***DEBUG: current_spawn_id = $current_spawn_id\n"
set ret [spawn ssh testuser#$ip_addr]
if { $ret == 0 } {
send_user "\n***DEBUG: login_root: spawn failed\n"
set spawn_id $current_spawn_id
return 1
} else {
set ptyID spawn_out(slave,fd)
}
} else {
send_user "\n***DEBUG: spawn_id doesn exist yet (no session opened yet)\n"
set ret [spawn ssh testuser#$ip_addr]
if { $ret == 0 } {
send_user "\n***DEBUG: login_root: spawn failed\n"
return 1
} else {
set ptyID spawn_out(slave,fd)
}
}
expect {
timeout { # We timed out
send_user "\n***DEBUG: login_root: timed out. popped to spawn_id = $spawn_id\n"
return 1
}
"(yes/no)?" { # This is a new target
send "yes\n"
exp_continue
}
"testuser#0.0.0.0's password" {
send "$testuser_password\n"
exp_continue
}
"$root_prompt" { # All done - success
set sess_id $spawn_id
return 0
}
default { # We got an unexpected response
send_user "\n***DEBUG: login_root: Login failed. popped to spawn_id = $spawn_id\n"
return 1
}
}
set sess_id $spawn_id
return 0
}
I have checked and found that kernal/pty/max is 4k, half of the 8k connections I am getting through. the line "close -slave" or "close -slave pty1" is an attempt to ensure the pty associated with the ssh connection created by spawn ssh is closed properly, but it errors out every time saying
testuser# cannot find channel named "-slave"
while executing
"close -slave $pty1"

Expect: answering one or two ssh password prompts then interact

I have an ssh case (can't use sshkeys) that sometimes prompts for one password, sometimes for two (prompt is the same in both cases). I have tried variations of while, if, exp_continue, but can't seem to cook it correctly.
This works perfect if I only get one prompt:
#!/usr/bin/expect
set pwd "mypwd"
set prompt "*Password*"
set uid "*userid*"
set timeout -1
spawn -noecho ssh -q host.domain
expect $prompt
send $pwd
send \r
interact
Tried this, but not giving giving me the results I need for sometimes two pwds plus the KnownHost case.
expect {
"*yes/no*" { send \"yes\r\"; exp_continue }
$prompt { send \"$pwd\r\"; exp_continue }
$uid {interact}
}
Figured it out:
#!/usr/bin/expect -f
set pwd "mypwd"
set prompt "Enter PIN for 'PIV_II (PIV Card Holder pin)': "
set uid "*userid*"
set timeout -1
spawn ssh -q host.domain
expect {
"*yes/no*" { send "yes\r"; exp_continue }
$prompt { send "$pwd\r"; exp_continue }
$uid { send \r; interact }
}

Using nested spawn method in expect scripts

I have written following expect script:
/usr/bin/expect<<EOF
set SERVER_HOSTNAME "$env(SERVER_HOSTNAME)"
set USERNAME "$env(USERNAME)"
set PASSWORD "$env(PASSWORD)"
set timeout -1
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." { spawn ssh-keygen -R "$SERVER_HOSTNAME"; expect "known_hosts.old"; send_user "Updated host key details."; exp_continue}
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
EOF
Here I want that if the host key of a server can't be verified while spawing "ssh" process, then the nested spawn process "ssh-keygen -R" should remove old key. Then the "ssh" process should try to connect again so that new key can be added corresponding to that server.
But here, after the execution of:
send_user "Updated host key details."
method, expect process is exiting out from this script.
I know the alternative can be to divide this expect call into two steps, as follows:
/usr/bin/expect<<EOF
set SERVER_HOSTNAME "$env(SERVER_HOSTNAME)"
set USERNAME "$env(USERNAME)"
set PASSWORD "$env(PASSWORD)"
set timeout -1
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." { spawn ssh-keygen -R "$SERVER_HOSTNAME"; expect "known_hosts.old"; send_user "Updated host key details."; exp_continue}
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
EOF
But do we have a way to execute this expect call in one go. In nutshell, I want to know, can we do the nesting of spawn process?
Whitespace goes a long way toward maintainable code: you don't need to squish so many commands per line.
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." {
spawn ssh-keygen -R "$SERVER_HOSTNAME"
expect "known_hosts.old"
send_user "Updated host key details."
exp_continue
}
"continue connecting (yes/no)" {
send "yes\r"
expect "Permanently added"
exp_continue
}
"assword:" {
send_user "Keygen details are correctly mapped for this server\n"
}
}
I this case, you don't need to interact with ssh-keygen, so use exec to simply call it
"Host key verification failed." {
exec ssh-keygen -R "$SERVER_HOSTNAME"
puts "Updated host key details."
exp_continue
}
If you need to spawn something and interact with it, you need to know that there is an implicit variable, spawn_id, created by spawn, and used by expect and send. You'll need to save the spawn_id of the current process before you spawn any other process. For example:
spawn process1
set ids(1) $spawn_id
expect -i $ids(1) "some pattern"
send -i $ids(1) "some string\r"
spawn process2
set ids(2) $spawn_id
expect -i $ids(2) "some pattern from process2"
send -i $ids(2) "some string to process2\r"

show ip interface brief in expect script

I would like to be able to setup a expect script file that would remote into multiple switches.
Do you know how to write expert script for telnet to 200 switches and run a command example: show ip interface brief then list all switch status on Linux??
#!/usr/bin/expect -f
spawn telnet 192.168.0.1
expect "Username:"
send "MyUsername\r"
expect "assword:"
send "MyPassword\r"
send "show ip int br\r"
interact timeout 5
expect {
"MORE --, next page" {send -- " "; exp_continue}
"*?2626#*" {send -- "exit \r"}
}
Thanks Dinesh, after my expect script as below:
!/usr/bin/expect -f
Slurp up the input file
set fp [open "input.txt" r]
set file_data [read $fp]
close $fp
set prompt ">"
log_file -noappend switch_port_status.txt
foreach ip [split $file_data "\n"] {
puts "Switch $ip Interface Status"
spawn telnet $ip
expect "Username:"
send "MyUsername\r"
expect "assword:"
send "MyPassword\r"
expect $prompt
# To avoid sending 'Enter' key on huge configurations
send "show ip int br\r"
expect $prompt
expect {
-ex "--More--" { send -- " "; exp_continue }
"*>" { send "exit\r" }
}
set timeout 1; # Reverting to default timeout
# Sending 'exit' at global level prompt will close the connection
expect eof
}
Thats workable. But following is the error message i got, how can i fix this? thanks
switch-hostname>Switch Interface Status
spawn telnet
usage: telnet [-l user] [-a] host-name [port]
send: spawn id exp9 not open
while executing
"send "MyUsername\r""
("foreach" body line 5)
invoked from within
"foreach ip [split $file_data "\n"] {
puts "Switch $ip Interface Status"
spawn telnet $ip
expect "Username:"
send "MyUsername\r"
expec..."
(file "./autotelnet.sh" line 8)
You can use a file which inputs the switch IP information in a line by line manner. Using terminal length 0 will save our work & finally log_file comes in handy to save the output.
#!/usr/bin/expect -f
#Slurp up the input file
set fp [open "input.txt" r]
# To avoid empty lines, 'nonewline' flag is used
set file_data [read -nonewline $fp]
close $fp
set prompt "#"
log_file -noappend switch_port_status.txt
foreach ip [split $file_data "\n"] {
puts "Switch $ip Interface Status"
spawn telnet $ip
expect "Username:"
send "MyUsername\r"
expect "assword:"
send "MyPassword\r"
expect $prompt
# To avoid sending 'Enter' key on huge configurations
send "terminal length 0\r"
expect $prompt
set timeout 120;# Increasing timeout to 2mins, as it may take more time to get the prompt
send "show ip int br\r"
expect $prompt
set timeout 10; # Reverting to default timeout
# Sending 'exit' at global level prompt will close the connection
send "exit\r"
expect eof
}
Note : The close of connection is handled on the basis of sending exit command only. So, make sure, the exit command always sent at global level of prompt. Instead of depending on exit, we can also use typical way of closing the telnet connection i.e. 'Ctrl+]' key combination.
#This will send 'Ctrl+]' to close the telnet connection gracefully
send "\x1d"
expect "telnet>"
send "quit\r"
expect "Connection"
If suppose you want to get multiple commands output, then keep them in a file and send them one by one.

Sending commands via SSH with expect script

In a another question I was kindly provided with a solution to a dilemma I was having with expect and if/else statements (basically, my lack of knowledge in how they're written). The script I was given works with one exception: If I cleanly connect to a remote host the exit command is not sent which will close that connection and move onto the next host in the list.
This is the functional part of the script:
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh $host
expect {
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
exp_continue
}
"New password" {
send -- $pw1\r
exp_continue
}
"Retype new password" {
send -- $pw1\r
exp_continue
}
msnyder
}
send "exit\r"
}
}
interact
}
When I'm connected to the host I expect to see the standard Linux prompt: [username#host ~]$. At least, that's our standard prompt. The prompt is preceded by a banner. Might that be throwing a kink into this? I wouldn't think so since I'm only looking for "continue connecting" from the RSA fingerprint prompt and that is within text spread across multiple lines. I would think that expect is intelligent enough to ignore any scrolling text and only look at text in prompts that are looking for input.
I considered passing the exit command as an argument for the ssh $host command, but expect doesn't seem to like that.
Can someone now assist me in getting the exit command to be sent properly?
UPDATE: I've added the exp_internal 1 option to the script to see what it is doing and it seems to only ever be matching "continue connecting" even at the user prompt once an SSH connection is established. It doesn't appear to be executing the next comparison for "msnyder". http://pastebin.com/kEGH3yUY
UPDATE2: I'm making progress. Based on Glenn Jackman's script below, I was able to get this working until the password prompt appears:
set prompt {\$ $}
set file1 [open [lindex $argv 0] r]
set pw1 [exec cat /home/msnyder/bin/.pw1.txt]
set pw2 [exec cat /home/msnyder/bin/.pw2.txt]
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
exp_continue
}
"New password" {
send -- $pw1\r
exp_continue
}
"Retype new password" {
send -- $pw1\r
exp_continue
}
-re $prompt {
send -- exit\r
expect eof
}
}
} -re $prompt {
send -- exit\r
expect eof
}
}
}
I have explicitly told it to send the exit command twice rather than trying the short-circuit method that Glenn used (it didn't seem to work per my comment under his answer).
What is now happening, is that it will loop through the list of hosts, send 'yes' to the RSA fingerprint prompt and then send 'exit' to the command prompt. However, when it hits a server that needs to have the password set it stops. It seems to eventually time out and then restarts with the next host. The current password is not being sent.
# a regular expression that matches a dollar sign followed by a space
# anchored at the end of the string
set prompt {\$ $}
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh $host
expect {
"continue connecting" {
send "yes\r"
expect {
# ...snipped...
-re $prompt
}
}
-re $prompt
}
send -- exit\r
expect eof
}
We've moved the "exit" to a place where it will be executed if you match either "comtinue connecting" or if you log in directly to your prompt.
Nice answer but I hate seeing code cut and pasted. One comment and a bit of refactoring: When you hit the "current" block, you change the password but never exit. It occurs to be that each scenario should end with you seeing the prompt and exiting:
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
"continue connecting" {
send "yes\r"
exp_continue
}
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
exp_continue
}
-re $prompt
}
send -- exit\r
expect eof
}
If you see "continue connecting", you send yes and then wait to see "current" or the prompt.
If you see "current" you change the password and then wait to see the prompt.
If you see the prompt, the expect comment ends and the next thing you do is exit.
I've sorted out the answer. After looking at Glenn's script below it occurred to me that each block is a separate if/else when instead I need the password section to act as a single block and the exit to act as the else. This is what I now have:
set prompt {\$ $}
set file1 [open [lindex $argv 0] r]
set pw1 [exec cat /home/msnyder/bin/.pw1.txt]
set pw2 [exec cat /home/msnyder/bin/.pw2.txt]
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
-re $prompt {
send -- exit\r
expect eof
}
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
}
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
}
-re $prompt {
send -- exit\r
expect eof
}
}
}
}
}
It addresses four situations:
I do NOT have to accept the RSA fingerprint OR change my password.
I do NOT have to accept the RSA fingerprint but DO have to change my password.
I DO have to accept the RSA fingerprint AND change my password.
I DO have to accept the RSA fingerprint but NOT change my password.