LGTM

Looks Good To Me

やってみよう NETCONF

知ったかぶりしない NETCONF に続く, NETCONF エントリーです.

今回はNETCONF の実装をいくつか試す.

前のエントリーでは「NETCONF 便利そうだけど, データモデル*1 が共通じゃないので使うのつらそう. 特にいろんな種類のものがある場合は」というようなことを書いた.

一方で「できる範囲でいいから物理ルーター/スイッチを自動管理したい」ニーズはあるので, 今回は「現状どんなことができるの?」を確認しようと思う.

たくさん試したわけではないので, 他に便利アプリがあるかもしれない.
オススメ情報があったらぜひ教えてください.

管理対象 (Netconf Server)

今回はJuniper Firefly Perimeter (junos-vsr-12.1X46-D-10-domestic) を使ってみる.

A Ruby gem for NETCONF

実装されている機能

  • Extensible protocol transport framework for SSH and non-SSH

  • NETCONF Standard RPCs

    • get-config, edit-config
    • lock, unlock
    • validate, discard-changes
  • Flexible RPC mechanism

    • Netconf::RPC::Builder to metaprogram RPCs
    • Vendor extension framework for custom RPCs

( 引用: https://github.com/Juniper/net-netconf )

  • RPC mechanism はモジュールによって抽象化されているが, 実装はJuniper 用のみ
    • ユーザーが拡張してCisco に対応することは可能っぽい
    • “netconf” っていう名前のgem なんだけど, たとえばCisco 対応PR 書いたら 受け付けてくれんのかな…

使用例

インストール
$ gem install netconf
設定取得

password は環境変数に入ってるとして,

require 'net/netconf'

Netconf::SSH.new(target: '192.168.0.74', username: 'codeout', password: ENV['PASSWORD']) do |device|
  config = device.rpc.get_config {|x|  # 以下の <rpc> message を送信
    x.configuration {
      x.system {
        x.services
      }
    }
  }
  
  # <rpc>
  #   <get-config>
  #     <source>
  #       <running/>
  #     </source>
  #     <filter type="subtree">
  #       <configuration>
  #         <system>
  #           <services/>
  #         </system>
  #       </configuration>
  #     </filter>
  #   </get-config>
  # </rpc>
  
  puts config  # 以下の設定が取得できる (Nokogiri::XML::Element オブジェクト)
  
  # <data>
  #   <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1413833291"
  #     junos:commit-localtime="2014-10-20 19:28:11 UTC" junos:commit-user="codeout">
  #     <system>
  #       <services>
  #         <ssh>
  #         </ssh>
  #         <netconf>
  #           <ssh>
  #           </ssh>
  #         </netconf>
  #         <web-management>
  #           <http>
  #             <interface>ge-0/0/0.0</interface>
  #           </http>
  #         </web-management>
  #       </services>
  #     </system>
  #   </configuration>
  # </data>
  
  puts config.xpath('//services/netconf').to_xml  # xpath を用いてフィルターできる
  
  # <netconf>
  #   <ssh>
  #   </ssh>
  # </netconf>
end
  • 動く
  • パラメーター(<filter>) は, JUNOS 設定の構造をXML で表現したもの
  • 出力も JUNOS 構造そのままのXML

使うには JUNOS 設定の構造を知っていなければならない.

状態取得
require 'net/netconf'

Netconf::SSH.new(target: '192.168.0.74', username: 'codeout', password: ENV['PASSWORD']) do |device|
  puts device.rpc.get_firmware_information  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <get-firmware-information/>
  # </rpc>
  
  # 以下の状態が取得できる
  
  # <firmware-information xmlns="http://xml.juniper.net/junos/12.1X46/junos-chassis">
  #   <chassis junos:style="firmware">
  #     <chassis-module>
  #       <name>FPC</name>
  #       <firmware>
  #         <type>O/S</type>
  #         <firmware-version>Version 12.1X46-D10 by builder on 2013-12-05 15:05:41 UTC</firmware-version>
  #       </firmware>
  #     </chassis-module>
  #     <chassis-module>
  #       <name>FWDD</name>
  #       <firmware>
  #         <type>O/S</type>
  #         <firmware-version>Version 12.1X46-D10 by builder on 2013-12-05 15:05:41 UTC</firmware-version>
  #       </firmware>
  #     </chassis-module>
  #   </chassis>
  # </firmware-information>
  
  puts device.rpc.get_interface_information(interface_name: 'ge-0/0/0', terse: true)
  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <get-interface-information>
  #     <interface-name>ge-0/0/0</interface-name>
  #     <terse/>
  #   </get-interface-information>
  # </rpc>
  
  # 以下の状態が取得できる
  
  # <interface-information xmlns="http://xml.juniper.net/junos/12.1X46/junos-interface" junos:style="terse">
  #   <physical-interface>
  #     <name>
  #       ge-0/0/0
  #     </name>
  #     <admin-status>
  #       up
  #     </admin-status>
  #     <oper-status>
  #       up
  #     </oper-status>
  #     <logical-interface>
  #       <name>
  #         ge-0/0/0.0
  #       </name>
  #       <admin-status>
  #         up
  #       </admin-status>
  #       <oper-status>
  #         up
  #       </oper-status>
  #       <filter-information>
  #       </filter-information>
  #       <address-family>
  #         <address-family-name>
  #           inet
  #         </address-family-name>
  #         <interface-address>
  #           <ifa-local junos:emit="emit">
  #             192.168.0.74/24
  #           </ifa-local>
  #         </interface-address>
  #       </address-family>
  #     </logical-interface>
  #   </physical-interface>
  # </interface-information>
end
  • 動く
  • パラメーターは, JUNOS XML API そのまま
  • 出力は JUNOS 仕様のXML

  • method-missing のしくみを使って 動的にXML を生成

    • JUNOS XML API に対応するメソッドがいちいち存在するわけではない
    • ヘルパー的な便利動作は少ない
2014-11-04 追記

show コマンドが分かっていれば | display xml rpcXML を調べることができる」と教えてもらいました.

例:

> show chassis firmware | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/12.1X46/junos">
    <rpc>                            #
        <get-firmware-information>   # ここを使う
        </get-firmware-information>  #
    </rpc>                           #
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>
設定変更
require 'net/netconf'

Netconf::SSH.new(target: '192.168.0.74', username: 'codeout', password: ENV['PASSWORD']) do |device|
  puts device.rpc.lock(:candidate)  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <lock>
  #     <target>
  #       <candidate/>
  #     </target>
  #   </lock>
  # </rpc>
  
  # 結果
  
  # <ok/>
  
  puts device.rpc.edit_config {|x|  # 以下の <rpc> message を送信
    x.configuration {
      x.interfaces {
        x.interface {
          x.name 'ge-0/0/0'
          x.description 'foo'
        }
      }
    }
  }
  
  # <rpc>
  # <edit-config>
  #    <target><candidate/></target>
  #    <config><configuration><interfaces><interface><name>ge-0/0/0</name><description>foo</description></interface></interfaces></configuration></config>
  # </edit-config>
  # </rpc>
  
  # 結果
  
  # <ok/>
  
  puts device.rpc.validate(:candidate)  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <validate>
  #     <source>
  #       <candidate/>
  #     </source>
  #   </validate>
  # </rpc>
  
  # 結果
  
  # <commit-results>
  # </commit-results>
  
  puts device.rpc.commit  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <commit/>
  # </rpc>
  
  # 結果
  
  # <ok/>
  
  puts device.rpc.unlock :candidate  # 以下の <rpc> message を送信
  
  # <rpc>
  #   <unlock>
  #     <target>
  #       <candidate/>
  #     </target>
  #   </unlock>
  # </rpc>
  
  # 結果
  
  # <ok/>
end
  • 動く
  • パラメーターは, JUNOS 設定の構造をXML で表現したもの
  • 変更対象を指定するためのキーを知っておく必要がある
    • たとえば, <interface> を特定するためのキーは<name>

想像だけでXML を作るのは難しい. 戦略としては次のような感じになると思う.

  1. <get-config> して設定を取得
  2. キーを推測
    • <get-xnm-information> しておけばXSD が手に入る -> 2. は不要
  3. 変更したい部分をXML で記述

結局, これは何?

  • NETCONF のwrapper

    • NETCONF の 3+1 レイヤー*2のうち
      • Transport レイヤー(SSH/TELNET) を抽象化してくれる
      • Operations レイヤー, Content レイヤーは抽象化してくれない
        • 入出力とも デバイス種別 + メーカー依存
        • 現時点ではどうしようもないので, ユーザーが抽象化するしかない -> 面倒くさい!!
    • <get-config>, <edit-config>, <validate>, <commit>, <lock>, <unlock> 用のヘルパーメソッド
    • Nokogiri によるXML DOM 読み書き
    • ちゃんと動く
  • perl, php, go 実装もある

  • CLI をparse するより安全そう

    • CLI 出力とは違って, 構造化されているし エラー捕捉が簡単
    • NETCONF 対応デバイスだけであれば, データモデルが違っても頑張れる気がする
    • 「NETCONF 非対応デバイスとの共存期間」問題は残る

Netopeer

  • https://code.google.com/p/netopeer/
  • New BSD License
  • アーキテクチャ
    • netopeer-cli / gui
    • netopeer-agent
    • netopeer-server

    の3つから成る. IAサーバーなどでnetopeer-agent + netopeer-server を常駐させ, netopeer-cli / gui からのSSHを終端する. その上でnetopeer-cli / gui (NETCONF クライアント) からnetopeer-server(NETCONF サーバー) 経由でIA サーバーを管理する.

f:id:codeout:20141030200429p:plain

( 引用: https://code.google.com/p/netopeer/ )

管理対象として 現時点ではLinux をターゲットにしているようだけれど「NETCONF に準拠していれば ルーターでもある程度動くんでは?」という淡い期待のもと, Netopeer-GUI を試してみた.

実装されている機能

  • persistent connection to multiple NETCONF servers via backend process
  • presentation of state and configuration information from datastores using YIN (/Yang) models
  • creation of configuration with advisor of possible information in current context
  • asynchronous NETCONF notifications using HTTP websockets
  • calling model defined RPC methods

( 引用: https://code.google.com/p/netopeer/ )

  • “persistant connection” をどうやって実現しているかというと, mod_netconf というapache module が常駐してSSH を終端
  • GUI 本体はphp + symfony + DB

これが動いたらすごいですよね!

GitHub wiki を見てもらえると, ワクワク感が伝わるかもしれません.

使用例

インストール

Debian jessie にインストールした.

をソースからビルドする.

ビルド用スクリプトにパッチしたり, Firefly 向けにNETCONF capability の名前空間を変えたりする必要があったが, 今回のポイントではないので省略.

バイスを登録

Firefly は, SSH ポート(22/tcp) もしくはNETCONF ポート(830/tcp) で登録できる.

f:id:codeout:20141030200446p:plain

きちんとcapability の交換ができている.

f:id:codeout:20141030200500p:plain

それ以上, ぜんぜん動かない…

データモデルに差があるとはいえ「初手では動かないだろうけど, 送るXML のテンプレート + XML を読むparser に手を入れたら動くだろう」くらいに思ってた… が, かなりつらい.

なにがつらいかというと, NETCONF の3+1 レイヤーのうち

           Layer                 Example
      +-------------+      +-----------------+      +----------------+
  (4) |   Content   |      |  Configuration  |      |  Notification  |
      |             |      |      data       |      |      data      |
      +-------------+      +-----------------+      +----------------+
             |                       |                      |          YANG
----------------------------------------------------------------------------
             |                       |                      |       NETCONF
      +-------------+      +-----------------+              |
  (3) | Operations  |      |  <edit-config>  |              |
      |             |      |                 |              |
      +-------------+      +-----------------+              |
             |                       |                      |
      +-------------+      +-----------------+      +----------------+
  (2) |  Messages   |      |     <rpc>,      |      | <notification> |
      |             |      |   <rpc-reply>   |      |                |
      +-------------+      +-----------------+      +----------------+
             |                       |                      |
      +-------------+      +-----------------------------------------+
  (1) |   Secure    |      |  SSH, TLS, BEEP/TLS, SOAP/HTTP/TLS, ... |
      |  Transport  |      |                                         |
      +-------------+      +-----------------------------------------+

Transport + Messages レイヤーはapache module, Operations + Content レイヤーはsymfony アプリとして実装されているが,

  • XML parser のふるまいを変えるのに apache module とsymfony 両方の面倒をみる必要があって, 時には情報の橋渡しをしてあげないとダメだったり…
  • NETCONF に使うXML 構造が, symfony 上でテンプレート化されていない…

予想どおりだけど, できたのは<hello> の交換までだった. 「デバイス種別+メーカーに応じてXML を出し分ける」とか 現時点では厳しそう.

まとめ

Juniper Firefly をコントロールするため, いくつかの実装を試してみた.
(ちなみにNETCONF WG のサイト から 他の実装も探せる)

上位レイヤー(Operations + Content) のモデル化が進んでいない現状では, 下位レイヤー(Transport + Messages) を薄くwrap するライブラリーを使うのが良いと思う. 上位レイヤーは, 自分たちが必要な範囲内で, 自分たちで抽象化する.

「なんだNETCONF 単体じゃあできないのか」というガッカリ感が先行するけれど, CLI を相手にするのも面倒だし.

リッチなアプリケーションはレールから外れたときに痛い目を見るし, ネットワークデバイス側は融通が効かない分 レールに乗せるための制約が大きすぎるとも感じた.

一方で, Netopeer のようなアプローチも “将来的には” 成功する可能性があると思っている. Netopeer 本来の使いかたであれば, netopeer-server が管理サーバー内で上位レイヤーの抽象化を担ってくれる. たとえば クライアント - サーバー間のAPI を厳密に規定する代わりに, サーバー内でnetopeer-server API を作ったほうがうまくいく場合もあると思う.

MBaaS のようにSDK を配ってAPI を呼んでもらうやりかたも一般的になってきたし, PyEZ みたいなやつで抽象化レイヤーを作れるかもしれない.

少しずつではあるが, NETCONF データのモデル化が進んできている ことにも注目したい. モデルが標準化されれば, 状況は変わってくると期待している.

*1:バイスが持つ設定や状態の表現方法のこと

*2:詳細は前のエントリー 見てください