以下のコマンドはサーバ($host)をLBから外して、jettyを再起動したあと、再度LBに組み込むものである。変数$hostをfor loopに組み込んで全サーバのjetty再起動を以下のコマンドで自動実行したかった。
# サーバパスワードを入力
read -sp 'password:' pw
# LBからサーバを1台外すコマンド
# コマンドは略
# expectでサーバにパスワードを入力せずログインする。
# 踏み台サーバ(jump_host)にログインしたあと、sudo ssh $hostを実行して、作業を実行したいremoteサーバに入る。
# ※踏み台から$hostへのsshはパスワード無しで実行できるとする。
# netstatの結果をawkを使いつつ確認し、jettyへのESTABLISHEDな接続がなくなったか確認する。
# awkの部分には間違いが含まれているので、後半で訂正していく。
expect -c "
set timeout 120
spawn ssh -t usr@jump_host \"sh -c 'sudo ssh $host'\"
expect \"usr@jump_host's password:\"
send \"$pw\n\"
expect \"#\"
send \" while test 0 -ne \`netstat -antp | grep ESTABLISHED | awk '{print $4}' | grep -c :8080\`; do sleep 5; done\n \"
expect \"#\"
send \"exit\n\"
expect \"Connection to jump_host closed.\"
exit 0
"
# jettyを再起動する。
expect -c "
set timeout 120
spawn ssh -t usr@jump_host \"sh -c 'sudo ssh $host'\"
expect \"usr@jump_host's password:\"
send \"$pw\n\"
expect \"#\"
send \"service jetty restart\n\"
expect \"#\"
send \"exit\n\"
expect \"Connection to jump_host closed.\"
exit 0
"
# jettyの起動確認を行う。
expect -c "
set timeout 120
spawn ssh -t usr@jump_host \"sh -c 'sudo ssh $host'\"
expect \"usr@jump_host's password:\"
send \"$pw\n\"
expect \"#\"
send \" while test 0 -eq \`grep -c STARTED /home/usr/jetty/jetty.state\`; do sleep 1; done\n \"
expect \"#\"
send \" ps -ef | grep jett\[y\] | wc -l\n \"
expect \"#\"
send \"exit\n\"
expect \"Connection to jump_host closed.\"
exit 0
"
# LBへの再組み込みを行う。
# コマンドは略
処理の流れは各コマンドに記載のコメントの通りだが、expectコマンド内でawkを使う点がうまく書けなかった。
expect -c cmd
とexpectコマンドを実行したとき、ターミナル上で実行されるので、shellとしてのエスケープ等を考える必要がある。つまり、cmd部分では[]
や`
のエスケープが必要になる。また変数展開に独特のルールがある。$param
としたときはshell内で定義された変数として扱われ、\$param
としたときはexpect内で定義された変数として扱われる。
print $4
の箇所をエスケープしなければ、shell内で定義された変数として展開されるが、変数4は定義されていないので、リモートサーバ先で実行したときに以下のように空白になってしまう。
while test 0 -ne `netstat -antp | grep ESTABLISHED | awk '{print }' | grep -c :8080`; do sleep 5; done
$N
をエスケープすると、expect内で定義された変数とされるので、\$N
としても定義されていない変数としてエラーが発生する。
spawn ssh -t usr@jump_host sh -c 'sudo ssh $host'
usr@jump_host's password:
Last login: Mon May 8 21:00:42 2017 from jump_host
[root@remote1 ~]# can't read "4": no such variable
while executing
"send " while test 0 -ne `netstat -antp | grep ESTABLISHED | awk '{print $4}' | grep -c :8080`; do sleep 5; done\n ""
can't read "4"ということからexpectコマンドでは4という変数が存在しないと言われてしまっている。
expect -c cmd
で実行するほかにもexpectスクリプトファイルを作成して実行する方法がある。expectスクリプトファイルはshellと独立しているので、[]
や`
のエスケープは不要になる。同様に、コマンドの場合にあったエスケープによる変数展開のルールはなく、$param
でexpectスクリプトのset
により定義された変数を参照する。\$param
とエスケープした場合は、expectスクリプト内に置いて変数ではなく文字列$param
として解釈される。
今回のawkの例では$4
を変数として解釈したくないため、\$4
とすれば、正しく動く。
#!/usr/bin/expect -f
set pw [lindex $argv 0]
set host [lindex $argv 1]
set timeout 120
spawn ssh -t usr@jump_host "sh -c 'sudo ssh $host'"
expect "usr@jump_host's password:"
send "$pw\n"
expect "#"
send " while test 0 -ne `netstat -antp | grep ESTABLISHED | awk '{print \$4}' | grep -c :8080`; do sleep 5; done\n "
expect "#"
send "exit\n"
expect "Connection to jump_host closed."
exit 0
パスワードとリモートホスト名はスクリプト(check_netstat.exp)の引数として渡すことにして、以下のように呼び出す。
./check_netstat.exp $pw $host
netstatの確認部分だけ別スクリプトにするのは微妙なので、他のexpect部分も同様に別スクリプトにして呼び出すようにするのがいい。
書き捨てのコマンドとして実行したく、別スクリプトのようなファイルを作りたくなければ、ヒアドキュメントと/dev/stdinを利用して、次のようにも書ける。expect -c cmd
とexpect -f file
のいい所取り(書き捨てしやすい + エスケープ周りが扱いやすい)なので、意外と使いやすい。
cat <<'EOF' | expect -f /dev/stdin $pw $host
set pw [lindex $argv 0]
set host [lindex $argv 1]
set timeout 120
spawn ssh -t usr@jump_host "sh -c 'sudo ssh $host'"
expect "usr@jump_host's password:"
send "$pw\n"
expect "#"
send " while test 0 -ne `netstat -antp | grep ESTABLISHED | awk '{print \$4}' | grep -c :8080`; do sleep 5; done\n "
expect "#"
exit 0
EOF