Nexus9000v の API を弄る(NX-API REST, NX-API CLI)
最初に
やること
Cisco 公式 / Cisco Nexus 9000 Series NX-OS Programmability Guide, Release 7.x のツリーを眺めると、NX-OS には色々な API が揃っていそうです。なので、様子を見ていきます。
ドキュメントレベルでは、少なくとも以下が揃っていそうで、今回はその一部を取り上げます。
なお、まずは機器側としての様子を見たかったため、ツールに関してはほぼ触れず。(まずは機器側の限界を知っておきたい)
- NX-API
- NETCONF
- ドキュメントを流し読みした感じ、割とちゃんと作られていそう
- OnBox Python
参考資料
試用する
環境情報と準備
NX-OS 側
torsw101a# show version | egrep -i nxos NXOS: version 7.0(3)I6(1) NXOS image file is: bootflash:///nxos.7.0.3.I6.1.bin NXOS compile time: 5/16/2017 22:00:00 [05/17/2017 15:21:28]
以下のように機能を有効化するだけ
torsw101a(config)# feature scp-server torsw101a(config)# feature nxapi torsw101a# show nxapi nxapi enabled HTTP Listen on port 80 HTTPS Listen on port 443
APIクライアント側準備
kotetsu@receiver:~/nxos_rest$ uname -a Linux receiver 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux kotetsu@receiver:~/nxos_rest$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=16.04 DISTRIB_CODENAME=xenial DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"
$ sudo apt install -y python3-pip $ pip3 install requests
...(無言)
NX-API REST 試用
参照
以下のような VLAN 設定の NX-OS に対して
torsw101a# show run vlan !Command: show running-config vlan !Time: Sun Oct 29 11:19:38 2017 version 7.0(3)I6(1) vlan 1,100,300,3901 vlan 100 vn-segment 10100 vlan 300 vn-segment 10300 vlan 3901 vn-segment 50001
こんな感じのサンプルスクリプトを作って実行すれば
(なお前半の認証部分は Cisco APIC REST API User Guide / Testing the API with Python のサンプルコードをマルパク)
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json base_url = 'http://10.0.0.230/api/' # create credentials structure name_pwd = {'aaaUser': {'attributes': {'name': 'admin', 'pwd': 'P@ssw0rd'}}} json_credentials = json.dumps(name_pwd) # log in to API login_url = base_url + 'aaaLogin.json' post_response = requests.post(login_url, data=json_credentials) # get token from login response structure auth = json.loads(post_response.text) login_attributes = auth['imdata'][0]['aaaLogin']['attributes'] auth_token = login_attributes['token'] # create cookie array from token cookies = {} cookies['APIC-Cookie'] = auth_token bd_url = base_url + 'node/mo/sys/bd.json?rsp-subtree=children' get_response = requests.get(bd_url, cookies=cookies, verify=False) print(json.dumps(get_response.json(), indent=2))
以下のような出力を得られる
{ "totalCount": "1", "imdata": [ { "bdEntity": { "attributes": { "childAction": "", "modTs": "2017-08-17T02:27:52.937+00:00", "persistentOnReload": "true", "sysDefaultSVIAutostate": "enable", "descr": "", "dn": "sys/bd", "monPolDn": "uni/fabric/monfab-default", "status": "" }, "children": [ { "l2BD": { "attributes": { "modTs": "2017-09-09T23:58:01.301+00:00", "vlanmgrCfgState": "0", "status": "", "fwdMode": "bridge,route", "uid": "0", "epOperSt": "", "pcTag": "1", "type": "bd-regular", "monPolDn": "", "controllerId": "", "childAction": "", "BdOperName": "VLAN0300", "fwdCtrl": "mdst-flood", "id": "300", "mode": "CE", "rn": "bd-[vlan-300]", "media": "enet", "bdDefDn": "", "adminSt": "active", "vlanmgrCfgFailedBmp": "", "hwId": "0", "bridgeMode": "mac", "accEncap": "vxlan-10300", "createTs": "1970-01-01T09:00:00.000+00:00", "vlanmgrCfgFailedTs": "00:00:00:00.000", "name": "", "unkMacUcastAct": "proxy", "unkMcastAct": "flood", "fabEncap": "vlan-300", "ctrl": "", "persistentOnReload": "true", "BdState": "active", "operSt": "up" } } }, { "l2BD": { "attributes": { "modTs": "2017-09-09T23:57:01.911+00:00", "vlanmgrCfgState": "0", "status": "", "fwdMode": "bridge,route", "uid": "0", "epOperSt": "", "pcTag": "1", "type": "bd-regular", "monPolDn": "", "controllerId": "", "childAction": "", "BdOperName": "VLAN0100", "fwdCtrl": "mdst-flood", "id": "100", "mode": "CE", "rn": "bd-[vlan-100]", "media": "enet", "bdDefDn": "", "adminSt": "active", "vlanmgrCfgFailedBmp": "", "hwId": "0", "bridgeMode": "mac", "accEncap": "vxlan-10100", "createTs": "1970-01-01T09:00:00.000+00:00", "vlanmgrCfgFailedTs": "00:00:00:00.000", "name": "", "unkMacUcastAct": "proxy", "unkMcastAct": "flood", "fabEncap": "vlan-100", "ctrl": "", "persistentOnReload": "true", "BdState": "active", "operSt": "up" } } }, { "l2BD": { "attributes": { "modTs": "2017-09-10T13:32:39.908+00:00", "vlanmgrCfgState": "0", "status": "", "fwdMode": "bridge,route", "uid": "0", "epOperSt": "", "pcTag": "1", "type": "bd-regular", "monPolDn": "", "controllerId": "", "childAction": "", "BdOperName": "VLAN3901", "fwdCtrl": "mdst-flood", "id": "3901", "mode": "CE", "rn": "bd-[vlan-3901]", "media": "enet", "bdDefDn": "", "adminSt": "active", "vlanmgrCfgFailedBmp": "", "hwId": "0", "bridgeMode": "mac", "accEncap": "vxlan-50001", "createTs": "1970-01-01T09:00:00.000+00:00", "vlanmgrCfgFailedTs": "00:00:00:00.000", "name": "", "unkMacUcastAct": "proxy", "unkMcastAct": "flood", "fabEncap": "vlan-3901", "ctrl": "", "persistentOnReload": "true", "BdState": "active", "operSt": "0" } } } ] } } ] }
設定
同様にこんな感じのサンプルスクリプトで
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json base_url = 'http://10.0.0.230/api/' # create credentials structure name_pwd = {'aaaUser': {'attributes': {'name': 'admin', 'pwd': 'P@ssw0rd'}}} json_credentials = json.dumps(name_pwd) # log in to API login_url = base_url + 'aaaLogin.json' post_response = requests.post(login_url, data=json_credentials) # get token from login response structure auth = json.loads(post_response.text) login_attributes = auth['imdata'][0]['aaaLogin']['attributes'] auth_token = login_attributes['token'] # create cookie array from token cookies = {} cookies['APIC-Cookie'] = auth_token bd_url = base_url + 'node/mo/sys/bd.json' post_payload = { "bdEntity": { "children": [ { "l2BD": { "attributes": { "accEncap": "vxlan-10190", "fabEncap": "vlan-190", "pcTag": "1" }}}]}} post_response = requests.post(bd_url, data=json.dumps(post_payload), cookies=cookies, verify=False) print(json.dumps(post_response.json(), indent=2))
{ "imdata": [] }
設定が追加されている。しかし startup-config
に反映されているわけではないので、要注意。
torsw101a# show run vlan !Command: show running-config vlan !Time: Sun Oct 29 11:27:28 2017 version 7.0(3)I6(1) vlan 1,100,190,300,3901 vlan 100 vn-segment 10100 vlan 190 vn-segment 10190 vlan 300 vn-segment 10300 vlan 3901 vn-segment 50001
さっきの情報取得をもう一回やると、設定したものが増えているのが分かります。
また URL 末尾に options として query-target-filter
で条件指定も出来ます。この辺はまあ netconf で xml 扱うのと同じようなノリで。
>>> bd_url = base_url + 'node/mo/sys/bd.json?query-target==children&query-target-filter=eq(l2BD.id,"190")' >>> >>> get_response = requests.get(bd_url, cookies=cookies, verify=False) >>> print(json.dumps(get_response.json(), indent=2))
{ "imdata": [ { "l2BD": { "attributes": { "BdOperName": "VLAN0190", "monPolDn": "", "hwId": "0", "status": "", "createTs": "1970-01-01T09:00:00.000+00:00", "unkMacUcastAct": "proxy", "accEncap": "vxlan-10190", "adminSt": "active", "pcTag": "1", "uid": "62982", "epOperSt": "", "controllerId": "", "vlanmgrCfgState": "0", "fwdMode": "bridge,route", "vlanmgrCfgFailedBmp": "", "fwdCtrl": "mdst-flood", "type": "bd-regular", "dn": "sys/bd/bd-[vlan-190]", "BdState": "active", "bridgeMode": "mac", "bdDefDn": "", "name": "", "id": "190", "persistentOnReload": "true", "childAction": "", "mode": "CE", "modTs": "2017-10-29T11:27:05.932+00:00", "ctrl": "", "operSt": "down", "fabEncap": "vlan-190", "unkMcastAct": "flood", "media": "enet", "vlanmgrCfgFailedTs": "00:00:00:00.000" } } } ], "totalCount": "1" }
NX-API CLI
参照
JSON-RPC message format
JSON RPC
には cli
と cli_ascii
というコマンドタイプがあります。
レスポンスを JSON フォーマットで得られないような (bash コマンドとか)やつは、cli_ascii
の方を使わないとエラーになります。
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json payload = [ { "jsonrpc": "2.0", "method": "cli", "params": { "cmd": "show nve vni 50001 detail", "version": 1 }, "id": 1 } ] response = requests.post('http://10.0.0.230/ins', data=json.dumps(payload), headers={'content-type': 'application/json-rpc'}, auth=('admin', 'P@ssw0rd'), verify=False) print(json.dumps(response.json(), indent=2))
{ "id": 1, "jsonrpc": "2.0", "result": { "body": { "TABLE_nve_vni": { "ROW_nve_vni": { "svi-state": "UP [vrf-id: 3]", "vlan-bd": "3901", "if-name": "nve1", "prvsn-state": "add-complete", "mcast": "n/a", "vni-state": "Up", "cp-submode": "bgp", "flags": "", "type": "L3 [VRF001]", "mode": "control-plane", "vni": "50001" } } } } }
こんな感じで bash コマンドも打てます。
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json payload = [ { "jsonrpc": "2.0", "method": "cli_ascii", "params": { "cmd": "run bash df -h", "version": 1 }, "id": 1 } ] response = requests.post('http://10.0.0.230/ins', data=json.dumps(payload), headers={'content-type': 'application/json-rpc'}, auth=('admin', 'P@ssw0rd'), verify=False) print(json.dumps(response.json(), indent=2))
{ "result": { "msg": "Filesystem Size Used Avail Use% Mounted on\n/dev/root 8.9G 761M 8.1G 9% /\nnone 10M 1.4M 8.7M 14% /nxos/tmp\nnone 10M 2.9M 7.2M 29% /nxos/xlog\nnone 80M 9.9M 71M 13% /nxos/dme_logs\nnone 50M 3.0M 48M 6% /var/volatile/log\nnone 2.0M 12K 2.0M 1% /var/home\nnone 120M 364K 120M 1% /var/volatile/tmp\nnone 900M 256K 900M 1% /var/sysmgr\nnone 500M 60K 500M 1% /var/sysmgr/ftp\nnone 20M 0 20M 0% /var/sysmgr/srv_logs\nnone 2.0M 0 2.0M 0% /var/sysmgr/ftp/debug_logs\nnone 1.0G 349M 676M 35% /dev/shm\nnone 600M 61M 540M 11% /volatile\nnone 2.0M 16K 2.0M 1% /debug\nnone 1.0G 736K 1.0G 1% /mnt/ifc/cfg/db\n/dev/loop1 57M 57M 0 100% /isan_lib_ro\n/dev/loop2 65M 65M 0 100% /isan_bin_ro\n/dev/loop3 28M 28M 0 100% /isan_bin_eth_ro\n/dev/loop4 14M 14M 0 100% /isan_lib_eth_ro\n/dev/loop5 768K 768K 0 100% /isan_lib_n9k_ro\n/dev/loop6 128K 128K 0 100% /isan_bin_n9k_ro\nunionfs 8.9G 761M 8.1G 9% /isan/bin\nunionfs 8.9G 761M 8.1G 9% /isan/lib\n/dev/sda4 3.3G 1.1G 2.3G 33% /bootflash\n/dev/sda5 643M 18M 592M 3% /mnt/cfg/0\n/dev/sda6 643M 18M 592M 3% /mnt/cfg/1\n/dev/sda2 317M 12M 289M 4% /mnt/plog\nnone 400M 6.1M 394M 2% /var/sysmgr/startup-cfg\n/dev/loop7 49M 49M 0 100% /lc_ro\n/dev/loop8 41M 41M 0 100% /lc_n9k_ro\nunionfs 8.9G 761M 8.1G 9% /lc\n/dev/sda3 643M 19M 592M 4% /mnt/pss\n/dev/sda7 2.4G 80M 2.2G 4% /logflash\n/dev/loop9 57M 1.2M 53M 3% /bootflash/.rpmstore/patching\n" }, "id": 1, "jsonrpc": "2.0" }
bash ではデフォルト VRF での動作になるが Cisco Nexus 9000 Series NX-OS Programmability Guide, Release 7.x / Guest Shell 2.3 にあるように chvrf management
とかをいれて別 VRF から IP 通信系のコマンドも実行できます。
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json payload = [ { "jsonrpc": "2.0", "method": "cli_ascii", "params": { "cmd": "run guestshell chvrf management ping 10.0.0.231 -c 3", "version": 1 }, "id": 1 } ] response = requests.post('http://10.0.0.230/ins', data=json.dumps(payload), headers={'content-type': 'application/json-rpc'}, auth=('admin', 'P@ssw0rd'), verify=False) print(json.dumps(response.json(), indent=2))
{ "result": { "msg": "PING 10.0.0.231 (10.0.0.231) 56(84) bytes of data.\n64 bytes from 10.0.0.231: icmp_seq=1 ttl=255 time=0.663 ms\n64 bytes from 10.0.0.231: icmp_seq=2 ttl=255 time=0.733 ms\n64 bytes from 10.0.0.231: icmp_seq=3 ttl=255 time=0.568 ms\n\n--- 10.0.0.231 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 2001ms\nrtt min/avg/max/mdev = 0.568/0.654/0.733/0.073 ms\n" }, "jsonrpc": "2.0", "id": 1 }
JSON message format
JSON
には以下のような type
があります(名前で察せられる通り)
cli_show
cli_show_ascii
cli_conf
bash
JSON
の場合は payload の JSON key 順序によっては Wrong request message version, expecting 1.0 Request.is.rejected
と Status Code 400 を返してくる...。
仕方がないので Python の dict 型は避けて、単純に文字列で渡す。
(正直、この辺で「JSON-XML に出来なくて JSON で出来ること、がなければ...もうこいつとは付き合わなくていいかなー」と思い始めた)
こんな感じで、JSON-XML
の時と同じ出力を得られます。
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json payload = """{ "ins_api": { "version": "1.0", "type": "cli_show", "chunk": "0", "sid": "1", "input": "show version", "output_format": "json" } }""" response = requests.post('http://10.0.0.230/ins', data=payload, headers={'content-type':'application/json'}, auth=('admin', 'P@ssw0rd')) print(json.dumps(response.json(), indent=2))
#! /usr/bin/env python3 # -*- coding: utf-8 -*- import requests import json payload = """{ "ins_api": { "version": "1.0", "type": "bash", "chunk": "0", "sid": "1", "input": "df -h", "output_format": "json" } }""" response = requests.post('http://10.0.0.230/ins', data=payload, headers={'content-type':'application/json'}, auth=('admin', 'P@ssw0rd')) print(json.dumps(response.json(), indent=2))
binding library を探せ
NX-API REST
見つかりませんでした(完)。
NX-API CLI
以下 2017/10/30
現在の様子ですが。
- github / datacenter/nexus9000
- github / jedelman8/pycsco
- 以前は
napalm
でも下回りで使われていたっぽい (napalm
側のドキュメントを見た感じ) 2017/10/30
現在、最後の commit は2016/09/20
とか...数件溜まっている Issue や PR も進捗ない
- 以前は
- github / networktocode/pynxos
naparm
の下回りとして使われている- これ単体ではドキュメントなどもなさげ
- Ansible 公式 / Network Modules / Nxos
- 今のところこれが一番機能は揃っていそう
ちなみに napalm
で NX-OS を数分弄った感触は以下の通りです。
2年半ぶりにNAPALMを数分弄って(対象は Nexus9000v)、なんか標準 getter 少ないし下回りのライブラリを自分で呼ぶか、RestClient 使って機器の REST API 直接叩いた方が何ぼかマシでは...という感触を得た
— こてつ (@kakkotetsu) 2017年10月25日
単体の Network OS を相手にしたら、そりゃそういう感触になるのは当然か
— こてつ (@kakkotetsu) 2017年10月25日
getter が足りていないなら...って、こーゆーのはもうえーっちゅーねん pic.twitter.com/TdOBJAfxXt
— こてつ (@kakkotetsu) 2017年10月25日
おしまい
感想を好きに述べます。
- NX-API REST
- DN が最新バージョンで変わっていたりして、まだまだ開発中ではありそう
- それゆえにかライブラリはまだ無さげ (欲しい)
- データモデル的は Telemetry と共通の DME で、そのドキュメントは「よー分からんところもあるから試行錯誤」が必要なところもあった
- 参照・設定に関しては良くて、その他のオペレーション(ファイル操作・再起動 etc)に関しては不足している感
-
- とりあえずこれを使えば何でも出来そう
- 「この処理は API で出来ないから expect で...」というケースは避けられる
- CLI で NW 装置を扱っているネットワーク屋にも扱いやすい気がする
- ARISTA eAPI と使用感がほぼ同じなので...感想も一緒
NX-OS の NX-API CLI と EOS の eAPI を見ていると、君たち仲良くして一緒に標準化すれば?と言いたくなる
— こてつ (@kakkotetsu) 2017年9月13日
- とりあえずこれを使えば何でも出来そう
ライブラリ
- Junos でいうところの
PyEZ
みたいに特定 NOS に特化した良い感じに機能が揃ったやつはなさげ - 例えば ARISTA EOS の
pyeapi
なんかは、(インストール手順を見ると)リモート制御用サーバと EOS 側とどちらで動かせるようになっているので- 現状 On-box library として NX-OS 側に入っているライブラリを、リモート制御用サーバに入れて使うような展開もありうるかも??
- パブリックな情報を眺めていると、公式的には ACI 方面の開発優先に見える
- でもまあ HTTP Client としての最低限処理だけなら、ライブラリがなくとも...
- Junos でいうところの
まあ色々と言いましたが、選択肢が多くて驚きですね。
最後に、もう ncclient
とかでシコシコと NETCONF を弄る気にはならなかったんで、そこもライブラリ欲しいな...。