vMX(VCP/VFP)をPyEZで制御してみるだけの優しい世界
はじめに
個人アカウントでvMX Trialのダウンロードができるようになったので、
GNS3でvMX(VCP/VFP)の動作確認 をして、
ついでに vMX(VCP/VFP)でVPLSを動かしてみる ところまで実施しました。
今度はそれを PyEZ(py-junos-eznc) で制御してみようと思います。
PyEZ
Juniperが提供している、Junos制御用の自動化ツールキットです。
NETCONFで接続して、JunosのRPC APIを叩いたりとか、Ansibleよろしくfactの収集がサポートされていたりとか、Junosに限って言えば色々できます。
1.x系は動作環境がPython 2.6 or 2.7でしたが、先日出た2.x系はpython 3.4以上をサポートするようになりました。
リリースノートにそう書いてあったもん!: https://github.com/Juniper/py-junos-eznc/releases/tag/2.0.0
まぁそういうやつです。
依存関係も比較的少なくて、コードも簡単とくれば、Teratermマクロの次の1歩としては丁度良いんじゃないですかね。Juniperなら。
Juniperに思うところがあるんじゃなくて、僕が装置を触ったりコードを書く機会から遠ざけられているからいじけているだけです。
あと今回のやつの参考とか。
環境
環境は、前回の vMX(VCP/VFP)でVPLSを動かしてみる 時のやつと大体一緒です。
左上に変なの(PyEZ動かすやつ)が増えてるくらいですね。
PyEZって書いてあるけど、ただのubuntu 16.04の仮想マシンです。
PyEZのインストール
インストール
1
2
3
4
5
|
sudo apt update
sudo apt install -y python3-pip
sudo apt install -y libssl-dev
pip3 install -U pip
sudo pip3 install junos-eznc
|
はい、オッケー。
- aptからpipを入れて、pip installすれば良いです。
- libssl-devはparamiko(pythonのsshライブラリ)が使うpycryptoのコンパイル用。
pip3 install -U pip
は、aptから入るpip3だと依存関係の解消中にrequirements.txtの解釈に失敗する場合があるのでアップデートしてます。
Junosの設定
1
2
3
|
configure
set system services netconf ssh port 830
commit
|
こんな感じです。
接続テスト
- 例外キャッチしてない完全正常系のコードしか使わないのでご容赦ください。
PyEZノード
1
2
|
sudo ip link set up ens4
sudo ip addr add 172.16.0.21/24 dev ens4
|
アドレスは適当に設定。制御対象は 172.16.0.1/24
が設定されてるものとします。
PyEZノードからpython3の対話シェルを起動して、Junosに接続します。
1
2
3
4
5
6
7
8
9
10
11
12
|
$ python3
Python 3.5.2 (default, Jul 5 2016, 12:43:10)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from jnpr.junos import Device
>>> dev=Device(host='172.16.0.1', user="root", password="vpls2016", port='830')
>>> dev.open()
Device(172.16.0.1)
>>> print(dev.facts)
{'switch_style': 'BRIDGE_DOMAIN', 'vc_capable': False, 'version_info': junos.version_info(major=(16, 1), type=R, minor=1, build=7), 'fqdn': 'router', 'version': '16.1R1.7', 'personality': 'MX', 'model': 'VMX', '2RE': False, 'ifd_style': 'CLASSIC', 'serialnumber': 'VM57DEF69785', 'master': 'RE0', 'hostname': 'router', 'version_RE0': '16.1R1.7', 'domain': None, 'RE0': {'status': 'OK', 'mastership_state': 'master', 'last_reboot_reason': 'Router rebooted after a normal shutdown.', 'up_time': '2 days, 23 hours, 31 minutes, 1 second', 'model': 'RE-VMX'}, 'HOME': '/root', 'virtual': True}
>>>
|
まぁこんな感じ。
情報収集
インデントが無くて少々汚いけど、Junosにログインして show vpls connections logical-system VPLS
と同じ内容を取得します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
$ python3
Python 3.5.2 (default, Jul 5 2016, 12:43:10)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from lxml import etree
>>> from jnpr.junos import Device
>>> dev=Device(host='172.16.0.1', user="root", password="vpls2016", port='830', gather_facts=False)
>>> dev.open()
Device(172.16.0.1)
>>> print(dev.display_xml_rpc('show vpls connections logical-system VPLS', format='text'))
<get-vpls-connection-information>
<logical-system>VPLS</logical-system>
</get-vpls-connection-information>
>>> tree=dev.rpc.get_vpls_connection_information(logical_system="VPLS")
>>> print(etree.tostring(tree).decode('utf-8'))
<vpls-connection-information>
<instance style="normal">
<instance-name>vpls2001</instance-name>
<edge-protection>Not-Primary</edge-protection>
<reference-site>
<local-site-id>vpls-site (1)</local-site-id>
<connection heading="connection-site Type St Time last up # Up trans">
<connection-id>2</connection-id>
<connection-type>rmt</connection-type>
<connection-status>Up</connection-status>
<last-change>Sep 18 22:25:43 2016
</last-change>
<up-transitions>1</up-transitions>
<remote-pe>2.2.2.2</remote-pe>
<control-word>No</control-word>
<inbound-label>262146</inbound-label>
<outbound-label>262145</outbound-label>
<local-interface>
<interface-name>lsi.17826048</interface-name>
<interface-status>Up</interface-status>
<interface-encapsulation>VPLS</interface-encapsulation>
<profile-name/>
<profile-varset-name/>
<interface-description>Intf - vpls vpls2001 local site 1 remote site 2</interface-description>
</local-interface>
<vc-flow-label-transmit>No</vc-flow-label-transmit>
<vc-flow-label-receive>No</vc-flow-label-receive>
</connection>
</reference-site>
</instance>
</vpls-connection-information>
>>>
|
dev.display_xml_rpc
は、ルータで <command> | display xml rpc
と打った時に返ってくる情報と同じです。
こんなの。
1
2
3
4
5
6
7
8
9
10
|
root@router> show interfaces | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/16.1R1/junos">
<rpc>
<get-interface-information>
</get-interface-information>
</rpc>
<cli>
<banner></banner>
</cli>
</rpc-reply>
|
で、 ‘-’ を ‘_’ に変えるとメソッド名になるので、それを叩くと、プロンプトでshowコマンドを叩いているときと同じ情報が手に入ります。
上記の場合なら、 なので、
get_interface_information() になります。
pythonならelement名を抜き出して、dev.rpcからgetattrすればメソッド抜けるんじゃないですかね。確認してないですけど。
設定変更
設定も変更してみよう。
今こんな感じになっているとしましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
root@router> show configuration interfaces
ge-0/0/0 {
vlan-tagging;
}
ge-0/0/1 {
vlan-tagging;
}
ge-0/0/4 {
flexible-vlan-tagging;
encapsulation vlan-vpls;
}
fxp0 {
unit 0 {
family inet {
address 172.16.0.1/24;
}
}
}
|
そこで、 ge-0/0/5 にも同じような設定を追加してみます。
こんなやつ。
1
2
3
4
|
ge-0/0/5 {
flexible-vlan-tagging;
encapsulation vlan-vpls;
}
|
ではさっきと同様に。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
$ python3
Python 3.5.2 (default, Jul 5 2016, 12:43:10)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device
>>> from jnpr.junos.utils.config import Config
>>> conf = '''
... interfaces {
... replace: ge-0/0/5 {
... flexible-vlan-tagging;
... encapsulation vlan-vpls;
... }
... }
... '''
>>> dev=Device(host='172.16.0.1', user="root", password="vpls2016", port='830', gather_facts=False)
>>> dev.open()
Device(172.16.0.1)
>>> dev.bind(cu=Config)
>>>
>>> dev.cu.lock()
True
>>> dev.cu.load(conf, format='text')
<Element load-configuration-results at 0x7f87c2440448>
>>> print(dev.cu.diff())
[edit interfaces]
+ ge-0/0/5 {
+ flexible-vlan-tagging;
+ encapsulation vlan-vpls;
+ }
>>> dev.cu.commit(comment='[PyEZ] commit')
True
>>> dev.cu.unlock()
True
>>> dev.close()
|
そして、ルータでコンフィグとコミット情報を見てみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
root@router> show configuration interfaces
ge-0/0/0 {
vlan-tagging;
}
ge-0/0/1 {
vlan-tagging;
}
ge-0/0/4 {
flexible-vlan-tagging;
encapsulation vlan-vpls;
}
ge-0/0/5 {
flexible-vlan-tagging;
encapsulation vlan-vpls;
}
fxp0 {
unit 0 {
family inet {
address 172.16.0.1/24;
}
}
}
root@router> show system commit revision detail
Revision: re0-1474487932-20
User : root
Client : netconf
Time : 2016-09-21 19:58:55 UTC
Log : [PyEZ] commit
|
とまぁこんな感じ。
コミットメッセージにトランザクションIDなんか振ったりして、お洒落な感じにしたい気持ちになるね。
dev.cu.lock()
をしている最中は configure exclusive
と同等の状態になるので、他のユーザーとのcommit衝突は起こらないようにできます。
既に他のユーザーが設定変更途中である場合は、 dev.cu.lock()
の時にこんな例外を受け取れるはずです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
>>> dev.cu.lock()
Traceback (most recent call last):
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/device.py", line 516, in execute
rpc_rsp_e = self._rpc_reply(rpc_cmd_e)
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/device.py", line 943, in _rpc_reply
return self._conn.rpc(rpc_cmd_e)._NCElement__doc
File "/usr/local/lib/python3.5/dist-packages/ncclient/manager.py", line 171, in wrapper
return self.execute(op_cls, *args, **kwds)
File "/usr/local/lib/python3.5/dist-packages/ncclient/manager.py", line 231, in execute
raise_mode=self._raise_mode).request(*args, **kwds)
File "/usr/local/lib/python3.5/dist-packages/ncclient/operations/third_party/juniper/rpc.py", line 42, in request
return self._request(rpc)
File "/usr/local/lib/python3.5/dist-packages/ncclient/operations/rpc.py", line 337, in _request
raise self._reply.error
ncclient.operations.rpc.RPCError:
configuration database modified
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/utils/config.py", line 461, in lock
self.rpc.lock_configuration()
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/rpcmeta.py", line 156, in _exec_rpc
return self._junos.execute(rpc, **dec_args)
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/decorators.py", line 71, in wrapper
return function(*args, **kwargs)
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/decorators.py", line 26, in wrapper
return function(*args, **kwargs)
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/device.py", line 529, in execute
raise e(cmd=rpc_cmd_e, rsp=rsp, errs=err)
jnpr.junos.exception.RpcError: RpcError(severity: error, bad_element: None, message: configuration database modified)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.5/dist-packages/jnpr/junos/utils/config.py", line 464, in lock
raise LockError(rsp=err.rsp)
jnpr.junos.exception.LockError: LockError(severity: error, bad_element: None, message: configuration database modified)
|
まぁ、この辺りの状態遷移については他にも色々考えなきゃいけないけども。
VPNのように複数ルータにコンフィグを入れるアトミック操作の場合は、複数ルータのロックを取得してからコミット、全台のコミットが確認できたらロックを解放する、という流れが作れそうですね。
一部のマシンでコミットに失敗した場合は、残りのマシンはロールバックするとか。
そうそう、ロールバックしたいときはこう。
他にも、コンフィグをset文で書くのであればこう。
1
2
3
4
5
|
conf = '''
set system host-name hostname-pyez
set interfaces ge-0/0/5 flexible-vlan-tagging
'''
dev.cu.load(conf, format='set')
|
これならルータは触れるけどプログラムはちょっと、って人にも良い感じじゃないかな。
jinja2を使ったテンプレートの方は、ファイルに吐き出さないといけないっぽいので、ここでは割愛。
この辺を見てくると良いんじゃないかと思う。
https://www.juniper.net/techpubs/en_US/junos-pyez2.0/topics/task/program/junos-pyez-program-configuration-data-loading.html
おしまい
PyEZはなかなかシンプルに作られてるし、Junosが多い環境ならきっとかなり楽しいんだろうなぁ。
適当に触れてみた程度でも、案外動くものだという感触を得たので、Juniperがんばえー。