IOS XEのModel Driven Telemetryを試してみた


# はじめに

この記事はシスコの有志による[Cisco Systems Japan Advent Calendar 2017](https://qiita.com/advent-calendar/2017/cisco)の21日目です。

IOS XE 16.6.1からModel Driven Telemetryをサポートしましたので、今回はこれを試してみたいと思います。

* [Programmability Configuration Guide, Cisco IOS XE Everest 16.6.x: Model-Driven Telemetry](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/166/b_166_programmability_cg/model_driven_telemetry.html)

ちょうど昨日のCisco Advent Calender 20日目で@muramurakamiさんがIOS XRのTelemetryに関連した投稿をしてくれているので、そちらもぜひ参考にしてみてください。

* [PipelineでXRのTelemetryデータを取得してみた](https://qiita.com/muramurakami/items/1645ae6a48ef74d60e50)

# Model Driven Telemetry

Model Driven Telemetryは、（IETF NETCONF YANG Push[^1]やOpenConfig Streaming Telemetry[^2]のように）Push型かつストリーミング方式でリアルタイムにデバイスからコンフィグおよびステートの情報を取得するためのテクノロジー一般を指すCisco用語です。モデル駆動（Model Driven）と冠するだけあって、ネットワークデバイスから提供される情報がYANGでモデル化されています。IOS XEとIOS XRでは、（コーデックやトランスポート等の）実装方式が異なるのですが、いずれはOpenConfig Streaming Telemetryなど、現在サポートされていない仕組みも取り込んでいくことも見据えて、このような包括的なネーミングになってます。

[^1]: https://datatracker.ietf.org/doc/draft-ietf-netconf-yang-push/
[^2]: http://www.openconfig.net/projects/telemetry/

IOS XRにおけるModel Driven Telemetryの実装は、OpenConfig Streaming Telemetryの影響を反映しているのに対して、IOS XEにおけるModel Driven Telemetryのそれは、標準化作業中のIETF NETCONF YANG Pushドラフトを基調としてます。

# 前提

この記事では、IOS XEプラットフォームとして、すでにModel Driven Telemetryを正式にサポートしているCatalyst系のプラットフォームではなく、CSR1000Vのベータイメージで試しています :bow:
近いうちに、ルータ系のプラットフォームでも正式にサポートされるはずです :yum:

# デバイスの設定

YANGに対応したNETCONFを有効にします。

```
Router(config)#netconf-yang
```

さらに、NETCONFでアクセスするためのアカウントを作成します。`privilege`レベルは`15`にする必要があります。

```
Router(config)#username admin privilege 15 password admin
```

今回はCPU使用率しか取得しないので、以下の設定は不要ですが、取得したいデータに応じて、デバイス内部における（IOS上で動作する）NETCONFエージェント（実態はConfD[^3]）とIOS内部のデータストアを同期する方式と間隔を設定します。一見分かりづらいのですが、以下のような関係にあります。NETCONFクライアントはNETCONFエージェントと会話しますが、ここで設定しているのは、IOSデバイス内部におけるNETCONFエージェントとIOSデータストアの関係です。

[NETCONFクライアント]----------[NETCONFエージェント(ConfD)]----------[IOSデータストア]

[^3]: https://developer.cisco.com/site/confD/

```
Router(config)#netconf-yang cisco-odm polling-enable
Router(config)#netconf-yang cisco-odm actions OSPF 
Router(config-odm-action)#mode poll 
Router(config-odm-action)#polling-interval 1000
```

（具体的にどういった情報を取得できるか等、）詳細は以下のドキュメントをご参照ください。

* [Programmability Configuration Guide, Cisco IOS XE Everest 16.6.x: Configuring YANG Datamodel](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/166/b_166_programmability_cg/configuring_yang_datamodel.html)
* [Programmability Configuration Guide, Cisco IOS XE Everest 16.6.x: Operational Data Parser Polling](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/167/b_167_programmability_cg/operational_data_parser_polling.html?dtid=osscdc000283)

# 素のSSHで試す

まずは、仕組みを知る上で、素のSSHで接続して試します（以下、デバイスとクライアントの間でやりとりしているXMLメッセージは、可読性をよくするため整形してます）。

クライアントからデバイスにSSHします。重要な点として、`-s`オプションで`subsystem`として`netconf`を指定します。

```
$ ssh admin@192.0.2.1 -p 830 -s netconf
admin@192.0.2.1's password:*****
```

接続すると、デバイスから`<hello>`が返ってきます。`<capability>`の量が膨大なので省略してます。

__client <- device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.0</capability>
    <capability>urn:ietf:params:netconf:base:1.1</capability>
    ...
  </capabilities>
  <session-id>237955</session-id>
</hello>]]>]]>
```

クライアントからも`<hello>`を送信します。

__client -> device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.0</capability>
  </capabilities>
</hello>]]>]]>
```

