sudo をプログラムで使う際の注意点

Wake on LAN してみる Part 3 (shutdown)』 で Linux サーバを停止する際の sudo の使い方に問題があったので まとめておくことに...

問題1

sudo のバージョンによって標準入力からパスワードを渡す方法が異なるらしい。

Ubuntu 8.04 LTS の sudo (1.6.9p10) では

echo "pass" | ssh hoge@192.168.1.1 "sudo /sbin/shutdown -h now"
で問題がなかったが 最新の Ubuntu 10.04 LTS の sudo (1.7.2p1) では
sudo: no tty present and no askpass program specified
のようなエラーになってしまう。どうやら
echo "pass" | ssh hoge@192.168.1.1 "sudo -S /sbin/shutdown -h now"
のように "-S" オプションをつけて実行しないとダメらしい。

問題2

sudo に "-S" オプションをつけて実行すると プログラムから標準入力経由でパスワードを渡しても

[sudo] password for hoge: Sorry, try again.
[sudo] password for hoge: Sorry, try again.
[sudo] password for hoge: Sorry, try again.
sudo: 3 incorrect password attempts
のように認証エラーになる。

どうやらパスワードの最後に 改行文字 "\n" をつけて

channel.setInputStream(new ByteArrayInputStream("{password}\n".getBytes()));
としないとダメらしい。考えてみれば、当然だけど...
改行コードが無いと どこまでがパスワードなのか分からないからね。

無理して InputStream でパスワードを渡すより、ssh で実行するコマンド自体を

"echo {password} | sudo -S -p '' /sbin/shutdown -h now";
としてしまった方が簡単で良いかもしれない。
echo なら何も考えなくても改行コードつけてくれるから...

問題3

sudo を実行すると

[sudo] password for hoge: 
のように パスワード要求プロンプトが標準エラーに出力されてしまう。

パスワードを要求するプロンプトは "-p" オプションで指定できるので

sudo -p '' ...
のように空文字を指定すれば プロンプトの出力を消すことができます。

これらを踏まえると 最終的なコードは

import java.io.ByteArrayInputStream;
import java.lang.*;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;

// サーバ情報
def host = "192.168.1.1";
def port = 22;

// ユーザ情報
def username = "hoge";
def password = "pass";

// shutdown コマンド
def command = "sudo -S -p '' /sbin/shutdown -h now";

def jsch = new JSch();
def session = jsch.getSession(username, host, port);

try {

    // SchException: UnknownHostKey を防ぐための設定
    session.setConfig("StrictHostKeyChecking", "no");
    session.setPassword(password);
    session.connect(5000);

    var channel = session.openChannel("exec") as ChannelExec;

    try {
        channel.setCommand(command);

        channel.setInputStream(new ByteArrayInputStream("{password}\n".getBytes()));
        channel.setOutputStream(System.out);
        channel.setErrStream(System.err);
        channel.connect();

        // コマンドが終了するまで待機
        while (not channel.isClosed()) {
            Thread.sleep(1000);
        }

    } finally {
        channel.disconnect();
    }

} finally {
    session.disconnect();
}
のようになる。

もちろん 『Wake on LAN してみる Part 3 (shutdown)』 についても上記の内容でこっそり修正してあります。