rubyで複数のrsyncを、パスワード入力とともに実行。最後にcrontabに登録



なんかすげーひさびさの更新が、こんな味も素っ気もない記事ですな。

…最近なんかイベントがあったはずなんだけど、よくわかりませんな。中止になったんじゃないですか?


外部コマンドを実行するとパスワード入力を求められちゃう

さて、ある事情があって、異なるサーバ間でデータの同期を取る必要がありいろいろ細工してみた。

rsyncコマンドで対象ファイルを指定すればいいんだけど、その際に厄介なことは、パスワードの入力が求められること。

一回限りだったらそれでも別にいいんだけど、できればcronに突っ込んで1時間ごとに実行したい。

rsyncコマンドを実行する際にパスワード入力をさせない方法は幾つかあるようだけど、どれも面倒そう。こういう運用は2ヶ月ほどの予定で、それ以後は同期元のサーバは停止するので、わざわざrsyncデーモンを走らせるとかの処置は、やってもいいけどちょっとモチベーションがわかない。

本格的なバックアップサーバを用意するわけではないので、このままrubyで外部コマンドを実行する簡単なスクリプトを組んでおきたい。



で、スクリプトを組む上でのポイントは

  • 複数のrsyncコマンドを実行する
  • パスワードを入力する
  • 成功失敗に関わらず、実行結果をログに出す
  • ひとつのコマンドが失敗しても次のコマンドを実施する
ほんとうだったらここで、スクリプト中のパスワードも隠蔽したいけど、まあ、同期先も元も俺ひとりが管理しているし、コードの中に平文でもいいか。

こういう対話的な処理をrubyで実現する場合、ptyとexpectを利用するのが定番らしい。





rsyncの動き方

その前にrsyncコマンドの動き方をちょびっと調べてみた。



まず、正常系。


[root@vps ~]# rsync -avz -e ssh /hogehoge/hagehage1 root@newserver:/hogehoge/hagehage1
root@newserver's password:
building file list ... done
hagehage1

sent 93 bytes received 42 bytes 24.55 bytes/sec
total size is 0 speedup is 0.00

なるほど、「password:」と表示されたらパスワードを入力し、「done」と来たら正常に終わるのね。



次、異常系。


[root@vps ~]# rsync -avz -e ssh --timeout=10 /hogehoge/hagehage1 root@newserver:/hogehoge/hagehage1
io timeout after 10 seconds -- exiting
rsync error: timeout in data send/receive (code 30) at io.c(171) [sender=2.6.8]
つまり、「rsync error」が来たら何かエラーが来てるってことね。その続きの文字列を取得できれば、エラーの内容もわかるわけね。それで、パスワード入力を求められる前にエラーが来ることもあるってことね。

これを、同期をとりたいファイル/ディレクトリの数だけ実施するようにすればいいわけか。


こんなん出ましたけど

んで、でっち上げたのが以下のコード。

たぶん、似たような需要は結構あるような気がするので、ここに掲載する。


#!/usr/bin/ruby
require 'pty'
require 'expect'
require 'syslog'

# sshで暗号化して、タイムアウトを10秒設定で3箇所の同期をとります。
rsynccmd = [
"rsync -avz -e ssh --timeout=10 /hogehoge/hagehage1 root@newserver:/hogehoge/hagehage1",
"rsync -avz -e ssh --timeout=10 /hogehoge/dir1/ root@newserver:/hogehoge/dir1/",
"rsync -avz -e ssh --timeout=10 /foo/bar root@newserver:/foo/bar"
]

# 実行結果をsyslogに出します。
syslog = Syslog.open("rsyncdata",Syslog::LOG_NDELAY )

rsynccmd.each {|cmdstr|
warnmsg = nil # 警告メッセージ初期化
begin
PTY.spawn(cmdstr) do |r,w| # rsyncコマンドを実行 r にrsyncからの出力。 w にrsyncへの入力。
w.sync = true # 入力と同時にflushしなさいというおまじない
# まずrsyncコマンド出力、最初の処理。
# 基本的には最初にパスワードの入力を求められるけど、場合によってはエラーが帰って来ることもあり得るので
# 「password:」か「rsync erro」を待つ。
r.expect(/password:|rsync error/,10){ |line|
# このブロックは、「password:」「rsync erro」という出力があった場合に実行される。
# それか、タイムアウト(10秒)が来たらlineにnilが入った状態で呼び出される。ちなみに
# line==「password:」正常な処理であれば、これが来るはず。
# line==「rsync erro」同期先が落ちてるとかだと、これが来るかも。
# line==nil nilだとタイムアウトだけどこの例に限って、ここではタイムアウトはこないはず。
if line.to_s.include?("password") then
w.puts "newpassword" # rsyncコマンドへパスワード入力
else
# こっちに来るときは rsync error に引っかかっているはずなので
warnmsg = r.gets # rsyncでエラーが帰ってきたら続きの出力を保持。エラーメッセージ突っ込んでおく
end
}
r.expect(/rsync error|done/,10) { |line|# エラーか終了のメッセージを待つ
# パスワード入力後の処理を待つ。
# パスワードが無事入力できたら、あとはrsyncがうまく終了するか何かでエラーを返すかどっちか。
# なので、「rsync error」「done」を待っていればいい。
if line.to_s.include?("rsync error") then
warnmsg = r.gets # rsyncでエラーが帰ってきたらエラーメッセージ突っ込んでおく
else
# doneで受けている場合は処理が成功したとみなす。
warnmsg = nil
end
}
end
rescue PTY::ChildExited => e # PTYでキックしたプロセスが終了したときの処理
syslog.warning("PTY::ChildExited[" + e.status + "]")
exit 1
rescue => e # それ以外のエラーの処理。まぁsyslogに記録するだけだけど。
syslog.warning(e.class.to_s + "[" + e.message + "]")
exit 2
end

# 実行結果をsyslogに記録
if warnmsg == nil then
syslog.info("OK[" + cmdstr + "]")
else
syslog.warning("NG[" + cmdstr + "]")
syslog.warning("NG[" + warnmsg +"]")
warnmsg = nil
end
}
exit 0