以下のコマンドはサーバ($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 cmdexpect -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