Using GNU Screen completely transparently and automatically - scripting

Screen is amazing, of course, but I don't want to have to think about it. I often ssh to a machine, start doing a bunch of stuff, and then think "gosh, I wish I had thought to start a screen session before doing all that so I could reconnect to this from home later".
I'd like to have screen automatically started whenever I log in to a machine.
And when I get disconnected, I want to be able to immediately and simply reconnect without fussing with "screen -ls" and "screen -dr".
I have a script that implements one solution to this problem which I'll post as an answer. I'm interested to see other approaches.

Use the following, ssc, instead of ssh. If you just do "ssc remote.com" then it will list existing screen sessions. Give it a 3rd argument and it will connect to that screen session, or create it and connect to it. Either way, if you get disconnected you can just do "up-arrow, enter" in the shell to reconnect. Zero knowledge of screen required!
Edit: Thanks to #klochner for extending this to handle arbitrary ssh options. You can now use this just like ssh!
#!/usr/bin/env perl
# Use 'ssc' (this script) instead of 'ssh' to log into a remote machine.
# Without an argument after the hostname it will list available screens.
# Add an argument after the hostname to attach to an existing screen, or
# specify a new screen. Eg, ssc remote.com foo
# The numbers in front of the screen tag can usually be ignored.
# ssh option parsing by #klochner
my $optstring = "";
while ($val = shift) {
if ($val =~ /^-\w$/) { $optstring .= " ".$val.(shift); }
elsif ($val =~ /^-\w+$/) { $optstring .= " ".$val; }
elsif ($machine) { $tag = $val; }
else { $machine = $val; }
}
if (!$machine) {
print "USAGE: ssc [ssh options] remote.com [screen name]\n";
} elsif (!$tag) {
#screens = split("\n", `ssh $optstring $machine screen -ls`);
for(#screens) {
if(/^\s*(\d+)\.(\S+)\s+\(([^\)]*)\)/) {
($num, $tag, $status) = ($1, $2, $3);
if($status =~ /attached/i) { $att{"$num.$tag"} = 1; }
elsif($status =~ /detached/i) { $att{"$num.$tag"} = 0; }
else { print "Couldn't parse this: $_\n"; }
# remember anything weird about the screen, like shared screens
if($status =~ /^(attached|detached)$/i) {
$special{"$num.$tag"} = "";
} else {
$special{"$num.$tag"} = "[$status]";
}
}
}
print "ATTACHED:\n";
for(sort { ($a=~/\.(\w+)/)[0] cmp ($b=~/\.(\w+)/)[0] } keys(%att)) {
($tag) = /\.(\w+)/;
print " $tag\t($_)\t$special{$_}\n" if $att{$_};
}
print "DETACHED:\n";
for(sort { ($a=~/\.(\w+)/)[0] cmp ($b=~/\.(\w+)/)[0] } keys(%att)) {
($tag) = /\.(\w+)/;
print " $tag\t($_)\t$special{$_}\n" unless $att{$_};
}
} else {
system("ssh $optstring -t $machine \"screen -S $tag -dr || screen -S $tag\"");
}
Btw, there's a trick to forcing an ssh session to exit and give you back your local terminal prompt when you lose network connectivity:
https://superuser.com/questions/147873/ssh-sessions-in-xterms-freeze-for-many-minutes-whenever-they-disconnect

there is autossh which automatically reconnects disconnected ssh-sessions.
It comes with an example script called rscreen which does exactly that. It is, simply:
#!/bin/sh
autossh -M 0 -t $1 "screen -e^Aa -D -R"
Then you have to retrain your fingers to type rscreen hostname instead of ssh hostname

ssh user#host.com -t 'screen -dRR'
This will reload/create your screen session on connect. This does exactly what was requested, even if it moves the responsibility for spawning the session to the initiating client. Ideally you would want some process on the server managing what gets presented to connecting clients. As far as I know that doesn't exist. No one has suggested the ideal solution in this thread. For me this is less "not ideal" than the rest. No scripts, no bugs, no TTY issues, no bad interactions with other ssh commands, no potential for infinite loops, no file editing, no additional packages required.

Actually screen sets the TERM variable to 'screen'. So the script is even easier. Here is what I use:
if [ "$TERM" != "screen" ]; then
screen -xRR
fi
Works like a charm, the -x ensures that even if the screen is attached somewhere else I attach to it here. This way I only every have one screen where I can keep track of everything.

I have the following in my .bashrc
if [ "$PS1" != "" -a "${_STARTED_SCREEN:-x}" = x -a "${SSH_TTY:-x}" ]
then
export _STARTED_SCREEN=1;
sleep 1
screen -RR && exit 0
# normally, execution of this rc script ends here...
echo "Screen failed! continuing with normal bash startup"
fi
I found it online somewhere awhile ago, not sure where.
Update Fixed error that was pointed out in comments. Thanks R. Pate

i have used autossh, it is very useful to me

Maybe put exec screen -dr in your .login?

I use mosh (mobile shell). It keeps your connection on even if you go to sleep mode, disconnect from the network, change IP, and so on. Whenever you return, you get your connection back.

