TWSNMPのSSHポーリング機能(後編)
SSHポーリング機能について書いた前回
の続きです。昨夜は、雨が激しく降っていました。猫は夜中に何度も、かみさんを起こしていました。
サンプルプログラムの落とし穴
GO言語のSSHパッケージのドキュメントに記載されたサンプルプログラム
を見ると簡単にSSHでリモートのサーバー上でコマンドを実行するプログラムを作れそうです。このサンプルプログラムの要点は、
// SSHクライアントの設定を作成する
config := &ssh.ClientConfig{
User: "ユーザー名",
Auth: []ssh.AuthMethod{
ssh.Password("パスワード"),
},
HostKeyCallback: ssh.FixedHostKey(サーバーの公開鍵),
}
// 作成した設定でサーバーに接続する
client, err := ssh.Dial("tcp", "接続したいサーバー:22", config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
// クライアントがサーバーに接続できたらセッションを作成する
session, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer session.Close()
// セッションが作成できたらコマンドを実行する
var b bytes.Buffer
session.Stdout = &b
if err := session.Run("実行したいコマンド"); err != nil {
log.Fatal("Failed to run: " + err.Error())
}
fmt.Println(b.String())
という感じです。ますます簡単そうです。コマンド実行ポーリングの処理のコマンドを実行する部分をこの処理に置き換えれば完成と安易に考えるわけにはいきません。この方法には、三つほど心配なことがあります。
・サーバーにいつも接続できるとは限らない。
・実行したコマンドはすぐに終了するとは限らない。
・コマンドの実行結果を調べる方法が分からない。
長年、プログラマーをやっていると心配ばかりしている気がします。
コマンド実行に時間制限を付ける
最初の二つの心配ごとは、コマンド実行に時間制限を付ければ解決できます。以前書いた
の中で使っている
のようなパッケージがSSH用にあれば簡単に解決できますが、残念なことによいものが見つかりませんでした。なので、自力で解決することにしました。
サーバーに接続する時の時間制限(タイムアウト)は、SSHのクライアントの設定に項目(パラメータ)があったので簡単に解決できました。
のずばりTimeoutです。最初に見つけた時は、2つ目の心配ごとコマンドの実行時間も制限できるのかもしれないと思ったのですが、説明をよく読むと
Timeout is the maximum amount of time for the TCP connection to establish.
と書いてあったのでちょっとがっかりしました。
でも、
sshConfig := &ssh.ClientConfig{
User: n.User,
Auth: []ssh.AuthMethod{},
Timeout: time.Duration(p.Timeout) * time.Second,
}
のように修正して心配事を一つ解決できました。
二つ目のコマンドの実行時間を制限することについては、そのまま問題をストレートに考えるとあまり良い解決策が見つかりません。
session.Run("実行したいコマンド")
の部分を時間が来たら強制終了する方法などいろいろ考えましたが、どれも作ったプログラムが美しくないのです。美しくないプログラムは何か根本的なところで間違っている場合が多いという長年の感が働いて考え直しました。
この問題を解決するには、ちょっと発想の転換が必要でした。SSHのコマンドは、通信(TCP)を通して実行されます。コマンドが長い時間終わらない場合は、無通信の状態が続くはずです。ならば、無通信の時間に制限を付ければ結果的にコマンドの実行時間を制限できます。
でも、sshのパッケージの
client, err := ssh.Dial("tcp", "接続したいサーバー:22", config)
ではできません。この処理は分解する必要があります。
・まずはTCPのコネクションを作成する。
・次にTCPのコネクションの時間制限(タイムアウト)をつける。
・このTCPのコネクションを使ってSSHのクライアントを作成する。
幸いにも分解するために必要なTCPコネクションからSSHクライアントを作る関数がSSHのパッケージに用意されていたので、処理は、
// TCPのコネクションを作る
conn, err := net.DialTimeout("tcp", n.IP+":"+port, time.Duration(p.Timeout)*time.Second)
if err != nil {
return nil, nil, err
}
// TCPのコネクションに時間制限を付ける
conn.SetDeadline(time.Now().Add(time.Second * time.Duration(p.PollInt-5)))
// TCPコネクションからSSHのクライアントを作る
c, ch, req, err := ssh.NewClientConn(conn, n.IP+":"+port, sshConfig)
のようにしました。
コマンドの終了コードを取得する
コマンドを実行する
if err := session.Run("実行したいコマンド"); err != nil {
log.Fatal("Failed to run: " + err.Error())
}
で結果は、errで判断できます。コマンドの終了コードが示すエラーも含まれていますが、それ以外のコマンド実行時のエラーも含まれています。このerrからコマンドの終了コードを取り出す工夫が必要でした。
この問題は、errが
のExitErrorだった場合、その中にある
のWaitMsgが取り出すことができました。
err := session.Run("実行したいコマンド")
if err != nil {
if e, ok := err.(*ssh.ExitError); ok {
exitCode = e.Waitmsg.ExitStatus()
のような感じです。
開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。