kakkotetsu

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 上で Python スクリプトを動かせるやつ
    • NX-OS 内には謹製ライブラリが仕込まれている(多分上記の NX-API CLI を内部的には使っている感じ)
    • なお動作環境は Python 2.7

参考資料

試用する

環境情報と準備

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 には clicli_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 現在の様子ですが。

ちなみに napalm で NX-OS を数分弄った感触は以下の通りです。

おしまい

感想を好きに述べます。

  • NX-API REST
    • DN が最新バージョンで変わっていたりして、まだまだ開発中ではありそう
    • それゆえにかライブラリはまだ無さげ (欲しい)
    • データモデル的は Telemetry と共通の DME で、そのドキュメントは「よー分からんところもあるから試行錯誤」が必要なところもあった
    • 参照・設定に関しては良くて、その他のオペレーション(ファイル操作・再起動 etc)に関しては不足している感
  • NX-API CLI

    • とりあえずこれを使えば何でも出来そう
      • 「この処理は API で出来ないから expect で...」というケースは避けられる
    • CLI で NW 装置を扱っているネットワーク屋にも扱いやすい気がする
    • ARISTA eAPI と使用感がほぼ同じなので...感想も一緒
  • ライブラリ

    • Junos でいうところの PyEZ みたいに特定 NOS に特化した良い感じに機能が揃ったやつはなさげ
    • 例えば ARISTA EOS の pyeapi なんかは、(インストール手順を見ると)リモート制御用サーバと EOS 側とどちらで動かせるようになっているので
      • 現状 On-box library として NX-OS 側に入っているライブラリを、リモート制御用サーバに入れて使うような展開もありうるかも??
    • パブリックな情報を眺めていると、公式的には ACI 方面の開発優先に見える
    • でもまあ HTTP Client としての最低限処理だけなら、ライブラリがなくとも...

まあ色々と言いましたが、選択肢が多くて驚きですね。
最後に、もう ncclient とかでシコシコと NETCONF を弄る気にはならなかったんで、そこもライブラリ欲しいな...。