kakkotetsu

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 の構文に付き合うより楽な筈)というような時に使えそうです。

参考資料

これだけ

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 | comparenet commitcommit みたいなもんです、多分。

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 もあったんですねえ。
そんなわけで、今回は排他処理がどうこうとか面倒なことは一切していないユルフワな「やってみた」系の内容でした。