Bash環境 Shellscript Expect 対話型 コマンド実行自動化 (Linux, Mac)

概要

LinuxMacでコマンドの自動実行を行いう為のスクリプト
コマンドによっては対話処理が必要な場合がある。 (例: SSHのPW入力, yum/apt等のinstallコマンド)
対話入力を含めて自動実行をするスクリプト
対話処理には[expect]を使用する。

参考 https://linux.die.net/man/1/expect

install expect

yum install expect
apt-get install expect
brew install expect

Command

set [WORD] [VALUE]  #変数を宣言して値を代入
set timeout [SEC]   #expectのタイムアウト時間を指定 [無限: -1]
spawn [CMD]         #対話処理が発生するコマンド
expect \"[WORD]\"   #コマンド結果や処理中の中で対話処理のきっかけとなる文字を指定
exp_continue        #expectにマッチしても再度最初に戻りチェック (ループ処理のcontinueと同じ)
send [CMD]          #対話処理の際に入力するコマンドや文字列
send_user [WORD]    #標準出力に任意の文字を出力 (抑制状態でも出力される)
puts [STR]          #標準出力のみに指定の文字列を出力
open [PATH]         #指定したパスのファイルを読み込む
log_file [PATH]     #ログを指定のファイルに出力 (-noappend オプションで上書きモード)
log_file            #ファイル指定なしでログ記録終了
log_user [NO]       #標準出力へのログ(処理内容)の出力を制御 [0: 出力を抑制, 1: 抑制解除]
send_log [WORD]     #ログファイルに任意の文字列を書き込む
interact            #ユーザに制御を戻す (ログインだけを自動化する際などに使用)
exit                #spawnで開始したセッションを終了

Expect Pattern

正規表現の注意事項

  • [.]は改行文字を検知できない
  • 特殊文字エスケープが2重になる (例: "\$")

Case1: 通常

expect -c "
    spawn [CMD]
    expect \"[WORD]\"
    send [CMD]
"

Case2: 複数

expect -c "
        spawn [CMD]
        expect {
            \"[WORD]\" {
                send
            }
            \"[WORD]\" {
                send
            }
        }
"

Case3: 正規表現

expect -c "
        spawn [CMD]
        expect {
            -re \"[WORD]\" {
                send
            }
            -re \"[WORD]\" {
                send
            }
        }
"

Execute Pattern

Case1

#!/bin/bash
expect -c "
    spawn [CMD]
    expect \"[WORD]\"
    send [CMD]
"

Case2

  • expect program (test.exp)
#!/usr/bin/expect
spawn [CMD]
expect \"[WORD]\"
send [CMD]
  • execute
expect test.exp

条件分岐

if { 条件1 } {
    #処理1
} elseif { 条件2 } {
    #処理2
} else {
    #いずれにも該当しない場合の処理
}

ループ処理

特殊処理

  • break : 処理を中断しループを抜ける
  • continue : 処理を中断し次のループ処理へ移行

for

for { 変数の初期化 } { 条件 } { 変数の加算 } {
    #処理
}

### 例
for { set i 0 } {$i < 5 } { incr i } {
    #処理
}

while

while { 条件 } {
    #処理
}

比較演算子

### 論理演算子
x && y      :x条件とy条件の比較結果がともにTRUE
x || y      :x条件とy条件の比較結果のどちらかがTRUE
!x          :x条件の比較結果がFALSEの時にTRUE
### 関係演算子
x < y       :xよりyが大きい
x > y       :xよりyが小さい
x == y      :xとyが等しい
x != y      :xとyが等しくない
x <= y      :xとyが等しい、または小さい
x >= y      :xとyが等しい、または大きい。

例1 ssh接続を自動化

#!/bin/bash

HOST="xx.xx.xx.xx"              #:接続先IP
USER="user"                     #:ユーザ名
PASS="password"                 #:パスワード
WAITFLG="#"                     #:反応待ち文字
logFile="autolog.log"           #:ログの出力先

expect -c "
    set timeout -1
    spawn ssh ${USER}@${HOST}   #:SSH接続のプロセスを開始

    expect {
        \"password:\" {         #:パスワード要求を待機
            send \"${PASS}\n\"  #:パスワード要求に対して送信
            exp_continue
        }
        \"fingerprint\" {       #:初回接続時のfingerprintを待機
            send \"yes\n\"
            exp_continue
        }
        \"${WAITFLG}\" {        #:接続完了時のプロンプトを待機
            send \"\n\"
        }
    }
    interact                    #:接続状態でユーザに制御を戻す