続けて、クライアントから`<establish-subscription>`オペレーションを送信します。このときに、`xpath-filter`で、取得する情報の対象を特定するのと、`period`で情報をPushしてもらう間隔をセンチ秒（1/100秒）単位で指定します。以下の例では、5秒間のCPU使用率に関する情報（`/ios-emul-oper-db:ios-emul-oper-db/cpu-usage/five-seconds`）が10秒間隔でデバイスからPushされることになります。

__client -> device__

```
<?xml version="1.0" encoding="utf-8"?>
<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <establish-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-event-notifications" xmlns:yp="urn:ietf:params:xml:ns:yang:ietf-yang-push">
    <stream>yp:yang-push</stream>
    <yp:xpath-filter>/ios-emul-oper-db:ios-emul-oper-db/cpu-usage/five-seconds</yp:xpath-filter>
    <yp:period>1000</yp:period>
  </establish-subscription>
</rpc>]]>]]>
```

`<establish-subscription>`オペレーションに問題がなければ、以下のような`<rpc-reply>`がデバイスから返ってきます。
このとき通知される`<subscription-id>`は、のちにSubscriptionを停止する際に必要になります。

__client <- device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101"><subscription-result xmlns='urn:ietf:params:xml:ns:yang:ietf-event-notifications' xmlns:notif-bis="urn:ietf:params:xml:ns:yang:ietf-event-notifications">notif-bis:ok</subscription-result>
  <subscription-id xmlns='urn:ietf:params:xml:ns:yang:ietf-event-notifications'>2147483648</subscription-id>
</rpc-reply>]]>]]>
```

以降、`period`で指定した間隔で、以下のような情報が継続的にPushされてきます。

__client <- device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0"><eventTime>2017-12-15T05:01:44.32Z</eventTime>
  <push-update xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push">
    <subscription-id>2147483648</subscription-id>
    <datastore-contents-xml>
      <ios-emul-oper-db xmlns="http://cisco.com/ns/iosxe/ios-emul-oper-db">
        <cpu-usage>
          <five-seconds>0</five-seconds>
        </cpu-usage>
      </ios-emul-oper-db>
    </datastore-contents-xml>
  </push-update>
</notification>
```

一応、デバイス側でSubscriptionの状態を確認しておきましょう。

```
Router#show telemetry ietf subscription all
  Telemetry subscription brief

  ID               Type        State       Filter type
  -----------------------------------------------------
  2147483648       Dynamic     Valid       xpath
```

`<subscription-id>`を指定することでセッション毎の詳細を確認することができます。

```
Router#show telemetry ietf subscription 2147483714 detail
Telemetry subscription detail:

  Subscription ID: 2147483648
  Type: Dynamic
  State: Valid
  Stream: yang-push
  Filter:
    Filter type: xpath
    XPath: /ios-emul-oper-db:ios-emul-oper-db/cpu-usage/five-seconds
  Update policy:
    Update Trigger: periodic
    Period: 1000
  Encoding: encode-xml
  Source VRF:
  Source Address:
  Notes:
```

`<delete-subscription>`オペレーションでSubscriptionを停止します。`<subscription-id>`には、 `<establish-subscription>`オペレーションの際に通知されらものを使用します。

__client -> device__

```xml
<?xml version="1.0" encoding="utf-8"?>
<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <delete-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-event-notifications" xmlns:netconf="urn:ietf:params:xml:ns:netconf:base:1.0">
    <subscription-id>2147483648</subscription-id>
  </delete-subscription>
</rpc>]]>]]>
```

`<delete-subscription>`オペレーションに問題がなければ、以下のような`<rpc-reply>`がデバイスから返ってきます。

__client <- device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
  <subscription-result xmlns='urn:ietf:params:xml:ns:yang:ietf-event-notifications' xmlns:notif-bis="urn:ietf:params:xml:ns:yang:ietf-event-notifications">notif-bis:ok</subscription-result>
</rpc-reply>]]>]]>
```

最後に、`<close-session>`オペレーションでNETCONFセッション自体を終了します。

__client -> device__

```xml
<?xml version="1.0" encoding="utf-8" ?>
<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <close-session/>
</rpc>]]>]]>
```

デバイスからは以下のような`<rpc-reply>`が返って、SSHセッションが終了します。

__client <- device__

```xml
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101"><ok/>
</rpc-reply>]]>]]>
```

# Rubyで試す

次に、プログラミングを用いた例として、Rubyで試してみます。なぜRubyなのかといえば、最後に紹介するようにFluentdのプラグインを作りたかったからです。

サンプルコードは以下のとおりです。後述する理由により、`subscription-id`を取得するためにXMLメッセージをパースすると、割と込み入ったサンプルコードになってしまうため、`<delete-subscription>`オペレーションと`<close-session>`オペレーションをしないままで、SSHセッションを終了してます。showコマンド見る限り、SSHセッション自体を終了すれば、それに紐づくNETCONF YANG PushのSubscriptionはIOS上では解放されているようでしたが、プロダクション環境ではくれぐれもマネしないでください :sweat_smile:

```rb
require 'net/ssh'

