ruby expect でルータ操作
すごく前に作った perl-Expect なスクリプト を元に一括コンフィグバックアップスクリプトとか使ってたんですが、
- 適当に作ったから汎用性がない
- 最近ぜんぜん perl 使ってないからかなり忘れてしもた
という有様で。したがって機能追加とかすごくしにくい状況になったので、どうせだったら ruby で作り直してみようか、と思ったのでありました。あと、Cisco AutoInstall 使えるようにしたときに作った初期コンフィグ生成スクリプトも perl で、ホストの情報とか別々に管理する状態になってたのが煩わしかったので、ついでにその辺もまとめて統合する方向で。
ホスト情報
こんな感じでホストの情報を持っておきます。
- :hostname : 'r000' :type : 'C1812J' :ipaddr : '192.168.7.101' :mgmtif : 'Fa0' :mgmtifmac : '0015.2b3f.623a' :protocol : 'telnet' :username : 'stereocat' :password : 'p@ssw0rd' :enable : 'p@ssw0rd' - :hostname : 'r001' :type : 'C1812J' :ipaddr : '192.168.7.102' :mgmtif : 'Fa0' :mgmtifmac : '001f.6c1d.cedc' :protocol : 'telnet' :username : 'stereocat' :password : 'p@ssw0rd' :enable : 'p@ssw0rd'
単純に expect で各ルータに対してコマンド実行させるだけなら IP 情報だけあればいいんだけど、autoinstall で使う用の初期コンフィグ生成のための情報も持たせてあるのでアレコレ追加してあります。(そっちの話は今回はナシ)
Expect
スクリプト
とりあえずこんな感じにしてみた。
#!/usr/bin/ruby require 'pty' require 'expect' require 'yaml' require 'erb' # options if ARGV.size >= 2 and File.file?(ARGV[0]) and File.file?(ARGV[1]) params = YAML.load_file(ARGV[0]) public_cmd_list = YAML.load_file(ARGV[1]) else puts "#{$0} <hostlist.yml> <cmdlist.yml>" exit end # debug $expect_verbose = true params.each do |param| case param[:protocol] when /telnet/i cmd = ['telnet', param[:ipaddr]].join(' ') when /ssh/i cmd = ['ssh', '-o StrictHostKeyChecking=no', '-i', param[:username], param[:ipaddr]].join(' ') else puts "unknown protocol #{param[:protocol]}" exit end pass_prompt = '^Password\s*:' user_prompt = '^Username\s*:' sub_prompt1 = '\][:\?]' sub_prompt2 = '\[confirm\]' yn_prompt = '\[yes\/no\]:' hostname = '[\w\-]+' prompt1 = '^' + hostname + '>' prompt2 = '^' + hostname + '(:?\(config\))?\#' puts "# run expect: #{cmd}" timeout = 60 PTY.getpty(cmd) do |reader, writer, pid| writer.sync = true puts "\n## start host #{param[:hostname]}\n" cmd_list = public_cmd_list.dup # copy list while(reader.eof? == false) enabled = false # flag reader.expect( %r! ( #{pass_prompt} | #{user_prompt} | #{prompt1} | #{prompt2} | #{sub_prompt1} | #{sub_prompt2} | #{yn_prompt} )$ !x, timeout) do |match| if match case match[1] when /#{pass_prompt}/i if(enabled) writer.puts param[:enable] else writer.puts param[:password] end when /#{user_prompt}/i writer.puts param[:username] when /#{prompt1}/ writer.puts 'enable' enabled = true when /#{prompt2}/ if cmd_list.length > 0 cmderb = ERB.new(cmd_list.shift) writer.puts cmderb.result(binding) else puts "\n### break\n" writer.puts 'exit' end when /#{yn_prompt}/ # must mutch before sub_prompt writer.puts 'yes' when /#{sub_prompt1}/, /#{sub_prompt2}/ writer.puts '' end else break end end # expect end # while puts "\n## end host #{param[:hostname]}\n" end puts "# finish expect: #{cmd}" end
やれること
- login/password の自動入力〜自動的に enable mode に入るところまで。
- とりあえず telnet する分にはコレで問題ない。ssh にも対応…できるはず。いまテスト環境で ssh enabled な機材を動かしてないのでちゃんとテストできてないけど。
やれないこと
- 実験環境で使う想定だったのでセキュリティは考慮していません。パスワードをべた書きしてます。良くはないけどね…
- あらゆるエラー処理は全く考慮していません。
つかいかた
ホスト情報とコマンド情報の YAML ファイルを食わせるだけ。
stereocat@lambda02:/tftpboot$ cat save_config.yml - 'copy run start' - 'copy run tftp://192.168.7.16/' stereocat@lambda02:/tftpboot$ stereocat@lambda02:/tftpboot$ ./run_expect.rb test_hostlist.yml save_config.yml # run expect: telnet 192.168.7.101 ## start host r000 Trying 192.168.7.101... Connected to 192.168.7.101. Escape character is '^]'. User Access Verification Password: r000>enable Password: r000#copy run start Destination filename [startup-config]? Building configuration... [OK] r000#copy run tftp://192.168.7.16/ Address or name of remote host [192.168.7.16]? Destination filename [r000-confg]? !! 3167 bytes copied in 1.852 secs (1710 bytes/sec) r000# ### break exit Connection closed by foreign host. ## end host r000 # finish expect: telnet 192.168.7.101 stereocat@lambda02:/tftpboot$
コマンドファイルについては、ERB でパラメタ埋め込みとかしてもよし。
- 'conf net tftp://192.168.7.16/<%= param[:hostname] %>.confg'
みたいに書いておくと、tftpサーバ上の ["ホスト名".confg] を自動的に読み込んでくれます。hostlist.yml の情報を元にホスト別のコンフィグを別途スクリプトで生成して一気に反映させたいとかもできるようにしてみました。
本当は環境情報とかも(例えば tftp server のアドレスとか)別途設定ファイル化してコマンドリストのファイルに直書きしないように作った方が良かったかな…。まあそこまで汎用化する必要があるかという話になるんだけど。
はまったこと
IO::Expect使い方がよくわからん
サンプルとか少ないんだよね…。そもそも条件分岐の方法とかがわからなかったし。
Expect あるある
プロンプトの正規表現が甘くて変なところを拾っちゃうケースがなあ…。例えば上に書いた conf net
コマンドの場合
r000#conf net tftp://192.168.7.16/r000.confg Host or network configuration file [host]? This command has been replaced by the command: 'copy <url> system:/running-config' Address or name of remote host [192.168.7.16]? (略)
みたいな出力があるわけですが、プロンプト(prompt1
)の設定が甘いと url>
をプロンプトだと判断して変なコマンド送っちゃったりとか…。まあこの手のスクリプトではいつもこういうのがあるんだけど。
こういうのはまあよくあってですね。
- プロンプト文字列の変化がベンダとか OS とかによって違うケース
/[Pp]assword\s*[:\?]?\s*$
, 大文字小文字とか順番とかスペース有無とか記号有無とか…。いちいち違うんだよ…。- メッセージフォーマットが OS バージョンとかによって違うケースとかもね…。
- 特にコンフィグを表示して設定情報とろうみたいな場合は結構面倒くさいことが。
- Interface description, ACL remark, banner など、ある程度任意の文字や記号が使える物が悪さをするケース
- モノにもよるけど、コマンドでプロンプト設定が可能な物で、プロンプト文字列がコンフィグ出力コマンドの中に含まれるような場合
などなど。まあクソ面倒くさいわけですよこの手の対話処理の自動化は。そんなわけで、netconf とか、リクエストとレスポンスが明確に分けられると言うだけでも使い勝手があるんだよね。たとえ通常の CLI コマンドを適当な XML でラップしておくって、通常の CLI コマンドの出力が適当な XML でラップされて返ってくる、というだけでも。コマンドの実行ステータスとレスポンスが明示的に分離できるってだけで expect ベースな対話処理よりは自動化実装は楽なんすよ。
……なんてことをこの SDN がどうのこうのといわれはじめているご時世にもかかわらずまだやってるってのがね。まあ API ベースで操作できるようにする、というのは高度な自動化とかオーケストレーションとかやる上では必然だと思いますよ。というのをこういうベタなスクリプト書いてると思うよね。
その他一般的な Expect のあれこれ
実装上の検討ポイントとして、汎用性をどこまで持たせるかというのがあるかなあと。例えば今回、サブプロンプトあるいは yes/no プロンプトについては固定で応答するようにしちゃってます。が、拡張 ping コマンドとか使おうとすると、サブプロンプトに対して適切な入力を返してやらないといけなかったりするので今の実装だとちゃんと動かない。
手っ取り早いのは、サブプロンプトも prompt2
扱いにして、コマンドのリストの方にサブプロンプト時の入力も全部書くって方向なんだけどね。そうなるとコマンドごとのサブプロンプト出力全部把握しないといけないのよ。まあ、自動応答だろうがそうでなかろうが基本的にはコマンドのサブプロンプト応答は全部把握しないといけないんだけどさ…。実装上、空応答(default)でスルーしてはいけないところだけをコマンドリストに入れておく、みたいな実装も考えられるわけですが、作るのと、後テストが面倒なんでね。その辺はやりたいこととか重要度に応じて選択です。
あとエラー処理そのものも全スルーしてます。
- 誤ったコマンドを実行したとき/デバイス上のエラーの対処
- expect サブプロセスのエラー
- timeout など expect 実行上の(実装上の)エラー
なんかは本当はテストが要るよね。たとえば処理実行中にいきなりケーブル抜けたらどうなるんだろう、とか。仕事使うような場合には見る必要があるでしょう。この手のエラー処理はひどく対処が大変で……。中途半端に設定入れちゃった物を元に戻す → 戻し方は? 放置して先に進む → どこからはじめてどこまで進めるの? とかね。オペレーションには依存性があるわけで、他にも例えば、デバイス操作の排他制御とかどう考えるんだとかね。そういうのを踏まえて対応考えないといけない。そうなるとまずはちゃんと操作履歴残してそれをたどりましょう、という話が出てくるわけでして。ログとかどうするんじゃとか。いろいろ考えないとイカンわけです。無理です。