Cumulus VX の HTTP API を弄る
はじめに
やること
Cumulus Linux にも HTTP API というリモートから HTTP/HTTPS でアクセスして情報取得・設定変更・メンテナンス操作が出来る API があります。
ザックリ言うと「HTTP リクエストの中に CLI コマンドを入れて投げつける」系のやつです(Arista EOS eAPI や Cisco NX-OS の NX-API CLI と同じノリ)。
そういう作りな分、クライアント側の実装方式は割と何でも使えるのは嬉しい。
Cumulus Linux を相手にした時、実際のところ
- Ansible の各種 Linux 系 Modules (files, apt, systemd etc) による Linux 的な操作
- Ansible の Cumulus Linux 専用 Modules (nclu) による Network OS 的な操作
- Ansible 公式 / nclu module を参照 (Examples で雰囲気を察することができる)
- 内容は CLI で使う net 系コマンド(NCLU) を使ってコマンドを叩き込む expect 的なやつ(雑)
で完結できそうなのですが、後者相当のことを API でもできますよ、って話です。
繰り返し・条件判定などを要するちょっとした処理を、使い慣れた言語で使い捨てスクリプトを作ってサクッと済ませたい(人によっては Ansible Playbook の構文に付き合うより楽な筈)というような時に使えそうです。
参考資料
これだけ
- Cumulus Networks 公式 / HTTP API
- 設定方法と超基本的な使い方サンプル
- Cumulus Networks 公式 / SOLUTION / Automation
- Automation 周りの KB 色々
- Ansible とか Serverspec なんかの「サーバと同じように運用管理できるぜ!」って推している雰囲気の記事が多し
- HTTP API の話はなさげ
API Doc 的なやつは見つからず......。
環境情報
仮想版の Cumulus VX を使っております。
アカウントを作ればだれでも使える(2019/03/09 現在)やつで、使い方の公式リンクは 「Cumulus VX で VXLAN+EVPN (original : 2017/03/22) / 参考資料」あたりにも纏めているので、適宜ご参照下さいませ。
cumulus@tor000a:~$ net show system Hostname......... tor000a Build............ Cumulus Linux 3.7.3 Uptime........... 22 days, 21:36:43.650000 Model............ Cumulus VX Memory........... 426MB Disk............. 6GB Vendor Name...... Cumulus Networks Part Number...... 3.7.3 Base MAC Address. 0C:AA:FD:C7:3D:00 Serial Number.... 0c:aa:fd:c7:3d:00 Product Name..... VX cumulus@tor000a:~$ cat /etc/os-release NAME="Cumulus Linux" VERSION_ID=3.7.3 VERSION="Cumulus Linux 3.7.3" PRETTY_NAME="Cumulus Linux" ID=cumulus-linux ID_LIKE=debian CPE_NAME=cpe:/o:cumulusnetworks:cumulus_linux:3.7.3 HOME_URL="http://www.cumulusnetworks.com/" SUPPORT_URL="http://support.cumulusnetworks.com/"
API を突くクライアント側は...まー何でもいいですわ。
使う
事前処理 Cumulus Linux で機能有効化
Cumulus Linux 公式 / HTTP API に従ってやれば良し。
必要なパッケージは最初から入っていそうなので
cumulus@tor000a:~$ apt search python-cumulus-restapi Sorting... Done Full Text Search... Done python-cumulus-restapi/updates,now 0.1-cl3u9 all [installed] Rest API for Cumulus Networks
必要なサービスを起動して (認証方式や待ち受けポートなどなどは、必要に応じて nginx の設定ファイルを弄る、ここではデフォルトのまま)
cumulus@tor000a:~$ systemctl status restserver cumulus@tor000a:~$ sudo systemctl enable restserver cumulus@tor000a:~$ sudo systemctl start restserver cumulus@tor000a:~$ systemctl status restserver cumulus@tor000a:~$ systemctl status nginx
これだけ。
試用
まずは公式ガイドを参考に available な endpoint 一覧を取得。
endpoint を見ても、何となく機能が限られていそうな雰囲気(BGP とかを動かしているのに routing protocol 関係などが全く見当たらない)しか......。
cumulus@tor000a:~$ curl -X GET -k -u cumulus:CumulusLinux! https://127.0.0.1:8080 | python -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 573 100 573 0 0 12220 0 --:--:-- --:--:-- --:--:-- 12456
{
"endpoints": {
"bridgeCmd": "/ml2/v1/bridge/{bridge_name}/{vlan_id}",
"bridgeIntfCmd": "/ml2/v1/bridge/{bridge_name}/{vlan_id}/hosts/{host}",
"bridgeVxlanCmd": "/ml2/v1/bridge/{bridge_name}/{vlan_id}/vxlan/{vni_id}",
"hashCmd": "/ml2/v1/hash",
"intfCmd": "/ml2/v1/networks/{network_id}/hosts/{host}",
"main": "/",
"networkCmd": "/ml2/v1/networks/{network_id}",
"rpc": "/nclu/v1/rpc",
"vxlanCmd": "/ml2/v1/networks/{network_id}/vxlan/{vni}"
},
"version": {
"api_codename": "evo",
"api_status": "GA",
"api_version": "0.0.2",
"documentation": "http://docs.cumulusnetworks.com"
}
}
公式ガイドを参考に投げ込んでみると。
これは endpoint 的には /nclu/v1/rpc を使って CLI でいうところの net コマンドを包んで投げつけておきゃー大抵のこと出来るんだろうな、って察しました。
また、net コマンドでは json とつけると JUNOS の display json や EOS の | json みたいに JSON 形式で出力を得られたり得られなかったりします(全部得られるのか知らん)。
cumulus@tor000a:~$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "show counters"}' https://127.0.0.1:8080/nclu/v1/rpc
Kernel Interface table
Iface MTU Met RX_OK RX_ERR RX_DRP RX_OVR TX_OK TX_ERR TX_DRP TX_OVR Flg
------- ----- ----- ------- -------- -------- -------- ------- -------- -------- -------- -----
eth0 1500 0 216222 0 0 0 84162 0 0 0 BMRU
lo 65536 0 131 0 0 0 131 0 0 0 LRU
swp1 9216 0 127045 0 0 0 158455 0 0 0 BMRU
swp2 9216 0 108224 0 0 0 155806 0 0 0 BMRU
swp3 9216 0 127772 0 0 0 157187 0 0 0 BMRU
swp8 9216 0 74970 0 2 0 79474 0 0 0 BMRU
swp9 9216 0 72022 0 5 0 72909 0 0 0 BMRU
cumulus@tor000a:~$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "show counters json"}' https://127.0.0.1:8080/nclu/v1/rpc
{
"eth0": {
"Flg": "BMRU",
"MTU": 1500,
"Met": 0,
"RX_DRP": 0,
"RX_ERR": 0,
"RX_OK": 216459,
"RX_OVR": 0,
"TX_DRP": 0,
"TX_ERR": 0,
"TX_OK": 84284,
"TX_OVR": 0
},
"lo": {
...<snip>
設定変更系
雰囲気でやってますが、net pending は JUNOS でいうところの show | compare で net commit は commit みたいなもんです、多分。
cumulus@tor000a:/home/kotetsu$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "add time ntp server 10.0.0.64 iburst"}' https://127.0.0.1:8080/nclu/v1/rpc
cumulus@tor000a:/home/kotetsu$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "pending"}' https://127.0.0.1:8080/nclu/v1/rpc
--- /etc/ntp.conf 2018-09-05 14:20:56.000000000 +0900
+++ /run/nclu/ntp/ntp.conf 2019-03-09 20:31:28.736733340 +0900
@@ -53,10 +53,11 @@
#broadcast 192.168.123.255
# If you want to listen to time broadcasts on your local subnet, de-comment the
# next lines. Please do this only if you trust everybody on the network!
#disable auth
#broadcastclient
# Specify interfaces, don't listen on switch ports
interface listen eth0
+server 10.0.0.64 iburst
net add/del commands since the last "net commit"
================================================
User Timestamp Command
------- -------------------------- ----------------------------------------
cumulus 2019-03-09 20:31:28.738095 net add time ntp server 10.0.0.64 iburst
cumulus@tor000a:/home/kotetsu$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "commit"}' https://127.0.0.1:8080/nclu/v1/rpc
--- /etc/ntp.conf 2018-09-05 14:20:56.000000000 +0900
+++ /run/nclu/ntp/ntp.conf 2019-03-09 20:31:28.736733340 +0900
@@ -53,10 +53,11 @@
#broadcast 192.168.123.255
# If you want to listen to time broadcasts on your local subnet, de-comment the
# next lines. Please do this only if you trust everybody on the network!
#disable auth
#broadcastclient
# Specify interfaces, don't listen on switch ports
interface listen eth0
+server 10.0.0.64 iburst
net add/del commands since the last "net commit"
================================================
User Timestamp Command
------- -------------------------- ----------------------------------------
cumulus 2019-03-09 20:31:28.738095 net add time ntp server 10.0.0.64 iburst
cumulus@tor000a:/home/kotetsu$ curl -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "pending"}' https://127.0.0.1:8080/nclu/v1/rpc
cumulus@tor000a:/home/kotetsu$
Cumulus Linux ローカルで突いていますが、remote からも勿論突けます。
$ curl --noproxy "*" -X POST -k -u cumulus:CumulusLinux! -H "Content-Type: application/json" -d '{"cmd": "show counters json"}' https://tor000a:8080/nclu/v1/rpc
{
"eth0": {
"Flg": "BMRU",
"MTU": 1500,
"Met": 0,
"RX_DRP": 0,
"RX_ERR": 0,
"RX_OK": 223026,
"RX_OVR": 0,
"TX_DRP": 0,
"TX_ERR": 0,
"TX_OK": 85717,
"TX_OVR": 0
},
...<snip>
公式ガイドの最後に申し訳程度に載っていたサンプルをそのまま実行して /nclu/v1/rpc 以外の endpoint への PUT なんかも一応動くみたいだとはわかりましたが......。
いかんせんガイドもないので、何も分からん。
いわゆる普通の REST API っぽい雰囲気は醸し出しておりますが...。
cumulus@tor000a:~$ curl -X PUT -k -u cumulus:CumulusLinux! https://127.0.0.1:8080/ml2/v1/bridge/"br1"/200
""
cumulus@tor000a:~$ ip l sh br1
12: br1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether f6:81:d1:12:75:fa brd ff:ff:ff:ff:ff:ff
cumulus@tor000a:~$ bridge -d vlan
port vlan ids
br1 200
まあ /etc/network/interfaces に書き込まれちゃいないので、再起動すりゃー消えるんですが。
サンプルスクリプト
適当なサンプルとして以下のような処理をしてみます。
- 対象機器の BGP Neighbor を走査して
- 以下の全ての条件に合致する Neighbor だった場合は
- Local の Peer-Group が PG_SPINE である
- Remote の hostname が正規表現で "a$" に match しない (Remote の hostname capability が動いている前提)
- BGP clear する
......あんまり実用的ではない気はしますが、繰り返し処理や条件処理をして Ansible で書くのに僕が面倒くささを感じるあたり、を例示したものです。
言語的には HTTP リクエストと Response の JSON 構造パースあたりがあれば何でも良いので、好きなもの使えるよって言いたいだけ。(shell + curl + jq 程度でも)
ちなみに、あんまりシンプルだからか Cumulus Networks の Github リポジトリ を探してもラッパライブラリは見つからなかったです。
例示環境の対象 BGP 設定ですが、以下が /etc/frr/frr.conf より抜粋です。
interface swp1 description DEV=node1 IF=ens4 ! interface swp2 description DEV=node2 IF=ens4 ! interface swp3 description DEV=node3 IF=ens4 ! interface swp8 description DEV=spine000a IF=swp1 ! router bgp 4200000000 bgp router-id 172.31.0.0 bgp bestpath as-path multipath-relax bgp bestpath compare-routerid neighbor PG_K8S peer-group neighbor PG_K8S remote-as external neighbor PG_K8S description k8s-node neighbor PG_SPINE peer-group neighbor PG_SPINE remote-as external neighbor PG_SPINE description tor neighbor swp1 interface peer-group PG_K8S neighbor swp2 interface peer-group PG_K8S neighbor swp3 interface peer-group PG_K8S neighbor swp8 interface peer-group PG_SPINE neighbor swp9 interface peer-group PG_SPINE ! address-family ipv4 unicast redistribute connected exit-address-family !
んで、さっきの処理を今回は Ruby で書きました。
例によって、例外処理とかは適当な即席なやつで。
# encoding: utf-8 require 'rest-client' require 'optparse' require 'json' require 'pp' def exec_nclu(params, cmd) uri = "https://#{params[:target]}:8080/nclu/v1/rpc" payload = { 'cmd': cmd } headers = {content_type: 'application/json'} response = RestClient::Request.execute( :url => uri, :user => params[:username], :password => params[:password], :method => :post, :headers => headers, :verify_ssl => false, :payload => payload.to_json ) case response.code when 200 return JSON.parse(response) rescue "" else p response.code return nil end end # # ARGV check # # set default params arg_configs = { #:username => ENV['CUMULUS_USER'], #:password => ENV['CUMULUS_PASS'] } # required ARGV arg_required = [ :target, :username, :password ] # parse ARGV OptionParser.new do |opts| begin opts = OptionParser.new opts.on('-t', '--target STRING', "reaquired param. IP address or FQDN of Cumulus.") { |v| arg_configs[:target] = v } opts.on('-u', '--username STRING', "username of Cumulus.") { |v| arg_configs[:username] = v } opts.on('-p', '--password STRING', "password of Cumulus.") { |v| arg_configs[:password] = v } opts.parse!(ARGV) for field in arg_required raise ArgumentError.new("recuired param #{field} is not set...") if arg_configs[field].nil? end rescue => e puts opts.help puts puts e.message exit 1 end end # # main # begin # clean proxy env if I need ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"].each do |environment| ENV.delete(environment) end bgp_nei = exec_nclu(arg_configs, "show bgp neighbor json") exit if bgp_nei.nil? pp bgp_nei # for debug # key : value (portname : BGP Nei status) bgp_nei.each do |port,value| if value["peerGroup"] != "PG_SPINE" then puts "#{port} BGP peerGroup : #{value["peerGroup"]} good bye." next end puts "#{port} BGP peerGroup : #{value["peerGroup"]}" if value["hostname"] =~ Regexp.new("a$") then puts "#{port} BGP Remote Hostname : #{value["hostname"]} is system A, good bye." next end puts "#{port} BGP Remote Hostname : #{value["hostname"]} , OK, Let's clear!!" clear_bgp = exec_nclu(arg_configs, "clear bgp #{port}") puts "#{port} BGP Remote Hostname : #{value["hostname"]}, executed clear!!" if clear_bgp == "" end rescue Exception=>e puts e puts e.backtrace puts "\n\n!! Oh!! Exception!!\n\n" end
んで、これを実行すると
$ bundle exec ruby cumulus.rb -t tor000a -u cumulus -p CumulusLinux!
{"swp1"=>
{"bgpNeighborAddr"=>"172.30.0.1",
"remoteAs"=>4290000001,
"localAs"=>4200000000,
"nbrExternalLink"=>true,
"peerGroup"=>"PG_K8S",
"bgpVersion"=>4,
"remoteRouterId"=>"172.16.37.60",
"bgpState"=>"Established",
"bgpTimerUp"=>8561000,
"bgpTimerUpMsec"=>8561000,
"bgpTimerUpString"=>"02:22:41",
"bgpTimerUpEstablishedEpoch"=>1552010935,
"bgpTimerLastRead"=>1000,
"bgpTimerLastWrite"=>1000,
"bgpInUpdateElapsedTimeMsecs"=>8561000,
"bgpTimerHoldTimeMsecs"=>9000,
"bgpTimerKeepAliveIntervalMsecs"=>3000,
"neighborCapabilities"=>
{"4byteAs"=>"advertisedAndReceived",
"addPath"=>
{"IPv4 Unicast"=>{"txReceived"=>true, "rxAdvertisedAndReceived"=>true}},
"routeRefresh"=>"advertisedAndReceivedNew",
"multiprotocolExtensions"=>
{"IPv4 Unicast"=>{"advertisedAndReceived"=>true}},
"hostName"=>{"advHostName"=>"tor000a", "advDomainName"=>"n/a"},
"gracefulRestart"=>"advertisedAndReceived",
"gracefulRestartRemoteTimerMsecs"=>120000,
"addressFamiliesByPeer"=>{"IPv4 Unicast"=>{}}},
"gracefulRestartInfo"=>
{"endOfRibSend"=>{"IPv4 Unicast"=>true},
"endOfRibRecv"=>{"IPv4 Unicast"=>true}},
...<snip>
"swp9"=>
{"bgpNeighborAddr"=>"fe80::eaa:fdff:fe52:8e01",
"remoteAs"=>4210000001,
"localAs"=>4200000000,
"nbrExternalLink"=>true,
"hostname"=>"spine000b",
"peerGroup"=>"PG_SPINE",
"bgpVersion"=>4,
"remoteRouterId"=>"172.31.1.1",
"bgpState"=>"Established",
"bgpTimerUp"=>31000,
"bgpTimerUpMsec"=>31000,
"bgpTimerUpString"=>"00:00:31",
"bgpTimerUpEstablishedEpoch"=>1552019465,
"bgpTimerLastRead"=>1000,
"bgpTimerLastWrite"=>1000,
"bgpInUpdateElapsedTimeMsecs"=>29000,
"bgpTimerHoldTimeMsecs"=>9000,
"bgpTimerKeepAliveIntervalMsecs"=>3000,
"neighborCapabilities"=>
{"4byteAs"=>"advertisedAndReceived",
"addPath"=>{"IPv4 Unicast"=>{"rxAdvertisedAndReceived"=>true}},
"extendedNexthop"=>"advertisedAndReceived",
"extendedNexthopFamililesByPeer"=>{"IPv4 Unicast"=>"recieved"},
"routeRefresh"=>"advertisedAndReceivedOldNew",
"multiprotocolExtensions"=>
{"IPv4 Unicast"=>{"advertisedAndReceived"=>true}},
"hostName"=>
{"advHostName"=>"tor000a",
"advDomainName"=>"n/a",
"rcvHostName"=>"spine000b",
"rcvDomainName"=>"n/a"},
"gracefulRestart"=>"advertisedAndReceived",
"gracefulRestartRemoteTimerMsecs"=>120000,
"addressFamiliesByPeer"=>"none"},
"gracefulRestartInfo"=>
{"endOfRibSend"=>{"IPv4 Unicast"=>true},
"endOfRibRecv"=>{"IPv4 Unicast"=>true}},
...<snip>
swp1 BGP peerGroup : PG_K8S good bye.
swp2 BGP peerGroup : PG_K8S good bye.
swp3 BGP peerGroup : PG_K8S good bye.
swp8 BGP peerGroup : PG_SPINE
swp8 BGP Remote Hostname : spine000a is system A, good bye.
swp9 BGP peerGroup : PG_SPINE
swp9 BGP Remote Hostname : spine000b , OK, Let's clear!!
swp9 BGP Remote Hostname : spine000b, executed clear!!
とまあ、条件に合致した swp9 の Neighbor だけが BGP clear されましたとさ。めでたしめでたし。
おしまい
Cumulus Linux って名前からして「設定ファイルを書き換えて systemctl で操作!Linux と同じ管理ツールが使えて嬉しいな!!!以上!!」って感じかと勝手に思っていたんですが、こんな風にネットワーク屋さん向けなユルフワ(失礼)な API もあったんですねえ。
そんなわけで、今回は排他処理がどうこうとか面倒なことは一切していないユルフワな「やってみた」系の内容でした。