NetOpsCoding AdventCalender の20日目の投稿です。遅れてしまってごめんなさい。
概要
二週間前、こんな記事を書きました。
ソフトウェアからルータにSSH(Exscript)で設定してみる
ルータを自動設定するための課題の一つとして、ルータCLIの構文解析処理が非常に大変な点が挙げられます。ルータCLIにおける状態確認コマンドの出力結果は、人が目視確認することを前提として作られているため、プログラミング言語で用意されているSSHやTelnelパッケージを使って情報を自動抽出しようとすると、微妙な改行やスペース、特殊文字、エラー表示などを全て柔軟に対応する必要があるため、とても苦労します。
このあたりの話は @yuyarin さんのブログでも紹介されているので、参考にしてみてください。
運用自動化のためにネットワーク機器に求めるもの
SSHやTelnetパッケージによるCLI解析処理/例外処理の苦労から脱却すべく、今回は自動化で注目を集めているNETCONFを使ったソフトウェアを作ってみたいと思います。
正直なところ実装状況がメーカーによってまちまちなので、お仕事ですぐに使えるかどうかはわかりませんが、今回は勉強も兼ねて試してみます。
NETCONFとは
NETCONFとは、ネットワーク装置の設定や情報取得するための標準化されたプロトコルです。SNMPの代替として登場してきたNETCONFですが、複数ベンダーのネットワーク装置で利用することが想定されており、近年の自動化やSDNブームとあいまって注目されるようになりました。
NETCONFにおけるコンフィグ設定内容や状態出力は、全てXML-RPCフォーマットで定義されており、先述のような構文解析処理は必要最低限で済ますことができます。
1年ほど前に調べたときには、主要メーカの最新版OSではNETCONFはほぼほぼ実装されているようでした。ただしOS versionによって実装状況やサポート対象機能は大きく異なるので、お使いのversionでNETCONFがサポートされているか調べてみることをオススメします。
Manufactor | OS | Supported API |
---|---|---|
Cisco | IOS or IOS XE | OnePK, NETCONF |
IOS XR | OnePK, NETCONF | |
NX OS | OnePK, NX-API, NETCONF | |
Juniper | JUNOS | JUNOS XML Protocol, REST API, NETCONF |
Brocade | NetIron | REST API, NETCONF |
Network OS | REST API, NETCONF | |
Arista | EOS | eAPI (REST API) |
NETCONFの参考資料
NETCONFの概要を知るには、 @codeout さんが詳細に解説されているので、こちらの記事を参考にするといいと思います。
知ったかぶりしない NETCONF
やってみよう NETCONF
NETCONFやYANGモデルが登場してきた背景や標準化動向については、下記の発表資料で紹介されています。
JANOG36 NETCONF/YANG
さらに詳細を知りたい方はRFCを読んでみましょう。
RFC6241 Network Configuration Protocol (NETCONF)
RFC6020 YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)
NETCONF使ってみる
それではさっそくNETCONFを使ってルータに設定を入れていきます。NETCONFクライアントとルータの間はXML-RPCを用いて要求と応答のやりとりをします。
実装の流れとしては、以下のように進めました。
- 対象のルータOSで定義されたXML-RPCのスキーマ(タグによって構造化されたルール)を調べる
- XML-RPCフォーマットに従って設定したい内容を作成し、NETCONFクライアントを使ってルータに送信する
- ルータから応答のメッセージを確認する
- 人が設定内容を確認した上で判断し、commitを実行する
実装環境
- ルータOS
- JUNOS
- プログラミング言語
- Python 2.7.5
- ライブラリ
-
ncclient 0.4.6
- Pythonで開発されているNETCONFクライアント
-
ncclient 0.4.6
事前準備
Pythonプログラムを書くサーバにncclientをインストールします。
sudo pip install ncclient
JUNOSルータには事前にnetconfを許可する設定を入力しておきます。
set system services netconf ssh
ルータのXML-RPCフォーマットの調査
ルータのXML-RPCフォーマットについては、各ベンダのサポートページで公開されています。
NETCONF XML Management Protocol Developer Guide
JUNOSでは、ルータCLIにてコマンドの末尾に「 | display xml rpc」とつけると、コンフィグ内容やshowコマンドをRPCリクエストフォーマットで出力することができるので非常に便利です。
<rpc>...</rpc>で囲まれた部分をNETCONFで利用します。
user1@router> show interfaces xe-0/0/0 terse | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/xxxxx/junos">
<rpc>
<get-interface-information>
<terse/>
<interface-name>xe-0/0/0</interface-name>
</get-interface-information>
</rpc>
<cli>
<banner></banner>
</cli>
</rpc-reply>
showコマンドによる状態情報の取得
ncclientではcommand関数を使うことで、XML-RPCスキーマを使うことなくルータCLIでのshowコマンドをそのまま利用し、情報を取得することができます。ここではインタフェース xe-0/0/0の状態を取得するコマンドを実行します。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from ncclient import manager
username = 'user1'
password = 'pass1'
ipv4 = '192.168.0.1'
port = 22
connection = manager.connect(host = ipv4, port = port, username = username, password = password, timeout = 20, device_params={'name':'junos'}, hostkey_verify=False )
print '1. run show command'
print '='*40
print connection.command('show interfaces xe-0/0/0 terse')
実行結果がこちらです。多少スペースがずれていますが、<admin-status>や<oper-status>でインタフェースの状態が確認できていることがわかります。
$ python show_junos.py
1. run show command
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<interface-information style="terse">
<physical-interface>
<name>
xe-0/0/0
</name>
<admin-status>
down
</admin-status>
<oper-status>
down
</oper-status>
</physical-interface>
</interface-information>
</rpc-reply>
指定したインタフェースが無かった場合は、<rpc-error>というタグでエラー出力されます。
[t-tsuchiya@dev set_router_netconf]$ python show_junos.py
1. run show command
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<interface-information style="terse">
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<source-daemon>
ifinfo
</source-daemon>
<error-message>
device xe-0/0/0 not found
</error-message>
</rpc-error>
</interface-information>
</rpc-reply>
ちなみにncclientでは、人が見やすいテキスト形式( = ルータでのCLI結果と同様のもの)をオプションで出力するこもと可能です。
(省略)
# print connection.command('show interfaces terse xe-0/0/0')
print connection.command('show interfaces terse xe-0/0/0', format='text')
$ python show_junos.py
1. run show command
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<output>
Interface Admin Link Proto Local Remote
xe-0/0/0 down down
</output>
</rpc-reply>
自動化ソフトウェアが処理する場合にはXML-RPC形式、運用者が目視確認する場合にはテキスト形式にする、など使い分けるのがよいでしょう。
ルータコンフィグ情報の取得
ncclientではget_config関数やget関数で、現在のルータに設定されているコンフィグ情報を取得することができます。下記の例では、ルータに設定されてる全てのコンフィグ情報を取得しています。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from ncclient import manager
username = 'user1'
password = 'pass1'
ipv4 = '192.168.0.1'
port = 22
connection = manager.connect(host = ipv4, port = port, username = username, password = password, timeout = 20, device_params={'name':'junos'}, hostkey_verify=False )
print connection.get_config(source='running')
$ python show_junos.py
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<data>
<configuration changed-seconds="1450593048" changed-localtime="2015-12-20 15:30:48 JST">
<version>xx.xx.xx</version>
<system>
<host-name>router</host-name>
<time-zone>Asia/Tokyo</time-zone>
(以下、省略)
インタフェースの設定内容を見たい時など部分的にコンフィグを取得したい場合は、get_config関数にfilterオプションをつけることで取得できます。ただしこのときにXML-RPCの構造情報が必要になるので、事前にルータにて'show configration interaface xe-0/0/0 | display xml' コマンドなどでXML-RPCスキーマを調べておく必要があります。
(省略)
request_config_interface = """
<configuration>
<interfaces>
<interface>
<name>xe-0/0/0</name>
</interface>
</interfaces>
</configuration>"""
# print connection.get_config(source='running')
print connection.get_config(source='running', filter=('subtree', request_config_interface) )
$ python show_junos.py
1. run show command
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<data>
<configuration commit-seconds="1450593048" commit-localtime="2015-12-20 15:30:48 JST" commit-user="user1">
<interfaces>
<interface>
<name>xe-0/0/0</name>
<disable/>
</interface>
</interfaces>
</configuration>
</data>
</rpc-reply>
この時点では、xe-0/0/0に 'disable'の設定だけが入っている状態です。
user1@router> show configuration interfaces xe-0/0/0
disable;
インタフェース情報の設定
それではインタフェースに設定を投入するプログラムを作ります。ここでは下記に示すIPアドレスを設定し、インタフェースを有効化することを目的とします。
delete interfaces xe-0/0/0 disable
set interfaces xe-0/0/0 unit 0 family inet address 10.0.0.1/30
ルータコンフィグを設定するにはncclientのedit_config関数を利用します。上記設定をXML-RPCスキーマに落とし込んだ上で、edit_configの引数に指定して実行します。
作成したプログラムを記載します。プログラムの流れとしては以下の流れで実行していきます。
- Step 1. インタフェース部分のルータコンフィグを事前確認
- Step 2. edit_config関数を使ったルータコンフィグを候補コンフィグに設定
- Step 3. 候補コンフィグの正常性確認
- Step 4. 候補コンフィグの表示
- Step 5. Commitの実施(ここでは人が判断します)
- Step 6. インタフェース部分のルータコンフィグを事後確認
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from ncclient import manager
username = 'user1'
password = 'pass1'
ipv4 = '192.168.0.1'
port = 22
connection = manager.connect(host = ipv4, port = port, username = username, password = password, timeout = 20, device_params={'name':'junos'}, hostkey_verify=False )
print '='*40
print 'Step 1. show running-config before commit'
print '='*40
request_config_interface = """
<configuration>
<interfaces>
<interface>
<name>xe-0/0/0</name>
</interface>
</interfaces>
</configuration>"""
print connection.get_config(source='running', filter=('subtree', request_config_interface) )
print '='*40
print 'Step 2. set config on candidate-config'
print '='*40
request_set_config_interface = """
<config>
<configuration>
<interfaces>
<interface>
<name>xe-0/0/0</name>
<enable/>
<unit>
<name>0</name>
<family>
<inet>
<address>
<name>10.0.0.1/30</name>
</address>
</inet>
</family>
</unit>
</interface>
</interfaces>
</configuration>
</config>
"""
print connection.edit_config(target='candidate', config=request_set_config_interface)
print '='*40
print 'Step 3. validate candidate-config'
print '='*40
print connection.validate(source='candidate')
print '='*40
print 'Step 4. show config on candicate-config'
print '='*40
print connection.get_config(source='candidate', filter=('subtree', request_config_interface) )
print '='*40
print 'Step 5. commit'
print '='*40
print 'Do you commit? y/n'
choice = raw_input().lower()
if choice == 'y':
connection.commit()
else:
connection.discard_changes()
print '='*40
print 'Step 6. show running-config after commit'
print '='*40
print connection.get_config(source='running', filter=('subtree', request_config_interface) )
if connection:
connection.close_session()
実行すると下記のように動作します。
$ python set_junos.py
========================================
Step 1. show running-config before commit
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<data>
<configuration commit-seconds="1450688416" commit-localtime="2015-12-21 18:00:16 JST" commit-user="user1">
<interfaces>
<interface>
<name>xe-0/0/0</name>
<disable/>
</interface>
</interfaces>
</configuration>
</data>
</rpc-reply>
========================================
Step 2. set config on candidate-config
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<ok/>
</rpc-reply>
========================================
Step 3. validate candidate-config
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<commit-results>
</commit-results>
<ok/>
</rpc-reply>
========================================
Step 4. show config on candicate-config
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<data>
<configuration changed-seconds="1450688512" changed-localtime="2015-12-21 18:01:52 JST">
<interfaces>
<interface>
<name>xe-0/0/0</name>
<undocumented>
<enable/>
</undocumented>
<unit>
<name>0</name>
<family>
<inet>
<address>
<name>10.0.0.1/30</name>
</address>
</inet>
</family>
</unit>
</interface>
</interfaces>
</configuration>
</data>
</rpc-reply>
========================================
Step 5. commit
========================================
Do you commit? y/n
y
========================================
Step 6. show running-config after commit
========================================
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<data>
<configuration commit-seconds="1450688535" commit-localtime="2015-12-21 18:02:15 JST" commit-user="user1">
<interfaces>
<interface>
<name>xe-0/0/0</name>
<undocumented>
<enable/>
</undocumented>
<unit>
<name>0</name>
<family>
<inet>
<address>
<name>10.0.0.1/30</name>
</address>
</inet>
</family>
</unit>
</interface>
</interfaces>
</configuration>
</data>
</rpc-reply>
ルータCLIでも、正しく設定できたことを確認することができました。
user1@router> show configuration interfaces xe-0/0/0
enable;
unit 0 {
family inet {
address 10.0.0.1/30;
}
}
NETCONFプログラムを作ってみた感じたこと
ソフトウェアにやさしいエラー出力
edit_config関数でコンフィグ設定を実施し、成功した場合には以下のように結果を出力してくれます。
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<ok/>
</rpc-reply>
コンフィグ設定に失敗した場合は、エラー内容を出力してくれます。(下記例は変更できない設定を書き換えようとした場合)
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>
could not set enab_disab
</error-message>
</rpc-error>
<ok/>
</rpc-reply>
自動化プログラムを開発する際には、これが非常に助かります。ルータCLIを利用したソフトウェアであれば「エラーメッセージを検知 -> 問題箇所を発見するためにshowコマンドを実行、構文解析 -> また別のshowコマンドを実行、構文解析 -> ...」というしんどいソフトウェア実装をする必要がありましたが、エラーメッセージをXMLタグで必要部分のみを指定して抽出するだけで、次のプロセスに遷移することが可能になり、プログラムの量を大幅に少なくすることが出来ます。
deleteコマンドが上手くいかない
上記のプログラムを作るにあたって、deleteコマンドを実装するのに苦労しました。
delete interfaces xe-0/0/0 disable;
元々、XML-RPCスキーマでは、disable; は下記のように格納されています。
<interfaces>
<interface>
<name>xe-0/0/0</name>
<disable/>
</interface>
</interfaces>
この<disable/>を削除するのがやっかいでした。これはncclientの実装の問題かもしれないのですが、RFCで定義されている「<delete-config>...</delete-config>」がncclientでは使えずエラーが出ます。
(省略)
request_delete_config_interface = """
<delete-config>
<interfaces>
<interface>
<name>xe-0/0/0</name>
<disable/>
</interface>
</interfaces>
</delete-config>
"""
print connection.edit_config(target='candidate', config=request_delete_config_interface)
Traceback (most recent call last):
File "set_junos.py", line 62, in <module>
print connection.edit_config(target='candidate', config=request_delete_config_interface)
File "/usr/lib/python2.7/site-packages/ncclient/manager.py", line 157, in wrapper
return self.execute(op_cls, *args, **kwds)
File "/usr/lib/python2.7/site-packages/ncclient/manager.py", line 227, in execute
raise_mode=self._raise_mode).request(*args, **kwds)
File "/usr/lib/python2.7/site-packages/ncclient/operations/edit.py", line 62, in request
node.append(validated_element(config, ("config", qualify("config"))))
File "/usr/lib/python2.7/site-packages/ncclient/xml_.py", line 117, in validated_element
raise XMLError("Element [%s] does not meet requirement" % ele.tag)
ncclient.xml_.XMLError: Element [delete-config] does not meet requirement
次に、ncclientサンプルコードを参考にしてXMLタグ内の「delete = "delete"」という記述も試してみましたが、こちらは一応通るものの、RPCエラー応答が返ってくるので避けたほうがよさそうです。
(省略)
request_delete_config_interface = """
<config>
<configuration>
<interfaces>
<interface delete="delete">
<name>xe-1/3/0</name>
<disable/>
</interface>
</interfaces>
</configuration>
</config>
"""
print connection.edit_config(target='candidate', config=request_delete_config_interface)
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<rpc-error>
<error-type>protocol</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-message>
could not set enab_disab
</error-message>
</rpc-error>
<ok/>
</rpc-reply>
最終的に今回は、「delete disable;」ではなく「set enable;」を設定することで、回避(?)しました。根本解決にはなってない気がしますが、とりあえずエラーなく設定できるようになりました。
```py:set_junos.py
(省略)
request_delete_config_interface = """
<config>
<configuration>
<interfaces>
<interface>
<name>xe-1/3/0</name>
<enable/>
</interface>
</interfaces>
</configuration>
</config>
"""
print connection.edit_config(target='candidate', config=request_delete_config_interface)
<rpc-reply message-id="urn:uuid:xxxxxx-xxxxxx">
<ok/>
</rpc-reply>
「enable」が設定されたXML-RPCを見てみると、「<undocumented>..<undocumented>」というタグで囲われているため、詳細は不明ですが、XML-RPCでは定義されていない設定なのかもしれません。
<interfaces>
<interface>
<name>xe-1/3/0</name>
<undocumented>
<enable/>
</undocumented>
</interface>
</interfaces>
自分で調べたわけではないのですが、現時点ではNETCONFでも設定できない内容は一部であるそうなので、自動化したいネットワーク機能がNETCONFで設定実施が可能かどうかは、事前に調べておいたほうがよさそうです。
メーカによってXML-RPCのスキーマが異なり、調べるのがしんどい
元々このブログでは、JUNOSとIOSXRを対象にプログラムを書くつもりだったのですが、各XMLスキーマを調べるのに時間がかかりすぎて断念しました。
JANOG36 NETCONF/YANGの発表資料を見る限り、IOS XRでは下記のように書くようです。
<Configuration>
<InterfaceConfigurationTable>
<InterfaceConfiguration>
<Naming>
<Active>act</Active>
<InterfaceName Match="Loopback0"/>
</Naming>
<InterfaceVirtual>true</InterfaceVirtual>
<IPV4Network>
<Addresses>
<Primary>
<Address>172.16.255.1</Address>
<Netmask>255.255.255.255</Netmask>
</Primary>
</Addresses>
</IPV4Network>
</InterfaceConfiguration>
</InterfaceConfigurationTable>
</Configuration>
今回利用したJUNOSのXMLスキーマと比較しても、かなり異なることがわかります。
<configuration>
<interfaces>
<interface>
<name>xe-0/0/0</name>
<undocumented>
<enable/>
</undocumented>
<unit>
<name>0</name>
<family>
<inet>
<address>
<name>10.0.0.1/30</name>
</address>
</inet>
</family>
</unit>
</interface>
</interfaces>
</configuration>
JUNOSの場合は「display xml rpc」オプションがあるのでだいぶ楽ですが、その他のメーカのXML構造を調べるためにドキュメントをみながらの実装になるので多少骨が折れます。
NETCONFという業界標準のプロトコルが出てきたのは嬉しくはありますが、現状ではまだルータOSごとに専用のプログラムを書く必要があります。データモデルであるYANGが現在IETFで活発に議論と実装が進んでおり、近いうちにルータが持つデータ構造が統一化される日も来るかもしれません。
NETCONFを使うことのまとめ
現時点でのNETCONFを使うことの利点欠点を私なりにまとめてみました。
利点
- 状態確認の結果やエラー結果がXML-RPCで構造化されているため、自動化ソフトウェアを作る際に構文解析や例外処理のコード量を大幅に少なくすることができる
欠点
- ネットワーク装置メーカによってXML構造が異なっていて、まだしばらくはルータOSごとに専用プログラムが必要になる
- メーカごとにXML-RPCを調べるのがしんどい
- 現時点ではNETCONFで設定できない機能もある?
- 私自身が見つけた訳ではないのですが、LACPなどの設定ができなかったという話は聞いたことがあります。
最後に
今回の記事を書くにあたり、非常に良い勉強になりました。NETCONFプログラムを書く場合には、プログラムを書く以上に、メーカが公開しているXML-RPCスキーマ情報であったり、利用するライブラリのドキュメントをたくさん読む必要があります。現時点ではサンプルコードも少ないので苦労することも多いかもしれません。
ユーザ(=ネットワーク運用者、ソフトウェア開発者)にメーカごとのXML-RPCスキーマを意識させないユーザフレンドリなラッパーAPIが登場すると実装の敷居がかなり下がるので、ネットワーク運用自動化が一気に進むように思います。
以前ブログで紹介した NAPALMのような、複数ベンダーの装置を同一の関数で設定できるようなAPIライブラリが今後登場してくれば、ソフトウェア実装が楽になるのではないでしょうか。
過去記事:ルータ制御APIライブラリ NAPALMを触ってみた
(ちなみにNAPALMでは、JUNOSのみNETCONFで、あとはSSHだったりベンダAPIだったり、まちまちな実装を利用しているようです。)
今回の記事は以上です。みなさまがNETCONFを始める際のお役に立てれば幸いです。
お付き合いいただきありがとうございました。