◆ 目的
複数台のサーバやネットワーク機器にログインし、
任意のコマンドを発行する。
その出力結果をノード単位にファイルに保存する。
◆ 実現手段
rubyのexpectモジュールを利用する。
expectモジュールの使い方自体はすぐにわかると思うが、
仮想端末(pty)のmaster、slave周りの動作を
頭にイメージしながらコードを読み書きすると
ぐっとおもしろさが増すだろう。
理解できないと使えないわけでは決してないが、
動きを把握しきれていないものを使っても楽しくないだろう。
そのため以下のリンクを一読することを強く強くオススメする。
そのため以下のリンクを一読することを強く強くオススメする。
◆ コードの実行例
コードを示す前にツールの使い方とその実行結果を見てもらう。
対象のノードをファイルに記載する。
sshもしくは、telnetでログインできることが前提である。
$ vi auto.txt
node1
node2
node3
ノードを羅列したファイル名を引数に実行する。
ノードへのログイン名とパスワード、
そして発行するコマンドを打っていく。
最後はエンタだけでよい。
$ ./auto.rb auto.txt
login name:
login password:
command : ls -l
command : pwd
command :
コマンドの実行結果がノードごとのファイルに保存される。
$ ls /var/tmp/output_*
/var/tmp/output_node1
/var/tmp/output_node2
/var/tmp/output_node3
◆ コード
#!/usr/bin/ruby require 'pty' require 'expect' require 'timeout' $expect_verbose = true class Auto def initialize(file) @file = file @cmds = Array.new @timeout= 10 @write_file_prefix = "/var/tmp/write" # + '_' + node @prompt_password = "[Pp]assword: " @prompt_answer = "yes/no" @prompt_crlf = "\\r|\\n" @prompt_expect = ">|#|\\$" end def get_cmds() print "login name : " @name = gets @name.chomp! print "login password : " @password = gets @password.chomp! @cmd_login = "/usr/bin/telnet -l #{@name}" #@cmd_login = "/usr/bin/ssh -l #{@name}" loop do print "command : " cmd = gets if cmd == "\n" @cmds << cmd break else @cmds << cmd end end end def read_nodes() open(@file) do | fp | while host = fp.gets next if host =~ /^[\s\t]*\n/ @host = host.chomp! spawn() end end end def spawn() PTY.spawn("#{@cmd_login} #{@host}") do | read, write | @read = read @write = write #@write.sync = true login() execute() end end def login() loop do @read.expect(/(#{@prompt_password}|#{@prompt_answer})/, @timeout) do | match | case match[0] when /#{@prompt_password}/ @write.puts @password return when /#{@prompt_answer}/ @write.puts "yes" end end end end def execute() cmds = @cmds.dup wfp = open("#{@write_file_prefix}_#{@host}", "w") still_login = true while cmds.length != 0 @read.expect(/(#{@prompt_crlf}|#{@prompt_expect})/, @timeout) do | match | if match[0] =~ /#{@prompt_expect}/ cmd = cmds.shift @write.puts cmd still_login = false else wfp.print match[0] unless still_login == true end end end wfp.close end end auto = Auto.new(ARGV.shift) auto.get_cmds() auto.read_nodes()
コマンドを順次実行するexecute()メソッド内で、
still_loginというフラグを使っている。
ログインすると、以下のような応答が返ることがある。
Last login: YYYYMMDD from node1
コマンドの実行記録と、その結果だけをファイルに保存したかったため、
コマンドを発行するまでの応答は無視するための措置として導入した。
(補足2)
コマンドを発行する条件を下としている。
>
#
$
@prompt_expect変数内で指定している部分である。
ただし、プロンプトが返る前に、応答出力に上記が含まれると
そこでコマンドが発行されてしまうため意図しない動作をすることがあるかもしれない。
誤認識させないように何かしらの手を加えてもいいポイントだろう。
(補足3)
実行するコマンドを読み込む処理で、
コマンドの終わりを示すためにエンタを押すだけの部分も
それをコマンドとして読み込ませている。
if cmd == "\n"
@cmds << cmd
break
else
@cmds << cmd
end
この理由だが、spawnした処理はexpect関数を
呼び出さなくなった段階では終了を迎える。
コマンドの実行結果を待ちたいので、エンタだけのコマンドを追加し、
expect処理を走らせているわけである。
@input.expect(/(#{@prompt_crlf}|#{@prompt_expect})/, @timeout) ~の部分である。
説明が難しいが、実際に実行させて挙動を見れば一目だろう。
(参考)
expectを利用してscpを自動化