I often connect to a remote machine from multiple terminal tabs. This in my zshrc checks if there's any detached session, and if there's one, connects to it. Else, it'll create a new screen. This allows me to resume work easily after an facing a network interruption easily even if I had 3 ssh sessions open. I just have to reopen 3 ssh tabs or reconnect on the previous 3 tabs and they'll resume as if nothing happened.
First if checks if already inside another screen or tmux window and to make sure that the shell is in interactive mode.
if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ] && [[ ! "$TERM" =~ screen.xterm-256color ]]; then
SCLS=$(screen -ls | grep Detached)
if [ ${#SCLS} -gt 0 ]
then
echo -n $SCLS | wc -l
SID=$( echo $SCLS | head -n1 | cut -d. -f1 | awk '{print $1}' )
screen -r $SID
else
screen zsh -c "echo -e Spawned new screen '\n' && screen -ls && echo && zsh"
fi
fi
suggestions for improving are welcome.

Depends on your shell, but what about .bashrc? (If you use bash "screen -rd")

Related

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

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

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 }
}

Error On Expect Script

I keep getting an error on this after sending the new password. It will change the password, but not do the rest of the script. Any ideas why it keeps erroring out. I wrote a very similar script for a different device and it works prefect, but the stuff after changing the password is not needed on that device. This device will not save the password after reboot if the rest is not completed. Doing it manually through ssh works just fine so its not the cmds that is the problem, it's something with the script.
#!/usr/bin/expect
set timeout -1
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect "assword:"
send "$old\r"
sleep 10
send "passwd $user\r"
expect "assword:"
send "$new\r"
expect "assword:"
send "$new\r"
sleep 10
send "grep -v users.1.password= /tmp/system.cfg > /tmp/system.cfg.new\r"
sleep 10
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
sleep 10
send "cp /tmp/system.cfg.new /tmp/system.cfg\r"
sleep 10
send "save && reboot\r"
close
expect eof
}
The full script is alot bigger with with fail-safes if the device does not respond to ssh or the original password is wrong. That one won't work though until I figure out what is wrong with this portion. I just slimmed it down to figure out where the problem is happening. Also this line seems to be the issue as it does create the system.config.new on the line before it:
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
It was and works in ssh as this:
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print $2}'` >> /tmp/system.cfg.new\r"
But sends an error because of the $2 then is view-able by expect. I was told that putting a \$2 would make it only view-able to the remote shell. Any help would be great.
Originally it had expect "star" instead of the sleep cmds. I have been trying tons of stuff out on this script and once I get it to run incorporate it in my full script. The reason I am using sleep is because "star" doesn't seem to match output and fails on the second send $new/r. With sleep it has made it alot farther. I do have a correction though. It is actually making it right up to send "save && reboot\r". I am going to eventually use your $prompt suggestion from my other question in place of sleep or expect "star". With debug on this is where it throws the error:
send: sending "save && reboot\r" to { exp4 }
expect: spawn id exp4 not open
while executing
"expect eof"
("foreach" body line 21)
invoked from within
"foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect "assword:"
send "$old\r"
sleep 3
send "passwd $user\r"
e..."
(file "./ubnt.sh" line 15)
The "save && reboot\r" will kick out the ssh connection after it saves the settings, but it doesn't seem to be getting that far. Is there an issue with &&, maybe I need to /&&.
Interact instead of close and expect eof is the answer I will update with full script when it's done. Here is the working UBNT password change script:
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "passwd $user\r"
expect "*assword:*"
send "$new\r"
expect "*assword:*"
send "$new\r"
expect "*"
send "grep -v users.1.password= /tmp/system.cfg > /tmp/system.cfg.new\r"
expect "*"
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
expect "*"
send "cp /tmp/system.cfg.new /tmp/system.cfg\r"
expect "*"
send "save && reboot\r"
interact
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
Here is the script for Mikrotik passwords:
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn -noecho ssh -q -o "StrictHostKeyChecking=no" $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "user set $user password=$new\r"
expect ">"
send "quit\r"
close
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
Dinesh mentioned that I should not use "star" in my expect commands and said that instead use:
set prompt "#|%|>|\\\$ $"
And:
expect -re $prompt
Which I will probably change. The scripts also move onto next IP in list if the device cannot be reached via ssh or if the oldpassword does not work for the device. The only other change I plan to make on both is to have it create 3 log files listing successful ips, wrong password ips, and cant reach ips. This should only be one more command before each continue, but haven't yet researched how to output a variable to a local log file.
Thanks for your help on this Dinesh.

Prevent Expect's variable substitution for a remote shell command

I need to run this cmd with an expect script:
echo users.1.password=`grep %user% /etc/passwd | awk -F: '{print $2}'` >> /tmp/system.cfg.new
But it errors out because of the $2 in it. How do I fix this? I need the variable to only be visible to the device I am sending the cmd to.
Here is the full script for password change on UBNT equipment via script (works via ssh, but not as script because of $2):
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "passwd $user\r"
expect "assword:"
send "$new\r"
expect "assword:"
send "$new\r"
expect "*"
send "grep -v users.1.password= /tmp/system.cfg > /tmp/system.cfg.new\r"
expect "*"
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print $2}'` >> /tmp/system.cfg.new\r"
expect "*"
send "cp /tmp/system.cfg.new /tmp/system.cfg\r"
expect "*"
send "save && reboot\r"
close
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
You just have to escape the dollar sign with backslash.
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
By the way, using expect * might be dangerous and not a recommended one unless a read need of it.
You could use the a generalized approach for the matching the prompt as,
set prompt "#|%|>|\\\$ $"
Here, we have escaped the literal dollar sign with backslash and the last dollar is meant for the end-of-line regular expression.
After sending any commands to the remote shell, you can expect for the pattern as
expect -re $prompt

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.