シェルスクリプトからSSHログインして別端末で作業させるときの異常系に関するメモ

# とりあえずまとめ
* SSHログイン先では実行スクリプトと停止スクリプトを対にしておくこと
* trapによる処理を書くこと
* watch_dogのスクリプトを用意してタイムアウトさせること

# [基本] シェルスクリプトからSSHでコマンド実行
まずは基本として，SSHでコマンドを送り込むためのメモ．ちなみに環境はUbuntuでやってます．
CentOSの場合はパーミッションで蹴られることが多いので注意してください．

## SSH設定
パスフレーズ無しの鍵を作ってリモートへログインできるようにする．まずはログイン元の端末で下記のコマンドを実行して秘密鍵・公開鍵のペアを作成．

```console
$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (...): [ファイル名を指定する]
Enter passphrase (empty for no passphrase): [何も入れずにエンター]
Enter same passphrase again: [何も入れずにエンター]
Your identification has been saved in [ファイル名].
your public key has been saved in [ファイル名].pub.
The key fingerprint is:
[フィンガープリントが表示される]
```

ファイル名にhogeを指定した場合，秘密鍵のhogeと公開鍵のhoge.pubが生成される．hogeは厳重に管理．hoge.pubはまぁ適当に扱っていい．
デフォルトはid_rsaになっているが，オススメしない．デフォルトで使われる上に他の端末で作った鍵と混じったりしたときに訳が分からなくなる．どうせ鍵ファイル名はほとんど使わないので長い名前にしておく方が良い．

続いてconfigファイルの編集．.ssh/configファイルに下記を追記する．

```sh
Host [remote_name]
    User [user_name]
    Port [22]
    Hostname [hostname]
    IdentityFile [path/to/keyfile]
    IdentitiesOnly yes
    [ ProxyCommand ssh ssh_server_name -W %h:%p ]
```

[remote_name]は適当に使いやすい名前を指定．keyfileには秘密鍵を指定．.ssh/keyとかの下に置いてディレクトリごと700にしておくのが良い．最後のProxyCommandは踏み台サーバが必要なときの設定．直接SSHログインできるなら何も書かなくて良い．

最後にログイン先の端末での作業．

```console
$ cat hoge.pub >> .ssh/authorized_keys
```

基本的には公開鍵の中身をauthorized_keysに追記すればよい．リダイレクトを使ってるが，別にコピペでもいいし見ながら頑張ってキーボードを叩いても良い．
とにかく公開鍵の中身のテキストがauthorized_keysに追記されていれば良い．

ログイン元の端末から

```sh
$ ssh [remote_name]
```

でパスワードを入れずにログインできれば成功．ログイン出来なかったりパスワードを求められたりした場合は何かが間違えてる．
成功したら，ログイン先では/etc/ssh/sshd_configを編集してパスワード認証を切っておく．
**成功したかどうか確認せずにパスワード認証を切ってしまうとログイン出来なくなる場合があるので注意！**

```sh:/etc/ssh/sshd_config
# Change to no to disable tunnelled clear text passwords
PasswordAuthentication no
```

後は`sudo service sshd restart`とかでSSHデーモンをリスタートすればOK.

## SSHを使ったコマンド投入
sshはコマンドを後ろにくっつければそれを実行してくれる．

```sh:
$ ssh remote_name ls
Document
Download
Picture
...
```

例えばtcpdumpみたいなsudo権限が必要なコマンドを実行する場合，sudoするとパスワードを求められてしまう．秘密鍵を使ってrootログインできるようにするか，/etc/sudoersに下記の一行を追加する．

```sh:/etc/sudoers
[user_name]    ALL=(ALL) NOPASSWD: ALL
```

ちなみにrootのパスワードを設定せずにsudoで全部やってる人がsudoersファイルを編集する場合，編集を間違えるとsudoできなくなる場合があるので，**別の端末でsudo su等でルートになっておいてから編集すること**．

# [本番] シェルスクリプトからSSHでコマンド投入する場合
ようやく本番．下記のようなスクリプトを作る．control.shがremoteマシンのremote.shを呼び出す．

```sh:control.sh
#!/bin/sh

echo "Start"
ssh remote ~/remote.sh
echo "Done"
```

```sh:remote.sh
#!/bin/sh

echo "Remote Start"
sleep 10
echo "Remote Done"
```

remoteでは単に10秒待つだけのプログラム．まず，この時点でcontrol.shに実は誤りがある．
シェルスクリプト中で指定した`~/`は**ローカルのホームディレクトリを指し示すため，ユーザ名が異なる端末にログインする場合はフルパスで指定する必要がある**．全部同じユーザにしておくとその辺を気にしなくて良いが，例えばrootログインさせる場合などは注意が必要．