"

例2 ssh接続先での複数コマンド実行

Command file (cmd.txt)

cat /etc/os-release
df -h

Program (autocmd-single.sh)

#!/bin/bash

HOST="xx.xx.xx.xx"                              #:接続先IP
USER="user"                                     #:ユーザ名
PASS="password"                                 #:パスワード
CMDPATH="./cmd.txt"                             #:実行対象のコマンドリストのファイル
WAITFLG="#"                                     #:反応待ち文字
logFile="autolog.log"                           #:ログの出力先


expect -c "

    #SSH接続
    set timeout -1                              #:expect待機のタイムアウト時間 今回は無限
    spawn ssh ${USER}@${HOST}                   #:SSH接続のプロセスを開始
    
    expect {
        \"password:\" {                         #:パスワード要求を待機
            send \"${PASS}\n\"                  #:パスワード要求に対して送信
            exp_continue
        }
        \"fingerprint\" {                       #:初回接続時のfingerprintを待機
            send \"yes\n\"
            exp_continue
        }
        \"${WAITFLG}\" {                        #:接続完了時のプロンプトを待機
            send \"\n\"
        }
    }
    # Fileを読み込んでコマンド実行
    set FILE [open ${CMDPATH} r]                #:Fileパスからデータを読み込む
    while {! [eof \$FILE]} {                    #:ファイルを1行ずつ読み込む

        set line [gets \$FILE]                  #:現在読み込んでいる行のCommandを格納

        #コマンドの送信
        expect \"\${WAITFLG}\" {                #コマンドの反応待ち
            send \"\${line}\n\"                 #:コマンドを送信
        }
        #見やすいように改行を入れる
        for { set i 0 } {\$i < 3 } { incr i }{  #:3回改行を入れる
            expect \"${WAITFLG}\" {             #:コマンドの反応待ち
                send \"\n\"                     #:コマンドを送信
            }
        }
    }
" | tee -a $logFile

例3 複数のssh接続先でコマンド実行

仕様

  • コマンドを実行する対象のIPリストを[target.txt]で作成
  • 今回はCisco機器を対象として作成
  • IPリスト[target.txt]に記載したものと同じ名前で、以下3つのファイルを作成して配置
    • コマンドリストのファイルを[./cmdlist]配下に作成
    • ユーザリストのファイルを[./userlist]配下に作成
    • パスワードリストのファイルを[./passlist]配下に作成
    • 特権パスワードリストのファイルを[./enablelist]配下に作成
  • ディレクトリ構成
    • ./autocmd-multi.sh プログラム本体
    • ./target.txt コマンドの実行先リスト
    • ./cmdlist 各実行先ごとのコマンドを記載
      • xx.xx.xx.xx.txt
      • xx.xx.xx.xx.txt
    • ./userlist 各実行先ごとのログイン用のユーザ名
      • xx.xx.xx.xx.txt
      • xx.xx.xx.xx.txt
    • ./passlist 各実行先ごとのログイン用のパスワード
      • xx.xx.xx.xx.txt
      • xx.xx.xx.xx.txt
    • ./enablelist 各実行先ごとの特権パスワード
      • xx.xx.xx.xx.txt
      • xx.xx.xx.xx.txt
    • ./log ログの保存先ディレクト
      • xx.xx.xx.xx.log

hosts file (target.txt)

xx.xx.xx.xx

Command Directory (./cmdlist)

  • xx.xx.xx.xx.txt
show version
show run

UserList Directory (./userlist)

  • xx.xx.xx.xx.txt
username

PassList Directory (./passlist)

  • xx.xx.xx.xx.txt
password

EnableList Directory (./enablelist)

  • xx.xx.xx.xx.txt
enablepass

Program (autocmd-multi.sh)

more等の入力待機が入ると、待機状態で処理が終わらないので注意が必要

#!/bin/bash

