Rubyでexpectを用いて複数マシンのssh鍵を収集

今日、あるプロラムを複数のマシン上で走らせるために、あらかじめ各マシンのssh鍵を収集しておく必要ができた。使用するマシンのssh鍵をほとんど持っていなかったため、マニュアルでするとなると毎回sshでログインしてyesって打たなきゃならない。そんなことはやっていられなかったので、Rubyのexpectライブラリを用いてyesって入力してくれるスクリプトを書いた。
ssh鍵を取得するためのスクリプトとして、今回は2つスクリプトを作成した。1つは汎用的で使いやすいexpect on Rubyなライブラリ。もう1つはそのライブラリを用いて複数マシンから連続的にssh鍵を収集するスクリプト
最初の expect on Rubyライブラリは次の2つのページを参考に実装した。
Young risk taker.: Rubyのexpect.rbの使い方
http://www.ksky.ne.jp/~sakae/d2/20518.html

# new_expect.rb
require 'pty'
require 'expect'

module Expect
  def spawn(cmd)
    puts "CMD: #{cmd}" if $expect_verbose
    PTY.spawn(cmd) { |r,w,pid|
      @input_stream = r
      @output_stream = w
      @child_pid = pid
      yield
    }
  end

  def expect(pat, timeout=5)
    ret = @input_stream.expect(pat, timeout) { |match|
      raise "expect %s timeout " %(pat.kind_of?(Regexp)? pat.source : pat) unless match
      @output_stream.puts(yield(match))
    }
  end
end

実装は最初のページで紹介されているサンプルとほとんど一緒。だけど、PTY.protect_signal関数は現在の実装では呼ばなくても良いとのことなのでそこは消した。
で、このライブラリを用いてssh鍵を集めるスクリプトを次のように実装した。

require "new_expect"

class SshKeyCollector
  include Expect

  def initialize(fname)
    @list_name = fname
  end

  def collect
    File::open(@list_name) { |file|
      file.each_line { |line|
        hostnameFQDN = line.strip
        collect_one hostnameFQDN

        hostname = hostnameFQDN.split(".")[0]
        collect_one hostname if hostname
      }
    }
  end

  private
  def collect_one(hostname)
    STDERR.print "ssh to #{hostname}..."

    spawn("ssh #{hostname}") {
      begin
        expect("(yes/no)?", 1) { |match| "yes" }
      rescue
        STDERR.puts "failed"
        STDERR.puts "Key from #{hostname} may have been collected"
        return
      end
      # expect("Password:") { |match| "__PASSWORD__" }
      expect(/\$$/) { |match| "exit" }
    }

    STDERR.puts "done"    
  end
end


### main
if __FILE__ == $0
  if ARGV.size != 1
    puts "Usage"
    puts "    collect_sshkey NODE_LIST_FILE_NAME"
  end

  list_name = ARGV.shift
  if !FileTest.exists? list_name
    puts "#{list_name} doesn't exist"
  end

  SshKeyCollector::new(list_name).collect
end

実行するときは、

$ ruby collect_sshkey.rb host_list

って感じで、マシン名を1行に1つずつ記述したファイルの名前を与える。
SshKeyCollector#collectでhostnameFQDNとhostnameと同じマシンに対してホスト名だけの場合とFQDNの場合で鍵を取得しているが、これはsshが2つを区別するため。SshKeyCollector#collect_oneでは「(yes/no)?」プロンプトが出てこなくても処理が続けられるようにしている。これは既にssh鍵を持っている場合にはこのプロンプトは出力されなく、その場合にも後続するマシンの鍵を集めたいため。
このスクリプトを使って、簡単に全てのマシンの鍵を集めることができた。マシン名は設定ファイルで指定するので、移植も簡単。昔、素のexpect(URL)を使って挫折したことがあったんだけど、今回はRubyだったから(?)簡単にできた。