さて，実行すると下記のようになる．実行権限を付けるのを忘れないように．

```sh:
$ ./control.sh
Start
Remote Start
[ 10秒待機 ]
Remote Done
Done
$ 
```

正常系ならこのように問題無く動作する．

## control-Cで止めてみる
実行したが何か様子がおかしいので止めよう，というときにほぼ大半の人はcontrol-Cを押すと思う．このプログラムでもやってみる．

```sh:
$ ./control.sh
Start
Remote Start
[ control-Cを押下 ]
^C
$ 
```

一見止まったように見える．しかし，remote側で`ps ax`をしてみると

```sh:
...
23069 ?    Ss   0:00 /bin/sh /home/user/remote.sh
...
```

といった形でしっかり動いている．
control-Cの押下で行われるのはkillコマンドでSIGINT(2)を送ったことと同様になるのだが，**SSHログイン先で実行中のコマンドに対して送られるわけではなく，手元で動いているシェルスクリプトに対して送られる**．当たり前といえば当たり前なんだが，SSHで作業中にcontrol-Cを押してもSSHセッションが切れるわけじゃないので誤解しやすいと思う．

## 停止用スクリプトの作成
こうした異常系のことを考えてSSHログイン先のスクリプトを作成した場合，**必ず対となる停止用スクリプトを作成する**．
例えばremote.shを止めるには下記のようなスクリプトを用意する．

```sh:stop.sh
#!/bin/sh

ps ax | grep [r]emote.sh | awk '{print $1}' | xargs kill
```

「これだとremote.shを編集中のviとかまで死んじゃうじゃん」とか「複数動いてたらどうすんの」とかいろいろ突っ込みはあると思うので，適宜環境に合わせて作り替えてください．
たぶん一番正しいお作法はremote.shが起動直後に自身のPIDを`$$`から取得してそれを呼び出し元に教えたりして`kill`する方法だと思う．

control.shは下記のようにしてシグナルを取得，スクリプトを停止する．

```sh:control.sh
#!/bin/sh

echo "Start"
trap "ssh remote ~/stop.sh" 2 15
ssh remote ~/remote.sh
echo "Done"
```

trapコマンドはシグナルを受けたときのコマンド，シグナルの種類を指定する．2はSIGINTで，ついでにkillコマンドが実行されたときのSIGTERM(15)も追加しておく．
これでcontrol-Cによる停止やkillコマンドによる停止をしたときにSSH先のコマンドがちゃんと止まるようになる．

# タイムアウトさせる
スクリプトが完成してとにかく3日ほどいろんな項目を勝手に試させる，みたいなことをするとよくあるのが途中で勝手に止まって残りの項目を消化できていない場合だ．
そういった場合に備えて下記のようなwatch_dog機能を用意しておく．

```sh:watch_dog.sh
#!/bin/sh

if [ -z "$2" ]
then
    echo "Usage: $0 time pid"
    exit 1
fi

sleep $1 &
SPID=$!
trap "kill ${SPID} && exit" 15
wait

trap 15
kill -10 $2
```

`./watch_dog.sh 10 12345`といった形で実行すると，10秒後にPID=12345のプロセスに対してシグナル10を送る．止める場合は普通に`kill`するだけでよい．
さて，このwatch_dogスクリプトをcontrol.shで使うには以下のようにする．

```sh:control.sh
#!/bin/sh

echo "Start"
./watch_dog.sh 3 $$ &
WD_PID=$!

trap "ssh remote ~/stop.sh && exit 1" 10
trap "kill ${WD_PID} && ssh remote ~/stop.sh" 2 15

ssh remote ~/remote.sh &
wait $! && kill ${WD_PID}
echo "Done"
```

sshのコマンド発行を&でバックグラウンドに回してwaitで待機しているが，このようにしないと正常に動作しない．（割り込み処理にならないから？）
また，正常終了時やcontrol-Cによる処理のときはちゃんとwatch_dogを止める必要があるため，ちょっと面倒くさい実装になる．

# おわり
SSHの先でSSHして他の端末に指示するような場合はそれぞれで停止スクリプトなりタイムアウトなどを発火させる必要がある．とにかく大事なのは**動かすスクリプトと止めるスクリプトを対で用意し，異常時は止めるスクリプトを確実に動作させること**．例えば測定や試験の自動化を進めるときにこれをちゃんと書いておかないと異常時に爆発的にスレッドが増えてサーバがハングアップしかねない．何かあったら意地でも止めるという気持ちが大事．多分．