シェルスクリプト (bash & expect) での情報収集

# 前書き
ShellScript (bash & expect) で機器から情報収集をするってよくやりますよね。

[RANCID](http://www.shrubbery.net/rancid/) などのツールを使ってちゃんと管理するのが理想的ではありますが、ちょっとした作業なんかで私は時々スクリプトを書いています。

で、作るたびにゼロから作っている気がしたのでメモがてら投稿。

# スクリプト
```bash:get-config.sh
#!/usr/bin/env bash
###############################################
# 2017-10-03 とりあえず作成
# 2017-10-18 説明を少し修正・追記
# 2017-12-29 col コマンドによる改行コード削除を追加
#
###############################################
# memo
# ・連想配列使ってるので bash 4 系必須
# ・ホスト名の大文字小文字の違いは無視
#


function usage_exit() {
cat <<_EOT_
Usage:
  $0 [-h] [-d] [-n] [-l]

Description:
  ネットワーク機器へログインして情報を収集する

Options:
  -d  debug mode on (未使用)
  -c  no confirm before connecting to devices
  -l  no check line number of output log files
  -h  help

_EOT_
exit 1
}

# If there are 0 arguments -->  error
# if [ $# = 0 ]; then
#   usage_exit
#   exit 1
# fi

# フラグのデフォルト値設定
FLAG_DEBUG=off
FLAG_CONFIRM=on
FLAG_LOG_CHECK=on

# 引数処理
if [ "$OPTIND" = 1 ]; then
    while getopts cdhl OPT
    do
        case $OPT in
            c)
                FLAG_CONFIRM="off"
                echo ">>> confirmation OFF <<<"
                ;;
            d)
                FLAG_DEBUG="on"
                echo ">>> debug mode ON <<<"
                ;;
            l)
                FLAG_LOG_CHECK="off"
                echo ">>> log check OFF <<<"
                ;;
            h)
                usage_exit
                ;;
            \?)
                usage_exit
                ;;
        esac
    done
else
    echo "No installed getopts-command." 1>&2
    exit 1
fi
shift $((OPTIND - 1))


####################################################################
# check for $FLAG_CONFIRM

function check_flag_confirm(){
    echo ""
    echo ">>> ----------------------------------------------------------------"
    echo ">>> Target --> host: ${1} ( IP: ${2} )"
    echo ">>> Continue？"
    echo ">>> YES --> just push return key"
    echo ">>> NO  --> exit & return key"
    echo -n ">>> Please enter : "
    read line
    echo ""
    case ${line} in
        [eE][xX][iI][tT])
            exit
            ;;
        '')
            echo ">>> go to next..."
            ;;
         * )
            echo ">>> input error..."
            exit 1
            ;;
    esac
}

####################################################################
# Type C1 : cisco, telnet, no login username
function get_from_type_c1(){
    declare -A hosts
    hosts["xxxx-sw01"]="192.168.1.250"
    hosts["xxxx-sw02"]="192.168.1.251"

    login_pass="xxxx"
    enable_pass="xxxx"

    for host_name in  ${!hosts[@]}; do

        host_ip=${hosts[${host_name}]}
        if [ "$FLAG_CONFIRM" = "on" ]; then
            check_flag_confirm ${host_name} ${host_ip}
        fi

        expect -c "
            set timeout 5

            spawn telnet ${host_ip}
            expect -nocase \"Password:\" {

                send \"${login_pass}\n\"
                expect -nocase \"${host_name}>\" {

                    send \"ter len 0\n\"
                    expect -nocase \"${host_name}>\"

                    send \"enable\n\"
                    expect -nocase \"Password:\"

                    send \"${enable_pass}\n\"
                    expect -nocase \"${host_name}#\"

                    send \"show inventory\n\"
                    expect -nocase \"${host_name}#\"
                }
                send \"exit\n\"
            }

            expect eof
        " | tee ${log_dir}/${current_time}/${host_name}.tmp

        # エスケープ文字列等々を削除したファイル(.log)作成
        col -bx < "${log_dir}/${current_time}/${host_name}.tmp" > "${log_dir}/${current_time}/${host_name}.log"
        # 一時ファイル(.tmp)削除
        rm "${log_dir}/${current_time}/${host_name}.tmp"
    done
}

####################################################################
# Type V
#   --> vyos, ssh,
function get_from_type_v1){
    declare -A hosts
    hosts["vyos117-01"]="192.168.83.254"
    hosts["vyos117-02"]="192.168.83.253"

    login_user="vyos"
    login_pass="vyos"

    for host_name in  ${!hosts[@]}; do

        host_ip=${hosts[${host_name}]}
        if [ "$FLAG_CONFIRM" = "on" ]; then
            check_flag_confirm ${host_name} ${host_ip}
        fi

        expect -c "
            set timeout 5

            spawn ssh -l ${login_user} ${host_ip}
            expect -nocase \"Are you sure you want to continue connecting (yes/no)?\" {
                send \"yes\n\"
                expect -nocase \"password:\"

                send \"${login_pass}\n\"

            } -nocase \"password:\" {

                send \"${login_pass}\n\"
            }

            expect -nocase \"${host_name}:\" {

                send \"show configuration | no-more \n\"
                expect -nocase \"${host_name}:\"
            }
            send \"exit\n\"


            expect eof
        " | tee ${log_dir}/${current_time}/${host_name}.tmp

        # エスケープ文字列等々を削除したファイル(.log)作成
        col -bx < "${log_dir}/${current_time}/${host_name}.tmp" > "${log_dir}/${current_time}/${host_name}.log"
        # 一時ファイル(.tmp)削除
        rm "${log_dir}/${current_time}/${host_name}.tmp"

    done
}

####################################################################

# ログの行数確認をする際の閾値
min_line_num="10"

# ログを保存するディレクトリパスを指定
log_dir="/tmp/config"
current_time=`date +%Y%m%d_%H%M%S`


# ログを保存するディレクトリを作成
if [ ! -d ${log_dir}/${current_time} ]; then
    mkdir -p ${log_dir}/${current_time}
    chmod 775 ${log_dir}/${current_time}
fi

# 実際に機器へログインする関数呼び出し
get_from_type_c1
get_from_type_v1


# $FLAG_LOG_CHECK が ON の場合は、取得したログの行数を確認する
# $min_line_num 以下の場合はエラーとして、ファイル名を表示する
if [ "$FLAG_LOG_CHECK" = "on" ]; then
    echo ""
    echo "----------------------------------------------------------------"
    echo ">>> Check the line number of output files."
    echo ">>> Looking for files with ${min_line_num} lines or less"

    for file in `ls -1 ${log_dir}/${current_time}/`; do
        line_num=`wc -l  ${log_dir}/${current_time}/${file} | awk '{print $1}'`
        if [ ${line_num} -lt ${min_line_num} ]; then
            echo "${file}"
        fi
    done
fi
```

# 説明
多分作るときに迷う部分って、expect の条件分岐っぽいところだと思うんですよね。
なのでそこの説明を多少してみる。

## `get_from_type_c1` の部分
```
        expect -c "
            set timeout 5

            spawn telnet ${host_ip}
            expect -nocase \"Password:\" {

                send \"${login_pass}\n\"                   <-- ログインパスワードを入力して
                expect -nocase \"${host_name}>\" {         <-- 意図したプロンプト（ホスト名>） が表示されなければ

                    send \"ter len 0\n\"
                    expect -nocase \"${host_name}>\"

                    send \"enable\n\"
                    expect -nocase \"Password:\"

                    send \"${enable_pass}\n\"
                    expect -nocase \"${host_name}#\"

                    send \"show inventory\n\"
                    expect -nocase \"${host_name}#\"
                }
                send \"exit\n\"                            <-- exit する
            }

            expect eof
        " | tee ${log_dir}/${current_time}/${host_name}.log  <-- tee を使って、スクリプトを実行した画面に表示しつつファイルへ出力を保存している
```

上記説明の、間の部分は特に問題ないですよね。
enable して show コマンド叩いてるだけだし。

## `get_from_type_v1` の部分
```
        expect -c "
            set timeout 5

            spawn ssh -l ${login_user} ${host_ip}
            expect -nocase \"Are you sure you want to continue connecting (yes/no)?\" {  <-- ssh 初回ログインのアレが
                send \"yes\n\"                                      <-- でたら、yes 入力して
                expect -nocase \"password:\"
                send \"${login_pass}\n\"                            <-- パスワードを入力

            } expect -nocase \"password:\" {                        <-- アレがでなければ、パスワードを入力
                send \"${login_pass}\n\"
            }
            expect -nocase \"${host_name}:\" {                      <-- 意図したプロンプト（ホスト名:）が表示されなければ

                send \"show configuration | no-more \n\"
                expect -nocase \"${host_name}:\"
            }
            send \"exit\n\"                                         <-- exit する


            expect eof
        " | tee ${log_dir}/${current_time}/${host_name}.log
```
上記説明の、間の部分は show コマンド叩いてるだけ