int_pressed = false

trap :INT do
  int_pressed = true
end

hello = <<-EOS
<?xml version="1.0" encoding="utf-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.0</capability>
  </capabilities>
</hello>]]>]]>
EOS

establish_subscription = <<-EOS
<?xml version="1.0" encoding="utf-8"?>
<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <establish-subscription xmlns="urn:ietf:params:xml:ns:yang:ietf-event-notifications" xmlns:yp="urn:ietf:params:xml:ns:yang:ietf-yang-push">
    <stream>yp:yang-push</stream>
    <yp:xpath-filter>/ios-emul-oper-db:ios-emul-oper-db/cpu-usage/five-seconds</yp:xpath-filter>
    <yp:period>1000</yp:period>
  </establish-subscription>
</rpc>]]>]]>
EOS

Net::SSH.start("192.0.2.1", "admin", :port => 830, :password => "admin", :timeout => 10) do |ssh|
  ssh.open_channel do |channel|
    channel.subsystem("netconf") do |ch, success|
      raise "subsystem could not be started" unless success
      ch.on_data do |ch, data|
        puts "on data..."
        puts data
      end
      ch.on_close do |ch|
        puts "on close ..."
      end
      ch.on_eof do |ch|
        puts "on eof ..."
      end
      puts "send hello ..."
      ch.send_data(hello)
      puts "send subscription ..."
      ch.send_data(establish_subscription)
    end
  end

  ssh.loop(1) { not int_pressed }
  puts "close ssh ..."
  ssh.close
end
```

`subsystem`を`netconf`にする以外は、通常のnet-sshの使い方のとおりです。

実行すると、以下のような結果になります。`<hello>`の中身は今回も省略してます。

```
send hello ...
send subscription ...
on data...
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
<capability>urn:ietf:params:netconf:base:1.1</capability>
...
</capabilities>
<session-id>30628</session-id></hello>]]>]]><?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101"><subscription-result xmlns='urn:ietf:params:xml:ns:yang:ietf-event-notifications' xmlns:notif-bis="urn:ietf:params:xml:ns:yang:ietf-event-notifications">notif-bis:ok</subscription-result>
<subscription-id xmlns='urn:ietf:params:xml:ns:yang:ietf-event-notifications'>2147483653</subscription-id>
</rpc-reply>]]>]]>
on data...
<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0"><eventTime>2017-12-19T04:00:51.70Z</eventTime><push-update xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push"><subscription-id>2147483653</subscription-id><datastore-contents-xml><ios-emul-oper-db xmlns="http://cisco.com/ns/iosxe/ios-emul-oper-db"><cpu-usage><five-seconds>0</five-seconds></cpu-usage></ios-emul-oper-db></datastore-contents-xml></push-update></notification>
]]>]]>
on data...
<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0"><eventTime>2017-12-19T04:01:01.70Z</eventTime><push-update xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push"><subscription-id>2147483653</subscription-id><datastore-contents-xml><ios-emul-oper-db xmlns="http://cisco.com/ns/iosxe/ios-emul-oper-db"><cpu-usage><five-seconds>0</five-seconds></cpu-usage></ios-emul-oper-db></datastore-contents-xml></push-update></notification>
on data...
]]>]]>
on data...
<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0"><eventTime>2017-12-19T04:01:11.70Z</eventTime><push-update xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-push"><subscription-id>2147483653</subscription-id><datastore-contents-xml><ios-emul-oper-db xmlns="http://cisco.com/ns/iosxe/ios-emul-oper-db"><cpu-usage><five-seconds>1</five-seconds></cpu-usage></ios-emul-oper-db></datastore-contents-xml></push-update></notification>
]]>]]>
...
```

実行結果を注意深くみると分かりますが、一つのRPCメッセージが大きいと、デバイスからは複数回に分断されてメッセージが届きます（つまり、複数回on_dataが呼び出されます）。サンプルコードの複雑化を避けるため今回は割愛しましたが、XML形式の文字列をパースしたいときは、何らかの変数にいったん格納してから、デリミタ（`]]>]]>`）が現れたタイミングでまるっとパースする等の工夫が必要です。

# おまけ 

記事の中でも前振りしたとおり、IOS XEのModel Driven Telemetry用にFluentdのInputプラグインを作ってみましたー :sushi: 
具体的な使い方は、近日、別な記事で紹介するようにします！

* [Github: tetsusat/fluent-plugin-telemetry-iosxe](https://github.com/tetsusat/fluent-plugin-telemetry-iosxe)
* [RubyGems: tetsusat/fluent-plugin-telemetry-iosxe](https://rubygems.org/gems/fluent-plugin-telemetry-iosxe)

おしまい