ssh経由の操作をexpectコマンドで自動化しようとした時に、expectで調べると、入出力関連に関しては出てくるが、文字列操作や制御構文周りがよくわからない。
ここら辺は、tclに関して調べるとわかるようになっている。
参考にしたサイト
LInux JM Home Page「expect (1)」
FreeSoftNet 「Tcl>文法とコマンド」
アプリコット PukiWiki「expectで自動化」
if文の書き方
他の言語と同じように「if (条件){実行内容}」と書くと妙なエラーになる。
expectでは「if {条件} {実行内容}」というように、どちらも「{}」で囲む。
また、「if」と「{」の間、「条件の}」と「実行内容の{」の間のそれぞれにスペースを挟む必要がある。
文字列を比較して、異なる場合は「diff」、同じであれば「same」と出力するexpectは下記の様になる。
#!/usr/bin/expect -f
set hostnamenew "testhostnew"
set hostnamenow "testhost"
puts "hostnamenew: $hostnamenew"
puts "hostnamenow: $hostnamenow"
if { "$hostnamenew" != "$hostnamenow" } {
puts "diff"
} else {
puts "same"
}
誤って「if (条件) {実行内容}」とした場合、下記の様な「unbalanced open paren in expression」というエラーとなる。
unbalanced open paren
in expression "("
(parsing expression "(")
invoked from within
"if ( "$hostnamenew" != "$hostnamenow" ) {
puts "diff"
} else {
puts "same"
}"
「if(条件) {実行内容}」と「if{条件} {実行内容}」とifと{の間にスペースを入れない場合は下記のような「invalid command name」となる
invalid command name "if("
while executing
"if( "$hostnamenew" != "$hostnamenow" ){"
invalid command name "if{"
while executing
"if{ "$hostnamenew" != "$hostnamenow" }{"
面倒くさいことに「if {条件}{実行内容}」と、ifと条件の間にはスペース入れたけど、条件と実行内容の間にスペースがない場合は下記の「extra characters after close-brace while executing」というエラーになる。
extra characters after close-brace
while executing
"if { "$hostnamenew" != "$hostnamenow" }{"
コマンド出力から文字列を取り出しと文字列の切り出し
expectを使う場合は、sshで他のホストに接続してコマンドを実行する、という用途で使うことが多い。
コマンドの実行結果によって、処理を変えたい場合、どうすればいいのか?
「expect -indices -re “条件式”」で条件式に該当した行を$expect_out(数字,string)で拾うみたいなんだけど、いまいち動作がよくわからない。
「ip a s ens160」を実行して、そのIPアドレスを取得したい場合の例として以下を作った
expect "\[#$] "
send -- "ip a s ens160\r"
expect -indices -re "inet (.*)\r"
puts "===buffer==="
puts "$expect_out(buffer)"
puts "===str0==="
puts "$expect_out(0,string)"
puts "===str1==="
puts "$expect_out(1,string)"
puts "===="
これの実行結果は下記となった。
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
altname enp3s0
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===buffer===
ip a s ens160
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
altname enp3s0
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===str0===
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
===str1===
172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
====
valid_lft forever preferred_lft forever
inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
valid_lft forever preferred_lft forever
条件を「inet (.*)\r」から「inet6 (.*)\r」に変更
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
altname enp3s0
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
valid_lft forever preferred_lft forever
inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
valid_lft forever preferred_lft forever
===buffer===
ip a s ens160
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:33:27:f8 brd ff:ff:ff:ff:ff:ff
altname enp3s0
inet 172.17.44.48/16 brd 172.17.255.255 scope global noprefixroute ens160
valid_lft forever preferred_lft forever
inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
valid_lft forever preferred_lft forever
===str0===
inet6 fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
valid_lft forever preferred_lft forever
===str1===
fe80::cca7:388a:36e7:d688/64 scope link noprefixroute
valid_lft forever preferred_lft forever
====
ここからさらにIPアドレスの部分を取り出すには「string first ~」「string last ~」と「string range ~」を使って文字列を切り出す。
set str $expect_out(1,string)
set stred [string first "/" $str]
puts "[string range $str 0 $stred]"
set stred [expr $stred - 1]
puts "[string range $str 0 $stred]"
上記の実行結果としては下記のようになる。
172.17.44.48/
172.17.44.48
他の言語のsubstring系だと開始アドレスと、そこを起点に取り出す文字列の長さを指定するが、tclのstring range では、開始アドレスと終了アドレスの2つを指定する形になるので注意が必要になる。
リストファイルを順に処理する
サーバ名 IPアドレス ユーザ名 パスワードがかかれている一覧ファイルを読み込ませて順に処理させる例
まず、一覧ファイルの例
server1 172.17.44.51 admin password123#
server2 172.17.44.52 admin password123#
スクリプト
#!/usr/bin/expect -f
if { $argc < 1 } {
puts "Usage: $argv0 <serverlist>"
exit 1
}
set filename [lindex $argv 0]
set contents [read [open $filename]]
set contentline [split $contents "\n"]
foreach line $contentline {
#puts "$line"
set linesplit [split $line "\t"]
set hostname [lindex $linesplit 0]
set ipaddr [lindex $linesplit 1]
set username [lindex $linesplit 2]
set password [lindex $linesplit 3]
if { $hostname != "" } {
puts "hostname:$hostname, ipaddr:$ipaddr, username:$username, password:$password"
}
}
実行例
$ ./sample3 serverlist
hostname:server1, ipaddr:172.17.44.51, username:admin, password:password123#
hostname:server2, ipaddr:172.17.44.52, username:admin, password:password123#
$
なお、処理スクリプトに「if { $hostname != “” } {」を入れているのは改行のみやEOFのみの行を処理してしまわないようにするため
とはいえ、これを応用してssh接続させるスクリプトにしたところ、間にアクセスできないホストがあると、そこでエラー終了してしまうので、取り扱いが面倒であることが判明。
(エラー時の処理を実装すればいいんだけど、面倒)
よって、bashスクリプト側でリストは処理し、そこからexpectスクリプトを呼び出すこととなった。
そうやって作成したのが「Cisco UCSのCIMCにssh接続して設定を行う」になる