#Cisco機器へのコマンド実行を想定
HOSTSPATH="./target.txt"                                                                        #:宛先リスト
CMD="./cmdlist"                                                                                 #:コマンドリスト(共通)
USER="./userlist"                                                                               #:ユーザ名(共通)
PASS="./passlist"                                                                               #:パスワード(共通)
ENAPASS="./enablelist"                                                                          #:特権パスワード(共通)
WAITFLG="#"                                                                                     #:反応待ち文字
logdir="./log"                                                                                  #:ディレクトリ指定(ファイル名ではない&/で終わる)
CHALLENGE=2                                                                                     #:パスワード試行回数制限


echo "########### Directory and File Check ###########"
dirflg=1
if [ ! -f $HOSTSPATH ]; then echo "File not found: $HOSTSPATH";dirflg=0; fi                     #:ファイルの存在確認
if [ ! -d $CMD ]; then echo "Directory not found: $CMD";dirflg=0; fi                            #:ディレクトリの存在確認
if [ ! -d $USER ]; then echo "Directory not found: $USER";dirflg=0; fi                          #:ディレクトリの存在確認
if [ ! -d $PASS ]; then echo "Directory not found: $PASS";dirflg=0; fi                          #:ディレクトリの存在確認
if [ ! -d $LOGDIR ]; then echo "Directory not found: $LOGDIR";dirflg=0; fi                      #:ディレクトリの存在確認

if [ $dirflg -eq 0 ]; then echo "Execution finished";exit; fi                                   #:どれか一つでも存在しなければ処理を終了
echo -e "Check Complete\n\n"




echo "########### Start Get Command ###########"
while read line ; do

    fileflg=1
    if [ ! -f "${CMD}/${line}.txt" ]; then echo "File not found: ${CMD}/${line}";dirflg=0; fi   #:ファイルの存在確認
    if [ ! -f "${USER}/${line}.txt" ]; then echo "File not found: ${USER}/${line}";dirflg=0; fi #:ファイルの存在確認
    if [ ! -f "${PASS}/${line}.txt" ]; then echo "File not found: ${PASS}/${line}";dirflg=0; fi #:ファイルの存在確認
    if [ $dirflg -eq 0 ]; then echo "${line}: Skipped";continue; fi                             #:どれか一つでも存在しなければ対象をスキップ



    expect -c "

        log_user 0                                                                              #:Expectの処理内容を出力しない
        spawn ssh $(<${USER}/${line}.txt)@${line}

        set i 0                                                                                 #:ログイン直後のPW試行回数カウント
        set j 0                                                                                 #:特権ログインのPW試行回数カウント
        set flg 0                                                                               #:ログインが成否確認用フラグ
        set timeout 5
        expect {
            \"fingerprint\" {
                send \"yes\n\"
                exp_continue
            }
            -re \"\[P|p\]assword\" {
                if {!(\$i < ${CHALLENGE})} { exit }
                incr i 1
                send \"$(<${PASS}/${line}.txt)\n\"
                exp_continue
            }
            \">\" {
                send \"enable\n\"
                expect {
                    \"Password\" {
                        if {!(\$j < ${CHALLENGE})} { exit }
                        incr j 1
                        send \"$(<${ENAPASS}/${line}.txt)\n\"
                        exp_continue
                    }
                    \">\" {
                        if {!(\$j < ${CHALLENGE})} { exit }
                        send \"enable\n\"
                        exp_continue
                    }
                    \"${WAITFLG}\" {
                        set flg 1
                        send \"ter len 0\n\"
                    }
                }
            }
            \"${WAITFLG}\" {
                set flg 1
                send \"ter len 0\n\"
            }
        }
        if { \$flg == 0 } { send_user \"${line}: Connection failed\n\"; exit }                  #:ログイン出来なければ処理を終了

        send_user \"${line}: running\n\"                                                        #:処理中のホストを表示
        log_file -a ${LOGDIR}/${line}_$(date +"%y%m%d_%H%M%S").log                              #:ログの取得開始

        set FILE [open \"${CMD}/${line}.txt\" r]
        while {! [eof \$FILE]} {
            set cmd [gets \$FILE]
            expect \"${WAITFLG}\" {
                send \"\${cmd}\n\"
            }

            for { set i 0 } {\$i < 3 } { incr i } {                                             #:ログが見やすいよう改行を入れる
                expect \"${WAITFLG}\" {
                    send \"\n\"
                }
            }
        }
        log_file                                                                                #:ログの取得終了
    "
done < $HOSTSPATH

echo -e "\n\n~~ All Finish ~~\n"                                                                #:プログラムが終了したことを表示