IOS-XEのOn-box Pythonで行う機器監視

##はじめに
Ciscoのネットワーク機器の監視を行う際に、showコマンドの結果を参照したいと思うことがあると思います。
今回はIOS-XEのGuest Shellから、pythonを用いてshowコマンドの結果を取得し、Elasticsearchへ送信して可視化してみたいと思います。

### Guest Shellについて
Guest ShellとはIOxでホスティングされるLinuxのコンテナ環境で、Catalyst9000シリーズやISR4000シリーズなどの製品で実装されています。
Guest Shellではpythonのcliライブラリが提供されており、これを用いるとGuest Shellで実行されるpythonスクリプトからIOSdのコマンドを実行することができます。
IOxについては[こちら](https://developer.cisco.com/docs/iox/#!introduction-to-iox/what-is-iox)を参照すると良いと思います。


通信を行う際のイメージ図は以下です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223892/439f7f51-8197-14b1-48a5-3d8e520d16af.jpeg" height="500">

[Programmability Configuration Guide, Cisco IOS XE Fuji 16.9.x](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/169/b_169_programmability_cg/guest_shell.html)より

##環境
以下のような構成で環境を構築します。
（構築方法は省略）
監視対象はCSR1とします。
簡略化していますが、CSR1に通信負荷をかけるためにVMも用意しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223892/b4360f41-0500-79a7-9bf8-9b96a9cb6f32.jpeg" height=250>

| Software | Version |
| --- | ---|
|Elasticsearch| 6.5.0 |
|Kibana|6.5.0|
|CSR1000V|IOS-XE 16.09.04|

##Elasticsearchの準備
Elasticsearchは全文検索エンジンであり、ドキュメントストア（もしくはドキュメント指向型）と呼ばれるデータベースのマネジメントシステムとしても使用されます。
簡単に用語を書いておきます。
（飽くまで自分の理解です）

| 用語 | 意味 |
| --- | --- |
| Document | 格納されるJSONデータの単位 |
| Mapping | Documentの中身を具体的に定義したもの |
| Index | Documentに必ず付与される索引名<br>設定やクエリを行う単位として使用する<br>事前に作成することもDocumentの登録時に自動で作成することも可能|
| Template | 新たなIndexを作成するときに適用される設定とMapping |


CSR1から監視項目として以下の情報を取得することにします。

- 時刻(date)
- IOSdのCPU使用率(%)
- kernelのCPU使用率(%)
- IOSdのメモリ使用率(%)
- kernelのメモリ使用率(%)
- GigabitEthernet1の送受信量(bps)
- Interface/line protocolのステータス一覧(text)
- ルーティングテーブル(text)

上記のデータを格納するためにIndexのTemplateを定義します。
CSR1からDocumentを格納する際のIndexはrouter-csrとでもしておきましょう。

```
curl -XPUT 'http://elasticsearch.example.com:9200/_template/router' -H 'Content-Type: application/json' -d @router_template.json
```

```json:router_template.json
{
  "template": "router-*",
  "mappings":{
    "monitoring": {
      "dynamic": "strict",
      "properties": {
        "@timestamp":{
          "type": "date",
          "format": "date_time_no_millis"
        },
        "cpu_utilization_iosd":{
          "type": "integer"
        },
        "cpu_utilization_kernel":{
          "type": "integer"
        },
        "memory_utilization_iosd":{
          "type": "integer"
        },
        "memory_utilization_kernel":{
          "type": "integer"
        },
        "GigabitEthernet1_input":{
          "type": "integer"
        },
        "GigabitEthernet1_output":{
          "type": "integer"
        },
        "interface_status":{
          "type": "keyword"
        },
        "routing_table":{
          "type": "keyword"
        }
      }
    }
  }
}
```

##CSR 1000Vの設定
GigabitEthernet1の送受信量は30秒間の平均を用いることにします。
送受信パケット数の差分から算出することも可能ですが、`load-interval`を調整して`show interface`から取得するほうが楽かなと思います。

```
CSR1#conf t
CSR1(config)#interface gigabitethernet 1
CSR1(config-if)#load-interval 30
```

次にGuest Shellを有効にする設定と、Guest Shellから通信を行うためのNATの設定を同時に行います。

```
CSR1#conf t
CSR1(config)#iox

CSR1(config)#ip http server

CSR1(config)# interface GigabitEthernet3
CSR1(config-if)# ip nat outside
CSR1(config-if)# exit

CSR1(config)# interface VirtualPortGroup0
CSR1(config-if)# ip address 192.168.35.1 255.255.255.0
CSR1(config-if)# ip nat inside
CSR1(config-if)# exit

CSR1(config)# ip nat inside source list GS_NAT_ACL interface GigabitEthernet3 overload
CSR1(config)# ip access-list standard GS_NAT_ACL
CSR1(config-std-nacl)# permit 192.168.35.0 0.0.0.255
CSR1(config-std-nacl)# exit

CSR1(config)#app-hosting appid guestshell
CSR1(config-app-hosting)# app-vnic gateway0 virtualportgroup 0 guest-interface 0
CSR1(config-app-hosting-gateway0)# guest-ipaddress 192.168.35.2 netmask 255.255.255.0
CSR1(config-app-hosting-gateway0)# app-default-gateway 192.168.35.1 guest-interface 0
CSR1(config-app-hosting)# name-server0 192.0.2.1
CSR1(config-app-hosting)# end

CSR1#guestshell enable
```

これでGuest Shellが有効になりました。
特権モードから`guestshell`を実行するとコンテナのbashが起動してログインできます。
試しにGuset Shellを起動してIOSdのコマンドを実行してみましょう。

```
CSR1#guestshell
[guestshell@guestshell ~]$ dohost "show app-hosting list"

App id                           State
------------------------------------------------------
guestshell                       RUNNING

[guestshell@guestshell ~]$ python
Python 2.7.5 (default, Jun 17 2014, 18:11:42) 
[GCC 4.8.2 20140120 (Red Hat 4.8.2-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import cli
>>> cli.cli("show app-hosting list")
'\nApp id                           State\n------------------------------------------------------\nguestshell                       RUNNING\n'
>>> exit()
[guestshell@guestshell ~]$
```

IOS-XEからGuest Shellのコマンドを実行するにはEEMを使用します。
`event timer watchdog` をトリガーとすることで、pythonスクリプトを定期的に実行することができます。

```
CSR1#conf t
CSR1(config)#event manager applet elasticsearch
CSR1(config-applet)#event timer watchdog time 30
CSR1(config-applet)#action 100 cli command "enable"
CSR1(config-applet)#action 200 cli command "guestshell run python /home/guestshell/monitoring.py"
```

##実行スクリプト
それぞれの監視項目に対応するshowコマンドから目的の文字列を抽出してディクショナリ型オブジェクトの値とし、キーはMappingと同様のものを使用します。
時刻はフォーマットの変換、メモリ使用率は値の計算が必要です。


- 時刻(date) -> `show clock`
- IOSdのCPU使用率(%) -> `show processes cpu | i CPU`
- kernelのCPU使用率(%) -> `show processes cpu platform | i CPU`
- IOSdのメモリ使用率(%) -> `show processes memory | i Processor`
- kernelのメモリ使用率(%) -> `show processes memory platform | i System memory`
- GigabitEthernet1の送受信量(bps) -> `show interface GigabitEthernet1`
- Interface/line protocolのステータス一覧(text) -> `show ip interface brief`
- ルーティングテーブル(text) -> `show ip route`

すべてのディクショナリ型オブジェクトを連結しJSONデータとしてElasticsearchに送信します。

コードは下記リンク先に載せておきます。
https://gist.github.com/ecodrive-18/f808df5a69b9a47e97288789cfa53c9d

実行前に必要なライブラリはインストールしておきましょう。

```
CSR1#guestshell 
[guestshell@guestshell ~]$ sudo pip install -r requirements.txt 
```

```:requirements.txt
elasticsearch>=6.0.0,<7.0.0
python-dateutil==2.8.1
```

##実行結果の確認と可視化
スクリプトの実行結果が格納できているかElasticsearchにクエリを投げてみます。

```
curl -XGET 'http://elasticsearch.example.com:9200/router-csr/monitoring/_search?size=1' | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2765  100  2765    0     0   128k      0 --:--:-- --:--:-- --:--:--  128k
{
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 232,
    "max_score": 1,
    "hits": [
      {
        "_index": "router-csr",
        "_type": "monitoring",
        "_id": "hHV2Xm4BlqMaqiTz5GDW",
        "_score": 1,
        "_source": {
          "GigabitEthernet1_output": "30000",
          "routing_table": "\nCodes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\n       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \n       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\n       E1 - OSPF external type 1, E2 - OSPF external type 2\n       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\n       ia - IS-IS inter area, * - candidate default, U - per-user static route\n       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\n       a - application route\n       + - replicated route, % - next hop override, p - overrides from PfR\nGateway of last resort is 172.16.0.254 to network 0.0.0.0\nS*    0.0.0.0/0 [254/0] via 172.16.0.254\n      10.0.0.0/32 is subnetted, 2 subnets\nO        10.0.0.254 [110/2] via 192.168.255.2, 19:32:27, GigabitEthernet2\nO        10.100.0.254 [110/2] via 192.168.255.2, 19:32:27, GigabitEthernet2\n      172.16.0.0/16 is variably subnetted, 3 subnets, 2 masks\nC        172.16.0.0/24 is directly connected, GigabitEthernet3\nL        172.16.0.207/32 is directly connected, GigabitEthernet3\nS        172.16.0.254/32 [254/0] via 172.16.0.254, GigabitEthernet3\n      192.168.0.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.0.0/24 is directly connected, GigabitEthernet1\nL        192.168.0.254/32 is directly connected, GigabitEthernet1\nO     192.168.1.0/24 [110/2] via 192.168.255.2, 19:34:13, GigabitEthernet2\n      192.168.35.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.35.0/24 is directly connected, VirtualPortGroup0\nL        192.168.35.1/32 is directly connected, VirtualPortGroup0\n      192.168.255.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.255.0/30 is directly connected, GigabitEthernet2\nL        192.168.255.1/32 is directly connected, GigabitEthernet2\n",
          "cpu_utilization_iosd": "0",
          "cpu_utilization_kernel": "22",
          "memory_utilization_kernel": "61",
          "@timestamp": "2019-11-12T16:14:30+09:00",
          "interface_status": "\nInterface              IP-Address      OK? Method Status                Protocol\nGigabitEthernet1       192.168.0.254   YES manual up                    up      \nGigabitEthernet2       192.168.255.1   YES manual up                    up      \nGigabitEthernet3       172.16.0.207    YES DHCP   up                    up      \nVirtualPortGroup0      192.168.35.1    YES manual up                    up      \n",
          "memory_utilization_iosd": "14",
          "GigabitEthernet1_input": "787000"
        }
      }
    ]
  }
}

```
hits.hits._sourceの中にCSR1から送られてきた情報がありました。
ちゃんと格納されていますね。

### Kibanaで可視化
Elasticsearchに蓄積した情報をKibanaで可視化してみます。
Visualizeで格納されたテキストをそのまま表示するという機能がなかなか見つからず...
CanvasのMarkdownにその機能がありそうだったので使用してみました。
コードブロックにテキストをそのまま表示させれば改行もされるのでいい感じですね。
もちろんグラフなども作成できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223892/a89c35fd-1e7f-87e5-2b50-58fc17c6c296.png" width=800>




##まとめ
IOS-XEのGuest Shellから機器の情報を送信して可視化してみました。
この機能が多くの機器に実装されれば、SNMPではなく監視Agentを用いた機器監視ができるかもしれませんね。
ちなみにPull型の監視を行う場合、Guest ShellにIOSdのVTYを取得させたままPythonを常駐プロセスとしなければならないので、EEMの`event`を設定する際に`maxrun`を0とする必要があります。