Cisco IOS-XEでルーティングテーブルの変化を捕捉して外部サービスに投稿

# これは何？
シスコルータやスイッチでサポートされているEEM（Embedded Event Manager）と[on-box python](http://qiita.com/kikuta1978/items/1739d7d7063a20b1736f)を使って、ルーティングテーブルの経路が変化したことをトリガーに、外部サービス（Cisco Spark）に通知するサンプル。

通知する内容は、変化したネットワーク、変化の内容（追加・削除など）、変化させた原因（OSPF、EIGRP、BGP、CONNECTEDなど）とし、これらがEEMにより提供される。EEMスクリプトはPythonで記述。装置は、手元にあったCatalyst 3850を利用。

デモ動画（youtubeが開きます）
[![IOS-XE EEM Routing ED with Python Script and notify to Cisco Spark](http://img.youtube.com/vi/7Hla36hkwt0/0.jpg)](http://www.youtube.com/watch?v=7Hla36hkwt0)

※参考
[Cisco Catalyst IOS-XEでのPythonやBash](http://qiita.com/kikuta1978/items/1739d7d7063a20b1736f#eem%E3%81%8B%E3%82%89%E5%AE%9F%E8%A1%8C)
[Cisco IOS EEMを使ったTclスクリプトの実行](http://qiita.com/kikuta1978/items/5d7d92e7f35d0a03922b)
[CatalystスイッチのコンテナからCisco Sparkに装置イベントを共有](http://qiita.com/kikuta1978/items/e520af66ccc47a87728f)

# 環境
## IOS-XEバージョン

```
Cat3850-3#sh ver | i .bin
System image file is "flash:cat3k_caa-universalk9.16.06.01.SPA.bin"
```

## 設定
EEMポリシーをPythonスクリプトで記述したものを、CatalystのFlashに設置。

```cisco:routewatch.py
Cat3850-3#dir | i routewatch.py
54679  -rw-             1043  Aug 29 2017 13:28:46 +09:00  routewatch.py
```

IOSコマンドの設定は以下の二行。EEMスクリプトのPath指定と、実際のスクリプト（後述）登録。下記の二行目を実行すると、スクリプトがコンパイルされてメモリ上で動作する。

```cisco
event manager directory user policy "flash:/"
event manager policy routewatch.py type user
```

## EEM Pythonスクリプト

```python:routewatch.py
::cisco::eem::event_register_routing network 1.1.1.0/24 type all ge 24
#::cisco::eem::event_register_routing network 1.1.1.0/24 type all ge 24 ratelimit 60

import requests
import sys
import eem

ACCESS_TOKEN = "<my_access_token>"
ROOM_ID = "<my_room_id>"

#ヘッダ作成
def setHeaders():
    accessToken_hdr = 'Bearer ' + ACCESS_TOKEN
    spark_header = {'Authorization': accessToken_hdr, 'Content-Type': 'application/json; charset=utf-8'}
    return spark_header

#SparkルームにメッセージをPost
def postMsg(the_header,roomId,message):
    message = '{"roomId":"' + roomId + '","text":"' + message +'"}'
    uri = 'https://api.ciscospark.com/v1/messages'
    resp = requests.post(uri, data=message, headers=the_header)
    print resp

#EEMイベントごとに内部で提供する情報を取得し、メッセージを作成
event = eem.event_reqinfo()
message = '!!! RoutingTable Change Detected by EEM: !!! -> ' + event['network'] + '-' + event['type'] + '-BY-' + event['protocol']
          
header=setHeaders()
postMsg(header,ROOM_ID,message)
```

以下、いくつかポイント。

- 一行目：Cisco EEM記法。ルーティングイベント検知を利用。1.1.1.0/24 および ge(great equal)は、24ビットマスクより長い経路変化にマッチ。1.1.1.Xはマッチし、2.2.2.2はマッチしない。
- type allは、add/remove/modifyすべてを検出。
- ルートフラップなど、連続的に発生した状態をすべて捕捉しないように、一定時間以内のイベントを丸めておくことは現実的に必要（ratelimitで60秒に設定する例を併記、今回は設定しない）。
- デフォルトでスクリプト動作は20秒を超えるとIOSからKILLされる（MAXRUN Timer）。あらかじめ処理時間が長いことが想定され、問題ない場合は一行目の最後に拡げておく記述をする（例：MAXRUN 300）。
- import eemで、IOS EEM機能が提供するライブラリを使える -> eem.event_reqinfo()
- event_reqinfo()により、イベントごとに発生した内容を内部変数として配列を自動生成する。この例では、event['network']、event['type']、event['protocol']に活用。
- Cisco Sparkへの投稿は[こちら](http://qiita.com/kikuta1978/items/e520af66ccc47a87728f)を参照。


# 動作確認

## 1. 初期状態 -> OSPFで 1.1.1.200 を追加

```cisco:Before
Cat3850-3#sh ip route
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      1.0.0.0/32 is subnetted, 2 subnets
C        1.1.1.1 is directly connected, Loopback100
O        1.1.1.100 [110/2] via 192.168.1.2, 01:10:05, Vlan1
      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.1.0/24 is directly connected, Vlan1
L        192.168.1.1/32 is directly connected, Vlan1
```

```cisco:After1
Cat3850-3#sh ip route
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      1.0.0.0/32 is subnetted, 3 subnets
C        1.1.1.1 is directly connected, Loopback100
O        1.1.1.100 [110/2] via 192.168.1.2, 01:11:55, Vlan1
O        1.1.1.200 [110/2] via 192.168.1.2, 00:00:34, Vlan1 <---★★追加された
      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.1.0/24 is directly connected, Vlan1
L        192.168.1.1/32 is directly connected, Vlan1
```

### Sparkに投稿された内容：

- 変化した経路：1.1.1.200
- タイプ：add
- プロトコル：OSPF

![SS 2017-08-29 15.40.18.png](https://qiita-image-store.s3.amazonaws.com/0/157325/6acd4ffc-9391-1f1a-e9f6-befdd3ebff08.png)

## 2. OSPF更新によりテーブルから 1.1.1.100 が消失

```cisco:After2
Cat3850-3#sh ip route
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      1.0.0.0/32 is subnetted, 2 subnets
C        1.1.1.1 is directly connected, Loopback100
O        1.1.1.200 [110/2] via 192.168.1.2, 00:03:21, Vlan1
      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.1.0/24 is directly connected, Vlan1
L        192.168.1.1/32 is directly connected, Vlan1
```

### Sparkに投稿された内容：

- 変化した経路：1.1.1.100
- タイプ：remove
- プロトコル：OSPF

![SS 2017-08-29 15.42.19.png](https://qiita-image-store.s3.amazonaws.com/0/157325/fe1d7795-fff1-fac4-77c0-e3cad5d4821e.png)

## 3. 装置上のLoopback設定追加によりテーブルに 1.1.1.30 が追加

```cisco:After3
Cat3850-3#sh ip route
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      1.0.0.0/32 is subnetted, 3 subnets
C        1.1.1.1 is directly connected, Loopback100
C        1.1.1.30 is directly connected, Loopback300 <---★★追加された
O        1.1.1.200 [110/2] via 192.168.1.2, 00:05:57, Vlan1
      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.1.0/24 is directly connected, Vlan1
L        192.168.1.1/32 is directly connected, Vlan1
```

### Sparkに投稿された内容：

- 変化した経路：1.1.1.30
- タイプ：add
- プロトコル：connected

![SS 2017-08-29 15.44.52.png](https://qiita-image-store.s3.amazonaws.com/0/157325/9e283d49-3f9b-9c82-2a5e-c03d92d60936.png)

※Syslog的な感じではあるが、短いスクリプトで、中身は動的に補完されるところがポイント。

# EEMアプレット（CLI）での設定例

CLIでも似たようなことは可能。以下はあらゆる経路変更を検出するイベントの書き方と、イベントが生成する変数をSyslogで出力する例。

```cisco:IOS-Config
event manager applet routewatch-applet
 event routing network 0.0.0.0/0 type all
 action 101 syslog msg "_event_type_string is $_event_type_string"
 action 102 syslog msg "_routing_network is $_routing_network"
 action 103 syslog msg "_routing_protocol is $_routing_protocol"
 action 104 syslog msg "_routing_type is $_routing_type"
```

```cisco:動作例
Cat3850-3#
Aug 29 16:07:09.331: %HA_EM-6-LOG: routewatch-applet: _event_type_string is routing
Aug 29 16:07:09.331: %HA_EM-6-LOG: routewatch-applet: _routing_network is 1.1.1.100
Aug 29 16:07:09.331: %HA_EM-6-LOG: routewatch-applet: _routing_protocol is OSPF
Aug 29 16:07:09.331: %HA_EM-6-LOG: routewatch-applet: _routing_type is add
```

# event_reqinfo() とは？
IOS EEMでは、さまざまなイベントを検出してアクションを実行できるが、本例のようにルーティング変化を捕捉するだけでなく、変化した内容が内部的に提供されるため便利。

イベントごとに生成する変数（配列）を確認する便利なコマンドがある（オンラインマニュアルよりも便利）。以下のコマンド出力では、EEMイベントの記法に加え、Tcl event_reqinfo Array が確認できるが、本サンプルコードのようにPythonスクリプト内でも活用できる。

もちろん、アプレット（CLI）でも値を取り出し、Syslogやメールでの送信や、内部ファイルへの書き込み、ちょっとした分岐やループ処理も可能。

```
Cat3850-3#show event manager detector routing detailed 
No.  Name                Version   Node        Type    
1    routing             03.00     node0/0     RP      

	Tcl Configuration Syntax: 
	::cisco::eem::event_register_routing
		 [tag <tag-val>] 
		 network <network>/<length> 
		 [ge <ge-length>]
		 [le <le-length>]
		 [ne <ne-length>]
		 [type {add | remove | modify | all}]
		 [protocol <protocol-val>]
		 [queue_priority {normal | low | high | last}] 
		 [maxrun <sec.msec>]
		 [ratelimit <sec.msec>]
		 [vrf {all | default | name=regex}]
		 [nice {0 | 1}]

	Tcl event_reqinfo Array Names: 
	event_id
	job_id
	event_type
	event_type_string
	event_pub_time
        event_pub_sec
	event_pub_msec
	event_trigger_num
	event_severity
	network 
	mask 
	prefix_len 
	protocol 
	type 
	lastgateway 
	distance 
	time 
	time_sec 
	time_msec 
	metric 
	afi 
	lastinterface 

	Applet Configuration Syntax: 
	[ no ] event [tag <tag-val>] routing 
		 network <network>/<length> 
		 [ge <ge-length>]
		 [le <le-length>]
		 [ne <ne-length>]
                 [type {add | remove | modify | all}]
		 [protocol <protocol-val>]
		 [maxrun <sec.msec>]
		 [ratelimit <sec.msec>]
		 [vrf {all | default | name <regex>}]

	Applet Built-in Environment Variables: 
	$_event_id
	$_job_id
	$_event_type
	$_event_type_string
	$_event_pub_time
	$_event_pub_sec
	$_event_pub_msec
	$_event_severity
	$_routing_network 
	$_routing_mask 
	$_routing_prefix_len 
	$_routing_protocol 
	$_routing_type 
	$_routing_tag_name 
	$_routing_vrf_name 
	$_routing_topo_name 
	$_routing_lastgateway 
        $_routing_distance 
	$_routing_time 
	$_routing_time_sec 
	$_routing_time_msec 
	$_routing_metric 
	$_routing_lastinterface 
	$_routing_afi 
```

以下、Cat3850のIOS-XE 16.6で使えるイベントごとに、event_reqinfo が確認できるので、何かと眺めていると想像力が膨らみます。

```
Cat3850-3#show event manager detector ?                
  all                 All available event detectors
  application         Application event detector
  cli                 CLI event detector
  config              Config event detector
  counter             Counter event detector
  env                 Environmental event detector
  generic             Generic event detector
  gold                GOLD event detector
  identity            Identity event detector
  interface           Interface event detector
  ioswdsysmon         Ioswdsysmon event detector
  ipsla               IPSLA event detector
  mat                 mac-address-table event detector
  neighbor-discovery  neighbor discovery event detector
  nf                  NF event detector
  none                None event detector
  oir                 OIR event detector
  rf                  RF event detector
  routing             Routing event detector
  rpc                 RPC event detector
  snmp                Snmp event detector
  snmp-notification   Snmp notification event detector
  snmp-object         Snmp Object event detector
  syslog              Syslog event detector
  test                Test event detector
  timer               Timer event detector
```

NetFlowキャッシュやMACアドレステーブルのモニタリング、CDP/LLDPによる接続の検出など、楽しそうです。

```
Cat3850-3#show event manager detector nf detailed 
No.  Name                Version   Node        Type    
1    nf                  01.00     node0/0     RP      

	Tcl Configuration Syntax: 
	::cisco::eem::event_register_nf 
		 [tag <tag-val>] 
		 monitor_name <monitor-name value>
		 event_type <create|update|delete>
		 exit_event_type <create|update|delete>
		 event1-event4 <subevent-description>
		 [maxrun <sec.msec>]
		 [ratelimit <sec.msec>]
		 [nice {0 | 1}]

		 where <subevent-description> can be 
		 field <field value>
		 rate_interval <rate interval value> (event1 only)
		 entry_value <entry value>
		 entry_op {eq|ge|gt|le|lt|wc}
		 [exit_value <exit value>]
		 [exit_op {eq|ge|gt|le|lt|wc}]
		 [exit_rate_interval <exit rate interval value>] (event1 only)

	Tcl event_reqinfo Array Names: 
	event_id
	job_id
	event_type
	event_type_string
	event_pub_time
	event_pub_sec
	event_pub_msec
	event_trigger_num
	event_severity
	monitor_name 
        event_type
	ip_protocol
	source_address
	source_port
	dest_address
	dest_port
	app_name
	event[1-4]_field
	event[1-4]_value

	Applet Configuration Syntax: 
	 [ no ] event [tag <tag-val>] nf 
		 monitor-name <monitor-name value>
		 event-type <create|update|delete>
		 exit-event-type <create|update|delete>
		 event1-event4 <subevent-description>
		 [maxrun <sec.msec>]
		 [ratelimit <sec.msec>]

		 where <subevent-description> can be 
		 field <field value>
		 rate-interval <rate interval value> (event1 only)
		 entry-value <entry value>
		 entry-op {eq|ge|gt|le|lt|wc}
		 [exit-value <exit value>]
		 [exit-op {eq|ge|gt|le|lt|wc}]
		 [exit-rate-interval <exit rate interval value>] (event1 only)

	Applet Built-in Environment Variables: 
	$_event_id
	$_job_id
	$_event_type
	$_event_type_string
	$_event_pub_time
        $_event_pub_sec
	$_event_pub_msec
	$_event_severity
	$_nf_monitor_name 
	$_nf_event_type
	$_nf_ip_protocol
	$_nf_source_address
	$_nf_source_port
	$_nf_dest_address
	$_nf_dest_port
	$_nf_app_name
	$_nf_event[1-4]_field
	$_nf_event[1-4]_value
```

# まとめ
長くなりましたが、まとめると Cisco IOSのevent_reqinfo()を眺めてると楽しいです、っていう記事でした。Off-boxでやると面倒なことを、On-box programmabilityが補完できるといいですね。Cisco 1812Jや892JでもEEMやevent_reqinfo()は試せると思います。EEMをPythonで書こうとすると、IOS-XE16.5以上が必要なので、今のところCat3850やISR4000/ASR1000といった装置が必要です。

# 参考
[Embedded Event Manager Configuration Guide, Cisco IOS Release 15M&T](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/eem/configuration/15-mt/eem-15-mt-book.html)
[Programmability Configuration Guide, Cisco IOS XE Everest 16.6.1](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/166/b_166_programmability_cg/eem_python_module.html)