vMX(VCP/VFP)をPyEZで制御してみるだけの優しい世界

Posted on 2016/09/22(Thu) 05:40 in technical

イマココ

個人アカウントで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動かすやつ)が増えてるくらいですね。

0922a_vMX_trial_and_pyez_001.png

PyEZって書いてあるけど、ただのubuntu 16.04の仮想マシンです。

PyEZのインストール

インストール:

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の設定

configure
set system services netconf ssh port 830
commit

こんな感じです。

接続テスト

警告

例外キャッチしてない完全正常系のコードしか使わないのでご容赦ください。

PyEZノード:

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に接続します。:

$ 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 と同じ内容を取得します。:

$ 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 と打った時に返ってくる情報と同じです。

こんなの。:

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> なので、 get_interface_information() になります。

pythonならelement名を抜き出して、dev.rpcからgetattrすればメソッド抜けるんじゃないですかね。確認してないですけど。

設定変更

設定も変更してみよう。

今こんな感じになっているとしましょう。:

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 にも同じような設定を追加してみます。

こんなやつ。:

ge-0/0/5 {
    flexible-vlan-tagging;
    encapsulation vlan-vpls;
}

ではさっきと同様に。:

$ 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()

そして、ルータでコンフィグとコミット情報を見てみます。:

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() の時にこんな例外を受け取れるはずです。:

>>> 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のように複数ルータにコンフィグを入れるアトミック操作の場合は、複数ルータのロックを取得してからコミット、全台のコミットが確認できたらロックを解放する、という流れが作れそうですね。

一部のマシンでコミットに失敗した場合は、残りのマシンはロールバックするとか。

そうそう、ロールバックしたいときは:

dev.cu.rollback()

他にも、コンフィグをset文で書くのであれば、:

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がんばえー。