我将主机名列表存储在名为 ssh_hosts.txt 的文件中。在 shell 脚本中,我循环遍历 ssh_hosts.txt 中的主机名,并通过 SSH 在指定主机上执行命令。shell 脚本如下所示。
问题是脚本在处理完第一个主机后就退出了。但是如果我除了通过 ssh 在指定主机上执行命令之外还做了其他事情,脚本就会运行完成。
在下面的例子中,我注释掉了对 ssh 的调用,并将其替换为当前主机名的简单回显。这将运行至完成。
我正在从在 Windows 7 上以下 Cygwin 版本下运行的 bash shell 执行此脚本:
$ uname -a
CYGWIN_NT-6.1 myHostname 1.7.16(0.262/5/3) 2012-07-20 22:55 i686 Cygwin
涉及以下 SSH 版本:
$ ssh -V
OpenSSH_6.0p1, OpenSSL 1.0.1c 10 May 2012
$ ssh myUsername@remoteHost 'ssh -V'
myUsername@remoteHost password:
OpenSSH_6.7p1 Debian-5+deb8u3, OpenSSL 1.0.1t 3 May 2016
以下是 shell 脚本:
#!/bin/bash
if [[ $# -ne 1 ]]; then
echo "Usage: $(basename $0) <user name>"
exit 1
fi
USER="$1"
while IFS='' read -r ssh_host || [[ -n "$ssh_host" ]];
do
# This line will execute for all hosts listed in ssh_hosts.txt.
echo $ssh_host
# This line will execute for *only the first* host in ssh_hosts.txt.
# ssh $USER@$ssh_host 'echo $(whoami)@$(hostname)'
done < ssh_hosts.txt
我怎样才能让这个 shell 脚本在 ssh_hosts.txt 中的所有主机上执行,而不仅仅是在第一个主机上执行?
答案1
我认为你把它弄得太复杂了:
for ssh_host in $(cat ssh_hosts.txt)
do
echo $ssh_host
ssh $user@$ssh_host ....
done
另外,在 github 上搜索gsh
,这是一个 perl 程序,如果你经常这样做,它会做更多的事情。
答案2
您的循环中有两个程序都从 stdin 读取:read 和 ssh。当您的循环运行时,read 命令将从 ssh_hosts.txt 读取第一行,然后 ssh 读取其余部分。这就是为什么您只看到它连接到第一个主机。
我知道有两种方法可以解决这个问题。一种是重写循环,使其不从 stdin 重定向。一种好方法是使用for
eggo 在他的帖子中提供的循环。
另一个解决方案是将-n
选项传递给ssh
:
ssh -n $USER@$ssh_host 'echo $(whoami)@$(hostname)'
这告诉 ssh 不要使用 stdin,将 ssh_hosts.txt 的全部内容留给 read 命令。此解决方案要求您的 ssh 会话不需要与用户进行任何交互,包括提示输入密码(因此使用 ssh 密钥进行身份验证)。
答案3
您需要对其进行分叉,因为 ssh 会弄乱您的循环。
我手头有一个用于更新授权密钥的脚本,我使用两个脚本完成了此操作:
run.sh
:读取你的主机列表并分叉do_command.sh
脚本do_command.sh
:与控制套接字建立连接,执行命令并关闭
额外奖励:
- 所有主机同时连接
- 一个连接执行多个命令
- 包含结果的日志文件
主要脚本:run.sh
#!/bin/bash
TIMEOUT=60
COUNT=0
rm -rf log
mkdir log
while read -u10 HOST ; do ( ./do_command.sh $HOST & ); done 10< ssh_hosts.txt
sleep 1
for f in log/*.pid; do
[ -e "$f" ] && VMN=`ls log/*.pid`
echo "waiting for process to finish"
break
done
while [ "$VMN" != "" ]
do
sleep 3
COUNT=$(($COUNT+3))
if (("$COUNT" != "$TIMEOUT"))
then
VMN=""
for f in log/*.pid; do
[ -e "$f" ] && VMN=`ls log/*.pid`
break
done
if [ "$VMN" != "" ]
then
echo "waiting already "$COUNT" seconds ..."
#for VM in $VMN; do
#myPID=$(<"$VM")
#echo -e "\t pid:$myPID \t $VM "
#done
fi
else
echo -e "\n!!! timeout, waiting for remaining processes's to shutdown"
for VM in $VMN; do
myPID=$(<"$VM")
ITEM=$(sed -e 's/.pid//;s/log\///' <<<"$VM")
LOGX="log/$ITEM.log"
echo -e "killing $myPID -> item: $ITEM \t log: $LOGX"
kill -9 $myPID
rm -rf $VM
echo -e "\n ###### main Process ###### \n\n \t Killed due to Time out \nFAILED $ITEM" >>$LOGX
echo "$ITEM Killed due to Time out" >>log/error.txt
done
rm -fv log/*.sock
VMN=''
fi
done
if [ -f log/error.txt ]
then
rm -fv log/*.sock
echo -e "\n##### FAIL ##############\n"
cat log/error.txt
echo
exit 1
fi
echo
动作脚本:do_command.sh
#!/bin/bash
exec 5<&1
exec 6<&2
echo "PROCESS $1"
myPID=$$
echo $$ >"log/$1.pid"
chmod 777 "log/$1.pid"
exec 1> "log/$1.log" 2>&1
echo "PROCESS $1"
SOCKET="log/$1_$myPID.sock"
TARGET="root@$1"
echo "Connecting to $TARGET .... "
ssh -f -N -M -o ControlPath=$SOCKET $TARGET
if [ $? -ne 0 ]; then
echo "Could not connect $1, Cleanup "
echo "Could not connect $1, exit " >>log/error.txt
#ssh -S $SOCKET -O exit $TARGET
rm -fv "log/$1.pid"
rm -fv $SOCKET
echo "FAILED $TARGET">&6
echo "FAILED $TARGET"
exit 1
fi
# Do the thing you need to do
# This script copy a file to the remote and executes 2 commands
#
scp -o ControlPath=$SOCKET authorized_keys $TARGET:.ssh/new_authorized_keys
# Execute a command:
ssh -o ControlPath=$SOCKET $TARGET "mv -fv .ssh/authorized_keys .ssh/old_authorized_keys && cp -fv .ssh/new_authorized_keys .ssh/authorized_keys && chmod 600 .ssh/authorized_keys"
# Another remote command:
ssh -o ControlPath=$SOCKET $TARGET "ls -la .ssh"
# And Close the socket
ssh -S $SOCKET -O exit $TARGET
sleep 1
echo Cleanup
rm -fv "log/$1.pid"
rm -fv $SOCKET
echo "SUCCESS $TARGET"
echo "SUCCESS $TARGET">&5
exit 0