Pythonでネットワーク機器の操作を自動化してみる

この記事は[NIFTY Advent Calendar 2016](http://qiita.com/advent-calendar/2016/nifty) 14日目の記事です。
昨日は[@ntoofu](http://qiita.com/ntoofu)さんの[Ansibleの構成情報をグラフDBで管理する](http://qiita.com/ntoofu/items/84bf5641b5688fb1c29d)でした。

こんにちは。
自分は普段の業務では、ニフティクラウドにおけるネットワークインフラの構築・運用・保守を行っています。
今回はニフティクラウドのサービス側からは見えない、ネットワークの物理的な機器の運用における自動化の話題を少しだけ書きたいと思います。

## たとえばこんなシチュエーション
```
- 機器のリブートが走った
- 機器のインターフェースが落ちた
- モジュールが故障した
- とにかく何かおかしい
```
･･･みたいな事象が発生し、速やかに機器の状態を確認したい、といった状況がしばしば起こります。
こういった場合、（まだアクセスが可能なうちに）機器にログインし、
速やかにshow系のコマンドを叩き情報の確保を行うというオペレーションが必要になります。

その際のコマンドはある程度定型化されていると言えます。
（下記は特定の機器・OSに依らず、こういったコマンドが挙げられるよね、というリストです）

```
show running-config
show interfaces
show logging
show inventory
show modules
show tech-support
```
･･･など。
たとえばCisco IOSなど、ネットワークの機器・OSに対しては上記のようなコマンドを入力することで設定情報や状態の確認を行うことができます。

特に障害発生時のような緊急度が高い状況においては、
調査に時間を割くために定型的な作業についてはなるべく人の手を使わずに行いたいところではあります。

この記事では、上記のようなコマンドの結果を速やかに取得するため、
自動で機器にログインし、コマンドを投入の上、出力を取得するスクリプトをPythonで実装した話についてご紹介します。

## ネットワーク機器運用と自動化
一般に、ネットワーク機器に対する運用は3種類に分けられると考えています。

- 設定の確認
    上記で挙げたshowコマンドのような、機器の状態や設定を確認するコマンドの投入

- 設定の変更
    configモードや、commit、writeのような機器の状態・設定を変更するコマンドの投入

- 状態の監視
    SNMPによるTraffic流量などの監視、Syslogの取得

特にこの記事では、設定の確認にフォーカスしています。

参考の節でご紹介させていただいている記事で述べられているように、
ネットワーク機器に対するオペレーション手段の中で、汎用的なものはCLIによる操作です。
トラフィック流量などはSNMPで取得することができますが、情報の種類に限りがあります。
APIが実装されている場合もありますが、メーカによってまちまちです。

全ての機器はCLIによる操作が前提となっており、即ちsshやtelnetで接続した上での操作が、汎用的なネットワーク機器に対する操作方法となります。

つまり、こういった操作を自動化するということは、
下記のような通常人間がCLI上で行うオペレーションをスクリプトに代行させる、ということになります。

```
$ telnet 192.168.0.2
Trying 192.168.0.2...
Connected to 192.168.0.2 (192.168.0.2).
Escape character is '^]'.

User Access Verification

Username: root
Password: 

(hostname)#show run
...出力

(hostname)#
```



## 実装
スクリプトの内容についてご説明します。

### 実装環境
- Python 3.5.1
- Pexpect 4.2

Python3においてpexpectモジュールを用い、
expectによるCLIインタフェースに対する操作の代行を実装します。

### pexpectを用いたlogin
まず、pexpect.spawn()を用いて子プロセスを立ち上げます。
子プロセスはここではchildという名前とし、以降はchildに対して処理を進めます。
spawnの引数には立ち上げたいプロセスをコマンド文で指定する必要があり、ここではtelnetとしています。

child.logfile_readという変数にログファイル用のファイルディスクリプタを入れることで、
立ち上げた子プロセスの出力先をファイルに指定できます。
他に、この変数に標準出力(sys.stdout)を代入すると、コンソール上で出力を確認することができます。

```
def login(ipaddr, passwd):
    child = pexpect.spawn("telnet " + ipaddr)
    logname = "./log/" + "log_" + ipaddr + \
              "_" + datetime.now().strftime("%s") + ".log"
    wb = open(logname, 'wb')
    child.logfile_read = wb
```


-----

pexpectに限らずexpectを用いた処理は、

```
1. sendline()によるコマンドの送信
2. expect()による期待する文字列の出力待ち受け
```

の繰り返しになります。
pexpectの場合、待ち受ける文字列をリストで指定可能であり、ここでは予想される文字列に従い下記の通りに指定しています。

```
    expect_list = [u"#",
                   u">",
                   u"\nlogin: ",
                   u"Username: ",
                   u"Password: ",
                   u"Connection closed by foreign host.",
                   u"Login incorrect"]
```

pexpect.spawn(telnet [ipaddress]) を実行した時点で、機器に対してtelnetは実行され、機器は次の入力を待っています。
即ち、手動で実行した場合、次のようなイメージの状態です。

```
$ telnet 192.168.0.2
Trying 192.168.0.2...
Connected to 192.168.0.2 (192.168.0.2).
Escape character is '^]'.

User Access Verification
Username: [Cursor]
```

ここで、下記のようにexpectを実行します。

```
    index = child.expect(expect_list)
```

このとき、listの3番目に格納している"Username: "という要素と、機器の出力の最終行がマッチします。
expectで引数にリストを指定している場合、戻り値はリストの要素番号となります。
そのため、indexという変数には"3" が格納されます。
マッチする文字列が無い場合、expect()は機器の出力をさらに待ち続けます。
この場合タイムアウトするまで処理は終了しませんが、このときタイムアウトを待ち受けることでエラー処理を実装することも可能です。

-----

現在の実装では、indexの値に応じて下記の処理を行うようにしています。
機器やOSによって、Usernameを訊ねてきたり、いきなりパスワードを求めてきたり、様々です。
またログインに成功しても、enableを実行する必要がある機器の場合、再度パスワードの入力が必要となる場合があります。
それらの機器に対して個別に処理を記述するのは限界があるため、今はwhileによる実装で落ち着いています。
ログインを想定している全ての機器とOSに対して、手動の場合どのようにログイン処理が進むのかを確認し、
正常に分岐と処理が進むように設計する必要があります。

```
    while True:
        if index == 0:  # success to login.
            return child
        elif index == 1:  # need to promoted to enable mode.
            child.sendline("enable")
            index = child.expect(expect_list)
        elif index == 2 or index == 3:  # need to input "root".
            child.sendline("root")
            index = child.expect(expect_list)
        elif index == 4:  # need to input password.
            child.sendline(passwd)
            index = child.expect(expect_list)
        elif index == 5:  # Connection is closed.
            print("Unmatched password, or connection is closed.")
            return -1
        elif index == 6:  # incorrect password.
            print("\nFault: incorrect password.")
            return -1
```

これまでの例ではindexは値として3を保持しているため、分岐としては3つ目のif文に入ります。
sendline()によってUsernameとして求められている文字列を送信します。(rootは例です)
この時点で手動で機器の出力としては次の通りです。

```
Username: root
Password: [Cursor]
```

これは、expect_listで格納している"Password: "とマッチし、さらにwhileループが進みます。
同様にパスワードが機器に入力され、認証が通ればログイン完了となります。

ここでは"#" を受け取った時点でログイン完了としています。
これは下記のような特権モードでのログインを想定していますが、OSによっては誤動作が充分に考えられるため、改善の余地があります

```
Username: root
Password: 

(hostname)#
```




### pexpectを用いたコマンド実行
ログインが完了すると、機器は次のコマンド入力を待ち受けます。
今回は確認(show)系のコマンド入力を想定し、下記のような関数をつくりました。
commandsはリストで、要素毎に"show interfaces" などのコマンドを文字列として格納している想定です。
特にここではエラー処理を実装していません。

```
def exec_command(commands, child):
    expect_list = u"#"
    for c in commands:
        child.sendline(c)
        child.expect(expect_list)
```

もし設定変更系のコマンドを投入する場合、事前の設定の確認を行い、これから設定しようとしているコマンドを投入するにふさわしい状態かどうかを判定する必要があります。
そして投入後も、設定が無事反映されたか、他におかしなログが出ていないかどうかを確認する必要があり、
ここまでで説明したような文字列待ち受けでこれらを実装することはかなりの根気が必要です。

完全に定型化できている作業などは出力も想定ができるため、実装しているものもあります。
一方でかなり実作業に近い内容を設定するスクリプトのため、ここでの紹介は控えたいと思います。

さて、子プロセス立ち上げ時に設定した"child.logfile_read" に対して、
ログインからここまで実行したコマンドの出力は全て書き込みされています。
今回はデモ的な実装ですが、パスワードもスクリプトに入力させるようにした上で、
特定のSyslogを確認した時にこのスクリプトを叩くように設定すると、
例えばモジュールエラーが発報された直後に、機器の設定・状態を取得できるようにすることができます。

一部コマンドでは機器内部に保守情報を生成し、別途手動でFTPなどで取得する必要があるものもありますが、
これらの自動化についてはまたの機会に行いたいと思います。

## 今後に向けて

今回はpexpectを用いたCLI操作の自動化についてご紹介しました。
かなり泥臭いやり方ではありますが、一方でtelnetで操作できないネットワーク機器はありません（？）ので、
適切に設計すればどんな機器にでも使える方法でもあります。

JuniperのPyEzなど、特定メーカが提供しているREST APIについてもお話できたらいいな、と思っています。

ありがとうございました。

-----

明日は、[@hitsumabushi](http://qiita.com/hitsumabushi)さんの投稿です。

## 参考
- [運用自動化のためにネットワーク機器に求めるもの](http://yuyarin.hatenablog.com/entry/2015/12/01/000001)
- [なぜネットワーク運用自動化が進まないのか](http://www.slideshare.net/taijitsuchiya5/ss-47398248)
- [Pexpect version4.2](http://pexpect.readthedocs.io/en/stable/index.html#)
- [pexpect - 対話アプリの自動運転](http://eng-manima.blogspot.jp/2014/09/pexpect.html)  
    Pexpect2について解説されています