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

Arista + Openconfigbeat で試す OpenConfig gNMI ベース Telemetry

はじめに

やること

個人でもサクッと入手できる実装が整ってきている感じなので、軽く試してみます。
これまでは gRPC server 側に追加パッケージが必要かつ個人アカウントでは取得できない、とか gRPC client の実装が大変(& 個人で実装している特定 NOS 向け野良とか、メーカが出している自社 NOS 専用のはあったけど)とかで手を出しにくかったんですが、今やそんなこともなさそーだという。

以下やること概要

  • OpenConfig の Telemety 動作を試す
    • gNMI (gRPC Network Management Interface) ベースで動かす
    • gRPC client (collector) 側
      • Arista の Openconfigbeat という OpenConfig 準拠(っぽい)ものを使う
        • Elastic の Beats というフレームワークで実装されているので、Elasticsearch / Kibana との連携が楽
      • gRPC server に subscribe して streaming push を受け取り、Elasticsearch にデータを叩き込む
    • gRPC server 側
      • Arista vEOS
        • OpenConfig gNMI ベースで動かすのが手っ取り早そう & openconfigbeat のテストにはきっと EOS を使っているに違いない(Inteoperability でハマりにくそう)と推測したから
    • データ保持・視覚
      • Elasticsearch + Kibana
        • Beats ベースな openconfigbeat と組み合わせて使うのに、一番手っ取り早かったから

参考資料

環境情報

Arista EOS 側

いつも通りの vEOS-lab

vEOS01#show ver
Arista vEOS
Hardware version:
Serial number:
System MAC address:  0c00.da26.7071

Software image version: 4.20.1F
Architecture:           i386
Internal build version: 4.20.1F-6820520.4201F
Internal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91

Uptime:                 21 minutes
Total memory:           2017260 kB
Free memory:            1232576 kB

Openconfigbeat 側

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

$ uname -a
Linux ocb 4.15.0-23-generic #25-Ubuntu SMP Wed May 23 18:02:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Openconfigbeat をビルドするのに go と glide が必要です(が GitHub にビルド済のファイルもあるし、Docker でも動くようなので、動かし方によっては必要ない)

$ go version
go version go1.10.1 linux/amd64

$ glide --version
glide version 0.13.1-3

とってきた Openconfigbeat は以下

$ git show
commit 32d071a8bdaf7f7f2b3aa8e504d171c888a14113 (HEAD -> master, origin/master, origin/HEAD)
Author: Giuseppe Valente <gvalente@arista.com>
Date:   Wed Mar 14 12:21:13 2018 -0700

    docker: download latest release

    Change-Id: I3015b84b6f438bba8ddb884e2f332fad4f5e16e1

Elasticsearch + Kibana 環境

Elasticsearch と Kibana は docker でテキトーに動かします。母艦のサーバとしては、Openconfigbeat と相乗りです。

$ docker --version
Docker version 18.03.1-ce, build 9ee9f40

$ docker-compose --version
docker-compose version 1.21.2, build a133471

Elasticsearch も Kibana も 6.2.4

$ docker ps
CONTAINER ID        IMAGE                                                 COMMAND                  CREATED             STATUS              PORTS                              NAMES
99fc3355e3b6        docker.elastic.co/kibana/kibana:6.2.4                 "/bin/bash /usr/loca…"   4 days ago          Up 4 days           0.0.0.0:5601->5601/tcp             kibana
69663eaabc79        docker.elastic.co/elasticsearch/elasticsearch:6.2.4   "/usr/local/bin/dock…"   4 days ago          Up 4 days           0.0.0.0:9200->9200/tcp, 9300/tcp   elasticsearch

$ curl -XGET 'http://localhost:9200'
{
  "name" : "r5cOGL4",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "wMLRO2rxT0aUX2WrcybkSw",
  "version" : {
    "number" : "6.2.4",
    "build_hash" : "ccec39f",
    "build_date" : "2018-04-12T20:37:28.497551Z",
    "build_snapshot" : false,
    "lucene_version" : "7.2.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

動かす

Elasticsearch + Kibana 環境 構築

今回は openconfigbeat と同じサーバ上で virtualenv で動かしている docker-compose を使って立ち上げました。以下が使った docker-compose.yml

version: '3'
services:
  elasticsearch:
    container_name: elasticsearch
    image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4
    environment:
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata1:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
  kibana:
    container_name: kibana
    image: docker.elastic.co/kibana/kibana:6.2.4
    ports:
      - 5601:5601
    environment:
      SERVER_NAME: ocb
      ELASTICSEARCH_URL: http://<母艦のIPアドレス>:9200

volumes:
  esdata1:
    driver: local

vEOS 側

ARISTA EOS Central / OpenConfig 4.20.2.1F Release Notes を参考に gNMI ベースで動かす設定を入れるだけです。

vEOS01(config)#management api gnmi
vEOS01(config-mgmt-api-gnmi)#transport grpc ?
  WORD  transport name
vEOS01(config-mgmt-api-gnmi)#transport grpc TEST-vEOS01


vEOS01#bash
[kotetsu@vEOS01 ~]$
[kotetsu@vEOS01 ~]$ ss -natu | grep 6030
tcp    LISTEN     0      1024     :::6030                 :::*

これで Openconfigbeat (gRPC client)からの Subscribe 要求を受ける server 動作をしている筈。
なお、Openconfigbeat 側からの user / password 認証を受けられるように、アカウントは作っておきます(ssh とかして設定している時点で、それは済んでいる筈...)。

ちなみに、プロセスやらログやらを眺めていると vEOS 上で動く server 実装も Go みたいです。今まで vEOS 上で動くもろもろって Python ばかりだった気がしますが。

Openconfigbeat 側

ビルド環境準備

前述の通り GitHub にビルド済のファイルもあるし、Docker でも動くようなので、動かし方によっては必要ないですが。今回はビルドからやっていきます。
まずは Openconfigbeat をビルドするための requirements に入っている go と glide をば。以下を参考にパパッと。

$ sudo apt install -y software-properties-common
$ sudo add-apt-repository ppa:gophers/archive
$ sudo apt install -y golang-1.10-go
$ echo "export PATH=$PATH:/usr/lib/go-1.10/bin/" | sudo tee /etc/profile.d/golang.sh

$ sudo apt install -y golang-glide

本筋ではないですが、久々に glide を使ったら前述の glide README に以下の表記が...。

The Go community now has the dep project to manage dependencies. Please consider trying to migrate from Glide to dep. If there is an issue preventing you from migrating please file an issue with dep so the problem can be corrected. Glide will continue to be supported for some time but is considered to be in a state of support rather than active feature development.

設定作成~ビルド

まずは GitHub から clone してきて

$ mkdir -p /home/kotetsu/go/src/github.com/aristanetwork
$ cd /home/kotetsu/go/src/github.com/aristanetworks/
$ git clone https://github.com/aristanetworks/openconfigbeat.git
$ cd openconfigbeat/

<git clone してきた dir>/_meta/beat.yml が設定ファイルなので、必要な情報を書き換えます。

$ more ~/go/src/github.com/aristanetworks/openconfigbeat/_meta/beat.yml
################### Openconfigbeat Configuration Example #########################

############################# Openconfigbeat ######################################

openconfigbeat:

  # The addresses of the OpenConfig devices to connect to.
  addresses: ["10.0.0.171"]

  # The OpenConfig paths to subscribe to.
  paths: ["/"]

  # The default port to connect to if none is configured.
  default_port: 6030

  # The username on the device.
  username: "kotetsu"

  # The password for the user on the device.
  password: "kotetsu"

上の例だと、以下の感じ。

  • openconfigbeat
    • gRPC server の情報
    • addresses
      • vEOS の IP アドレスを配列で書き連ねる
    • paths
      • suvscribe するツリーの指定
      • / にしておけば、とれるもんは全部送ってもらえるようになる筈
    • default_port
      • vEOS 側で設定変更していないデフォルトなら 6030
    • usernamepassword
      • vEOS のアカウント情報

また <git clone してきた dir>/openconfigbeat.ymlデフォルト値っぽいのが定義されているので そこに定義されている output.elasticsearch: hosts: ["localhost:9200"] は明示的に指定しておりません。(今回は Kibana も Openconfigbeat とサーバ相乗りしているので)

何も指定せずに <git clone してきた dir>/make すればビルドされて openconfigbeat というファイルが出来ます。

$ make
$ file openconfigbeat
openconfigbeat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=33438dfebfda1d7a42d3a0ad3db140fbf8b47ff8, stripped
$ chmod go-w openconfigbeat.yml

Usage

$ ~/go/src/github.com/aristanetworks/openconfigbeat/openconfigbeat -h
Usage:
  openconfigbeat [flags]
  openconfigbeat [command]

Available Commands:
  export      Export current config or index template
  help        Help about any command
  keystore    Manage secrets keystore
  run         Run openconfigbeat
  setup       Setup index template, dashboards and ML jobs
  test        Test config
  version     Show current version info

Flags:
  -E, --E setting=value      Configuration overwrite
  -N, --N                    Disable actual publishing for testing
  -c, --c string             Configuration file, relative to path.config (default "openconfigbeat.yml")
      --cpuprofile string    Write cpu profile to file
  -d, --d string             Enable certain debug selectors
  -e, --e                    Log to stderr and disable syslog/file output
  -h, --help                 help for openconfigbeat
      --httpprof string      Start pprof http server
      --memprofile string    Write memory profile to this file
      --path.config string   Configuration path
      --path.data string     Data path
      --path.home string     Home path
      --path.logs string     Logs path
      --plugin pluginList    Load additional plugins
      --setup                Load the sample Kibana dashboards
      --strict.perms         Strict permission checking on config files (default true)
  -v, --v                    Log at INFO level

Use "openconfigbeat [command] --help" for more information about a command.

以下で設定確認

$ ~/go/src/github.com/aristanetworks/openconfigbeat/openconfigbeat export config
openconfigbeat:
  addresses:
  - localhost
  default_port: 6042
  paths:
  - /
output:
  elasticsearch:
    hosts:
    - localhost:9200
path:
  config: /home/kotetsu/go/src/github.com/aristanetworks/openconfigbeat
  data: /home/kotetsu/go/src/github.com/aristanetworks/openconfigbeat/data
  home: /home/kotetsu/go/src/github.com/aristanetworks/openconfigbeat
  logs: /home/kotetsu/go/src/github.com/aristanetworks/openconfigbeat/logs
setup:
  kibana: null

起動

先ほどビルドしたものを実行して、起動します。以下のようにやれば、フォアグラウンド動作・stdout に PUSH されてきた情報など垂れ流されます。

$ ./openconfigbeat -e -d openconfigbeat.go

様子を見る

ここからは Kibana で様子を見ていきます。

設定空っぽの Kibana の WebUI にアクセスすると「はよ Index 作れ」と促されるので、言われるがままに Openconfigbeat が投げ込んでくるやつ用の Index Pattern を作っておきます。

f:id:kakkotetsu:20180722220910p:plain

そうすると、Discover のところでデータがズラズラと出てくるので

f:id:kakkotetsu:20180722220853p:plain

ポチポチと様子見用の search とか Visualize とか Dashboard を作りました。今回は「vEOS の特定物理インターフェースを流れる in-octets と in-unicast-pkts (累計)」を。
何で累計かというと、Kibana で Timelion とか使って bps なり pps の算出設定をするのが面倒くさかったからです。(個人的に、その手の設定は Grafana の方がなんぼか楽)

で、平常時は LLDP と BGP KeepAlive くらいしか流れていない当該物理インターフェースに、13s 程度だけトラフィックを流してみると

f:id:kakkotetsu:20180722220928p:plain

16:06:02 - 16:06:15 くらいの間だけ、vEOS 側がインターフェースカウンタの上昇に合わせて 1s 間隔程度で push してきてくれている様子が見て取れました。(グラフの丸部分の間隔がその時だけ狭まっている)

おしまい

ダラダラと所感を。

  • gNMI と YANG に準拠してくれている(っぽい) OpenConfigbeat ならば、それに準拠している NOS ならば一元的に collector 動作してくれそうな匂いを感じとった
    • 何せ vEOS しか試せていないので...
  • Elastic の Beats というフレームワークは(自分で実装したわけじゃないけど)、fluentd プラグインと同じように使えそう
    • Ruby より Go が良いとか、fluentd のバージョン対応どうしようとか、そういう開発者には良いのかも
  • Telemetry でよく言われる「バーストトラフィック検知」は Openconfig ベースなものでも実現できそうな気がする
  • 2018/07/22 時点の感触としては、データモデルが YANG 準拠な分、メーカ独自に自分たちの NOS 向けに作りこんでいる仕組みには取得できる情報観点では及ばなさそう
    • 一例として、以前試した 「Nexus9000v で Telemetry」 だと Cisco 独自のデータモデルを使っているので、結構細かい情報も取れていた
  • 目的がハッキリしているならば、現時点でもそれなりに使えそうな感はある
    • 例えば「SNMP Get や API での監視で 1-5 min 間隔なインターフェースカウンタや細かいデータ(NOS や HW に応じた監視項目や設定に応じた table 情報)を取得し、xflow で通信内容の傾向をつかめるようにはなっている」「が、バーストトラフィックの検知ができていないからそこを何とかしたい」というような場合、こういうのがピッタリと嵌りそう
    • 逆に、何もかもこの仕組みで賄おうとするのは(当面は)夢見すぎ感ある

ThousandEyes で NW 装置 monitoring を Free Try

はじめに

やること

ShowNet 2018 で「ネットワーク可視化 SaaS」として使われていた ThousandEyes を試用します(Free Try で)。
見る機能は「SNMP 対応 NW 装置から情報収集してインターネット経由で SaaS 環境に投げ込む probe 的な Enterprise Agents を動かして様子を見る(Devices 機能)」部分に絞ります。ザッと公式ページを見た感じ、本機能はオマケっぽいですが。

こんなんが見えるところまで。

f:id:kakkotetsu:20180617232453p:plain

構成概要・環境情報

以下の通りです。

  • 宅内
    • KVM+GNS3 上で以下を動かす
      • ThousandEyes の Enterprise Agents
        • 物理 NW では NAPT で The Internet に到達可能になっている
        • こいつ自体では The Internet に到達する為のネットワーク設定をするくらいで、ほとんど弄ることはない
        • こいつが SNMP manager になるので、以下の収集対象 NW 装置と疎通可能にしておくこと
      • 収集対象 NW 装置
        • 今回は Arista の vEOS-lab を使っているが、SNMP と LLDP が動けば何でも良い筈
  • SaaS 環境のダッシュボード
    • 設定や参照は全てここでやる
    • The Internet

今回物理装置や KVM+GNS3 部分の構築には触れません。Enterprise AgentsESXi とかベアメタルサーバとか Docker とかでも動くので、そこの動かし方はやりやすいようにやれば良かろうかと。
Cisco IOS XEJuniper NFX 上の専用コンテナとかもあるみたいですが、今回弄ってないです。

参考資料

  • ThousandEyes 公式
    • top
      • Try It Free というボタンがデカデカとあるので、ありがたく使わせて頂く
    • Pricing
      • 今回試用するフリー版で出来ること、課金すると出来ること、が並んでいる
      • Devices 機能観点では、課金することでカスタム MIB を使ってより多くの情報を収集可能になりそう
    • Knowledge Base
      • マニュアル
  • その他紹介記事など

動かす

環境準備

ThousandEye Free Try 登録

まずは ThousandEyes 公式Try It Free を押して、ユーザ登録します。2018/06/17 現在、個人ユーザ + Web メールでも弾かれたりせず。

f:id:kakkotetsu:20180617232412p:plain

メールが来るので、レジスター用のリンクから本登録しておわり。
公式ページ上の Login リンクからダッシュボードに飛べます。

ThousandEye Enterprise Agents 取得 ~ GNS3にインポートして起動 ~ 初期設定

probe として動く Enterprise Agents のイメージは、ダッシュボードで SETTINGS -> Agents -> Enterprise Agents と進めばダウンロードできます。
今回は Virtual Appliance を選択して OVA を頂いてきました。

f:id:kakkotetsu:20180617232607p:plain

ダウンロードした ova ファイルを展開して、qcow2 に変換して KVM+GNS3 が動いている母艦に放り込みます。

$ ll thousandeyes-va-0.126.ova
-rw-r--r-- 1 kotetsu kotetsu 965096448 Jun 16 23:43 thousandeyes-va-0.126.ova

$ tar -xvf thousandeyes-va-0.126.ova
thousandeyes-va-64-16.04.ovf
thousandeyes-va-64-16.04-disk1.vmdk

$ ll thousandeyes-va-*
-rw-r--r-- 1 kotetsu kotetsu 965096448 Jun 16 23:43 thousandeyes-va-0.126.ova
-rw-r--r-- 1 kotetsu kotetsu 965087744 May 10 05:23 thousandeyes-va-64-16.04-disk1.vmdk
-rw-r--r-- 1 kotetsu kotetsu      6261 May 10 05:21 thousandeyes-va-64-16.04.ovf

$ qemu-img convert -f vmdk -O qcow2 thousandeyes-va-64-16.04-disk1.vmdk thousandeyes-va-64-16.04.qcow2

$ file thousandeyes-va-*
thousandeyes-va-0.126.ova:           POSIX tar archive
thousandeyes-va-64-16.04-disk1.vmdk: VMware4 disk image
thousandeyes-va-64-16.04.ovf:        XML 1.0 document, ASCII text, with very long lines
thousandeyes-va-64-16.04.qcow2:      QEMU QCOW Image (v3), 21474836480 bytes

ThousandEyes 公式 KB / How to set up the Virtual Appliance あたりと、ova に入っていた ovf の中身を参考に以下の感じで GNS3 にデプロイ。

f:id:kakkotetsu:20180617232625p:plain

f:id:kakkotetsu:20180617232634p:plain

f:id:kakkotetsu:20180617232642p:plain

以下、生情報。

$ ps -ef | grep [T]housand
root      8862  5214  2 13:32 pts/12   00:10:10 /usr/bin/qemu-system-x86_64 -name ThousandEyes-va-64-16.04-1 -m 2048M -smp cpus=1 -enable-kvm -machine smm=off -boot order=c -drive file=/home/kotetsu/GNS3/projects/thousandeyes/project-files/qemu/2c910cdc-3d96-4de7-b8f5-42541188cd17/hda_disk.qcow2,if=ide,index=0,media=disk -uuid 2c910cdc-3d96-4de7-b8f5-42541188cd17 -serial telnet:127.0.0.1:5007,server,nowait -monitor tcp:127.0.0.1:41113,server,nowait -net none -device e1000,mac=0c:00:da:cd:17:00,netdev=gns3-0 -netdev socket,id=gns3-0,udp=127.0.0.1:10031,localaddr=127.0.0.1:10030 -nographic

ネットワークインターフェースは、The Internet に到達できるように適宜 GNS3Cloud とかを使って繋いでおきます。
あとは、起動してコンソールを見ていると Enterprise Agents の初期 URL (DHCPで得た IP アドレスに http で)と初期アカウント・パスワードが出るので、それに従って WebUI でアクセスします。

f:id:kakkotetsu:20180617232716p:plain

ポチポチして、ログインパスワードや Static IP アドレスや DNS サーバや NTP サーバや Default Gateway 情報や ssh ログイン用の公開鍵を設定します。
試していないですが、ssh で入って様子を見ると Ubuntu 16.04 なので、WebUI で出来ない細かい設定も可能かもです。

f:id:kakkotetsu:20180617232741p:plain

Enterprise Agents がインターネット上の SaaS サービスに情報を送ることができていれば、ダッシュボードにこのエージェントが登場します。

f:id:kakkotetsu:20180617232835p:plain

f:id:kakkotetsu:20180617232849p:plain

SNMP + LLDP が動く NW 機器を GNS3 で動かす

何か適当にどうぞ(クソ雑)。
今回は以下の通り Arista の vEOS-lab を 3 個並べておきました。

Enterprise AgentsSNMP manager として情報を取得にくるので、そこの到達性を確保し、SNMP 設定はしておきます。あとは LLDP を動かしておくとトポロジ情報も出してくれるので、動かしておきます。

f:id:kakkotetsu:20180617232918p:plain

ダッシュボードで色々やる

Devices の登録

Enterprise Agents から情報収集する対象(Devices というらしい)の NW 装置を設定してみます。

まずは Device Credentials として収集対象の SNMP Community 設定を登録して

f:id:kakkotetsu:20180617233027p:plain

Device Settings にて Find New Devices で対象の IP アドレスや先の Device Credentials や、どの Enterprise Agents を manager として使うか、を入力すると

f:id:kakkotetsu:20180617233055p:plain

以下のように登録されます。

f:id:kakkotetsu:20180617233120p:plain

情報収集対象の Interface を選択するときには Monitored Interfaces 設定のチェックボックスをポチポチと。
interval5 minutes から変えられないっぽいですね......。

Views で Device Layer を眺める

これで何となくそれっぽいのが出力されている筈なので、ダラダラと様子を見ていきます。

Topology として LLDP ベースで勝手にそれらしい絵が出来ています(今回は vEOS 側で管理インターフェースである Ma1 の LLDP を無効化してみた)。良いねー。
なお、上の時間軸を弄ることで特定時刻の Topology や Interface Metrics の様子を見ることができます。

f:id:kakkotetsu:20180617233143p:plain

Topology はノードを DD で動かすこともできます。

2 本接続して ECMP させているリンクは、こんな風にもなるし

f:id:kakkotetsu:20180617233159p:plain

ワンクリックでバラして見ることもできます。

f:id:kakkotetsu:20180617233229p:plain

今回、仮想環境だから Et インターフェースの speed 値を取得できていなかったので、まともに動かせたのは Ma1 だけだったのですが、Highlighting で「この時間帯に帯域の XX % 以上使っていたリンクを赤くする」ってのを出来ます。まあ、interval は 5 minutes なんですが......。

Interface Metric の様子...は、まあ単に標準 ifMib 見ているだけですね。無課金なので。

f:id:kakkotetsu:20180617233245p:plain

f:id:kakkotetsu:20180617233258p:plain

通知系設定

Devices にできる設定としては以下があるようです。

  • Devices -> Notification Rules
    • トリガ
      • 対象 Devices への到達性変化 (SNMP Get の失敗とか)
      • 対象 Devices のインターフェースの増減
    • 通知方法
      • メール
      • 各種 Webhooks
      • Slack や Hipchat など
  • Alerts -> Alert Rules
    • トリガ
      • インターフェースの以下が特定の条件・閾値を超えた時
        • Throughput
        • Error
        • Discards
        • Admin Status
        • Operational Status
    • 通知方法は上のと一緒

標準 MIB ベースで SNMP Manager が拾えるトリガなので、このくらいでしょうねー。
以下設定例です。Topology の Highlighting 機能で赤くするやつだと「speed の X%」っていう設定しか出来ないのですが、こちらでは Mbps での設定も出来るようです。

f:id:kakkotetsu:20180617233316p:plain

検知を WebUI で参照した例

f:id:kakkotetsu:20180617233432p:plain

で、View の Device Layer でも Alert は時系列でオレンジ色で出してくれています。

f:id:kakkotetsu:20180617233443p:plain

おしまい

以下ダラダラtp所感です。

  • WebUI は綺麗だしサクサク (この規模だと)
  • 1000台単位で Devices を長期間動かした時の様子を見てみたい
    • Topology が一体どうなってしまうのか
    • インターフェース数はどこまで拾えるのか
    • 中身は rrd 的な動作してるのか・データの持ち方どうなっているのか(SaaS側?)
  • 5 minutes interval 固定だとすると、「メンテナンス時や障害時に設計通りにトラフィックが切り替わったかのリアルタイム/事後確認」用途には使いづらいかも (バースト検知とかは言うまでもなく...)
  • LLDP の自動描画系のは割と聞くけど、良い
    • 静的に描画するものでも描画設定ファイルを自動生成すれば良かろう、だけど面倒くさいことはしたくない
    • Network Weathermap とかで手をかけて人間が分かりやすいものをちゃんと作ってやるか、こういうので手抜きしてそれなりにやるか、ってのは情報量とか用途次第かな...
  • 適用対象によっては 「SaaS 型である」という一点で一発アウトなやつかも
  • 無課金の Devices 機能は「標準 MIB の SNMP で出来ること」が限度なので、こんなもんでしょうなー
    • 多分今回見ていない RIPE Atlas みたいな機能がメインなんだと思う
    • でもオマケにしてはよく出来ていた

ARISTA vEOS に jq を入れて使う (小ネタ)

小ネタです。twitter で「Arista に jq 欲しい」って話を見かけたので「確かに vEOS デフォルトで入ってなくて、入れりゃー使えるけど(この記事)...最初から入っていて欲しいよなあ」というだけのお話。

環境

いつも通り(3年ぶり)の vEOS ですぞな。

localhost#show version
Arista vEOS
Hardware version:
Serial number:
System MAC address:  0021.96aa.9b52

Software image version: 4.20.1F
Architecture:           i386
Internal build version: 4.20.1F-6820520.4201F
Internal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91

Uptime:                 2 minutes
Total memory:           2017260 kB
Free memory:            1259404 kB

[admin@localhost ~]$ uname -a
Linux localhost 3.18.28.Ar-6765725.4201F #1 SMP PREEMPT Wed Nov 15 09:47:13 PST 2017 x86_64 x86_64 x86_64 GNU/Linux

インストール

前述の環境では一応 /etc/yum 配下が揃っていそうなのですが、ササッと binary ファイル取ってきて PATH 通ったところに放り込む感じで。

まず curlインターネッツからとってこれるように DNS キャッシュサーバと Ma1 の IP アドレスくらいを EOS で設定して。

ip name-server vrf default <DNSキャッシュサーバの IP address>
!
interface Management1
   ip address <EOS の IPv4 アドレス(DHCPでも)>
!
ip route 0.0.0.0/0 <インターネッツに抜ける Gateway IP address>

bash におりて binary ファイルを取ってくるだけ。

localhost#bash

Arista Networks EOS shell

[admin@localhost ~]$
[admin@localhost tmp]$ cd /var/tmp
[admin@localhost tmp]$ curl -LO https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64

[admin@localhost tmp]$ ll
total 2964
drwxrwxrwt 4 root  root         100 Nov 29 13:30 agents
drwxrwxrwx 2 root  root          40 Nov 29 13:19 cli
-rw-r--r-- 1 admin eosadmin 3027945 Nov 29 13:40 jq-linux64
-rw-rw-rw- 1 root  root           2 Nov 29 13:20 startup-config.loaded

[admin@localhost tmp]$ sudo cp jq-linux64 /bin/jq
[admin@localhost tmp]$ sudo chmod 755 /bin/jq

[admin@localhost ~]$ which jq
/bin/jq

EOS から使う

後はお好きなように。EOS シェル側に戻れば、ほいこの通り。

localhost#show int | json | jq '.interfaces[] | [.name, .lineProtocolStatus]'
[
  "Management1",
  "up"
]
[
  "Ethernet2",
  "up"
]
[
  "Ethernet3",
  "up"
]
[
  "Ethernet1",
  "up"
]

最後に

僕も EOS にデフォルトで入れておいて欲しいです。 (EOS ならばお手軽に eAPI を突いてスクリプト言語json ライブラリで処理する、のが常道なのかも知れないけど...shell だけでチョチョイとやりたいことも多々あるよね)

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 を弄る気にはならなかったんで、そこもライブラリ欲しいな...。

Nexus9000v で VxLAN+EVPN (MAC Mobility Extended Community 簡易動作確認編)

最初に

やること

先日の記事(Nexus9000v で VxLAN+EVPN (anycast gateway 編)) で、EVPN で学習した MAC アドレステーブルを見た僕が「Seq No があるってことは MAC Mobility Extended Community が使えるんじゃないのか!?」と口走っていたので、その簡易動作確認をします。

EVPN MAC Mobility ?

RFC 7432 (BGP MPLS-Based Ethernet VPN) / 15. MAC Mobility に書かれています。

EVPN PE を跨ぐような LiveMigration や Flapping が発生した際に、網内の PE 達が当該 MAC アドレスの最新の居場所(どの PE 配下にあるか)を正しく把握するために Sequence No を埋め込んで使う Extended Community ... それが MAC Mobility Extended Community です。 今回は確認できませんでしたが、RFCによれば同じ Sequence No が複数 PE から来た場合には PE の IP アドレスが小さい方を選択...てなルールもあるようです。

なーんて、僕の拙い日本語よりも、"EVPN MAC Mobility" で Google 画像検索でもすれば、Juniper さんあたりの分かりやすい図面が出てきますよ(雑)。

環境情報 / 事前準備

先日の記事(Nexus9000v で VxLAN+EVPN (anycast gateway 編)) のまま、完全シングルホーム環境。

なお、後述のシナリオのために torsw101atorsw201aEt 1/3 に以下設定をした上で、

torsw101a# show run int et 1/3

!Command: show running-config interface Ethernet1/3
!Time: Sun Oct 22 17:31:42 2017

version 7.0(3)I6(1)

interface Ethernet1/3
  description DEV=ostinato IF=Port0
  switchport access vlan 100
torsw201a# show run int et 1/3

!Command: show running-config interface Ethernet1/3
!Time: Sun Oct 22 17:31:49 2017

version 7.0(3)I6(1)

interface Ethernet1/3
  description DEV=ostinato IF=Port1
  switchport access vlan 100

トラフィックジェネレータとして Ostinato を接続してあります。
一応軽く GNS3 で手っ取り早く準備できるトラフィックジェネレータである Ostinato の参考資料も以下に載せておきます。

f:id:kakkotetsu:20171022220554p:plain

簡易動作確認

シナリオ

こんな風に様子を見ます。

  1. EVPN PE である torsw101atorsw201a それぞれの Et 1/3 (VNI 10100 にマッピングされる VLAN を割当済)に、同一 Src MAC address(11:11:11:11:11:11) の ARP トラフィックを流し続ける
  2. 以下を見る
    • torsw101atorsw201a 間に流れるパケット(主にEVPN 周りの UPDATE)
    • torsw101atorsw201a での EVPN MAC アドレス学習情報 遷移

結果

MAC Mobility Extended Community シーケンス

拾ったパケットを追っていくと、以下の感じ (thanks WebSequenceDiagrams)

f:id:kakkotetsu:20171022220617p:plain

今回 PE x2 のみで Ostinato でチンタラトラフィックを流している環境では、Sequence No 2 以上が使われることはなかったです。
各 loop の最後に MAC Mobility Extended Community なしの(= Sequence No 0 扱い) UPDATE が出るのは RFC で以下の記載があるからかも知れません。

In order to process mobility events correctly, an implementation MUST handle scenarios in which sequence number wraparound occurs.

上記の loop を 4 回繰り返した後、30s 程度の間は一切の UPDATE を双方が出さなくなりました。

f:id:kakkotetsu:20171023000406p:plain

これはどうやら「180s の間に 5 回 move が発生したら 30s の hold timer を発動する(カスタマイズ可能)」というデフォルト値動作によるものみたいです。(以下の本いわく)

Building Data Centers with VXLAN BGP EVPN: A Cisco NX-OS Perspective (Networking Technology)

Building Data Centers with VXLAN BGP EVPN: A Cisco NX-OS Perspective (Networking Technology)

こんな syslog も出ていた

2017 Oct 22 11:40:17 torsw101a  %USER-2-SYSTEM_MSG: Detected duplicate host 1111.1111.1111, topology 100, during Local update, with host located at remote VTEP 198.18.1.21, VNI 10100 - l2rib

次に MAC Mobility Extended Community で Sequence No = 1 で UPDATE 吐いているパケットをピックアップすると以下の感じ

f:id:kakkotetsu:20171022220736p:plain

RFC 7432 (BGP MPLS-Based Ethernet VPN) / 7.7. MAC Mobility Extended Community にある format を見ると、Sticky/static を示す Flags が 0 になっています。この Flags の用途は RFC 7432 (BGP MPLS-Based Ethernet VPN) / 15.2. Sticky MAC Addresses の通り、MAC address の移動が起こりえない環境で 1 をたててアラートをあげるような使い方を想定している模様。
NX-OS の設定で変更できるのかは調査した限りでは分からずです。余談ですが、例えば Juniper の実装だと Juniper 公式 / EVPN MAC Pinning Overview のように、この Flags を有効化する設定で MAC アドレス遷移事故を防ぐようなことも出来る(当然、ライブマイグレーションを使わないなどの制約と引き換えに)ようです。

MAC アドレス学習状況

先のシーケンス図と比較しながら見ていきましょう。(余談ですがこれ、拾うタイミングが結構シビアでした。)

torsw101a (VTEP 用の lo IP アドレス = 198.18.1.11) 配下に当該 MAC address があると思っている torsw201a だったが

torsw201a# show l2route evpn mac all

Flags -(Rmac):Router MAC (Stt):Static (L):Local (R):Remote (V):vPC link
(Dup):Duplicate (Spl):Split (Rcv):Recv (AD):Auto-Delete(D):Del Pending (S):Stale (C):Clear
(Ps):Peer Sync (O):Re-Originated

Topology    Mac Address    Prod   Flags         Seq No     Next-Hops
----------- -------------- ------ ------------- ---------- ----------------
100         1111.1111.1111 BGP    Rcv           0          198.18.1.11

自分配下から同 MAC address を学習し、それを Sequence No = 1 として扱い

torsw201a# show l2route evpn mac all

Flags -(Rmac):Router MAC (Stt):Static (L):Local (R):Remote (V):vPC link
(Dup):Duplicate (Spl):Split (Rcv):Recv (AD):Auto-Delete(D):Del Pending (S):Stale (C):Clear
(Ps):Peer Sync (O):Re-Originated

Topology    Mac Address    Prod   Flags         Seq No     Next-Hops
----------- -------------- ------ ------------- ---------- ----------------
100         1111.1111.1111 Local  L,            1          Eth1/3
100         1111.1111.1111 BGP    D             0          198.18.1.11

多分 torsw101a が WithDrawn を送ってくれたタイミングで、Next-Hops 198.18.1.11 の経路を削除しつつ、自分が持っている経路の Sequence No を 0 にリセットして再度 UPDATE を送っている。

torsw201a# show l2route evpn mac all

Flags -(Rmac):Router MAC (Stt):Static (L):Local (R):Remote (V):vPC link
(Dup):Duplicate (Spl):Split (Rcv):Recv (AD):Auto-Delete(D):Del Pending (S):Stale (C):Clear
(Ps):Peer Sync (O):Re-Originated

Topology    Mac Address    Prod   Flags         Seq No     Next-Hops
----------- -------------- ------ ------------- ---------- ----------------
100         1111.1111.1111 Local  L,            0          Eth1/3
torsw201a#

なお、タイミング次第では Dup フラグを観測できたこともありました。

torsw101a# show l2route evpn mac all

Flags -(Rmac):Router MAC (Stt):Static (L):Local (R):Remote (V):vPC link
(Dup):Duplicate (Spl):Split (Rcv):Recv (AD):Auto-Delete(D):Del Pending (S):Stale (C):Clear
(Ps):Peer Sync (O):Re-Originated

Topology    Mac Address    Prod   Flags         Seq No     Next-Hops
----------- -------------- ------ ------------- ---------- ----------------
100         1111.1111.1111 Local  L,Dup,        1          Eth1/3
100         1111.1111.1111 BGP    Dup,Rcv       0          198.18.1.21

「Seq No があるってことは MAC Mobility Extended Community が使えるんじゃないのか!?」という自分自身の問いに対して「何となく動いていそうな雰囲気はあるぞ」と答えておきます。

それはそれとして、この Sequence No がリセットする間もなくブリバリカウントアップしていくような環境を今回は作れずでしたが、まぁそんな恐ろしい環境には関わりたくないお気持ちでございます。

Nexus9000v で Telemetry

最初に

やること/サマリ

  • NX-OS の Telemetry 機能を軽く様子見
    • 細かいカスタマイズをシコシコやっていると、まるで盆栽のように終わりがなかったので、ほんの触り
    • 送信側は以下のように動かす
      • 送信プロトコル gRPC
        • 選択肢としては HTTP や TCPUDP も可能らしい
      • エンコード方式 GPB(Google protocol buffer)
        • 選択肢としては JSON も可能らしい
      • データコレクタタイプ DME(Data Management Engine)
        • メーカがプレ定義したスキーマに従う (netconf schema やら SNMP MIB やら...を思い浮かべて頂ければと)
        • show コマンドの結果を NX-OS API を使って出すこともできるらしい
    • 受信側は以下のように動かす
      • gRPC サーバ、GPB デコード、Elasticsearch への転送をしてくれる Cisco の Receiver を使う
      • Elasticsearch にデータを溜め込み、Kibana で可視化(クエリの定義などシコシコシコシコ...) これも Cisco が用意しているものをベースにする

今回の完成図はこんなところまで

f:id:kakkotetsu:20171009140610p:plain

構成

前回の構成 と一緒
BGP + VxLAN + EVPN あたりが動いている torSW101a を送信側として使います。

なお、receiver 側は各スイッチの management ポートと通信可能なところに、適当なサーバを置いておきます。

参考資料

機器側からの送信に関してはマニュアルとデータ構造(詳細仕様までは分からないが...)が揃っています。
受信に関しては、インストールしてノンカスタマイズで綺麗に見えるようなメーカ謹製系のやつは見当たらなかったので、メーカが提供している Docker Hub 上のそれっぽいのを。

環境情報

受信側サーバ (前述の Cisco Docker image を動かす)

Docker インストールは Get Docker CE for Ubuntu あたりを参考に済ませているものとして。

$ uname -a
Linux receiver 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"

$ docker --version
Docker version 17.09.0-ce, build afdb6d4

$ docker-compose --version
docker-compose version 1.16.1, build 6d1ac21

NX-OS

例によって 4GB メモリで限界を狙う。

torsw101a# show version

...

NX-OSv9K is a demo version of the Nexus Operating System

Software
  BIOS: version
  NXOS: version 7.0(3)I6(1)
  BIOS compile time:
  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]


Hardware
  cisco NX-OSv Chassis
   with 4037916 kB of memory.
  Processor Board ID 90SNLUQJ25I

  Device name: torsw101a
  bootflash:    3509454 kB
Kernel uptime is 28 day(s), 7 hour(s), 12 minute(s), 9 second(s)

構築~動作確認

Telemetry Receiver 初期設定

以下、どちらも同じサーバ上で動かします。
何度も立ち上げなおしたり恒久的に動かすものでもないので docker-compose は使ってないです。

dockercisco/elklat インストール~サービス起動

まずは Docker Hub の公式手順 ままで pull ~ run ~ 初期設定

$ docker pull dockercisco/elklat
$ docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
dockercisco/elklat              latest              826e2f062fc4        10 months ago       6.54GB


$ docker run -d -p 5601:5601 -p 9200:9200 -p 5044:5044 -it 826e2f062fc bash

$ docker exec -it  cranky_sinoussi service elasticsearch start
$ docker exec -it  cranky_sinoussi service elasticsearch status
 * elasticsearch is running

$ docker exec -it  cranky_sinoussi service kibana start
$ docker exec -it  cranky_sinoussi service kibana status
 * kibana is running

これで http://<Docker母艦IPアドレス>:5601/ で Kibana の画面を見られる筈です。

Elasticsearch の mapping 設定(最低限)

Kibana で各 _source@timestamp を拾うにあたって、Elasticsearch に格納されているどのフィールドを使うか...という設定をするのですが。
この後入れる telemetryreceiver が送り付けてくるデータ構造は、postDate というフィールドが何故か Unix Time 形式になってしまっています。
このまま Elasticsearch で自動的に mapping が生成されると postDate が Kibana 上で String やら Integer やらとして解釈されてしまいます。
仕方ないので、ここだけは手動で mapping を作ってやります。

まず、既に過去のデータが telemetry という index に格納されているので、これを掃除して

$ curl -XDELETE <Docker母艦IPアドレス>:9200/telemetry

そのうえで以下のように mapping を定義

$ curl -X PUT -H "Content-Type: application/json" -d @- <<EOT http://<Docker母艦IPアドレス>:9200/_template/tmpl_telemetry
{
  "template": "telemetry*",
  "mappings": {
    "modify": {
      "properties": {
          "postDate": {
            "type": "date",
            "format": "strict_date_optional_time||epoch_millis"
          }
      }
    }
  }
}
EOT

dockercisco/telemetryreceiver インストール

2017/10/08 時点で見たところ latest の更新日時が 2017/10/06 になっていて、絶賛開発中の模様です。
で、なんとなく嫌な予感がして一世代前の v4 を入れました。(深い理由はないです)

$ docker pull dockercisco/telemetryreceiver:v4

$ docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
dockercisco/telemetryreceiver   latest              fbbc29139f5b        2 days ago          1.1GB
dockercisco/telemetryreceiver   v4                  454c1e98fcbb        7 weeks ago         1.1GB
dockercisco/elklat              latest              826e2f062fc4        10 months ago       6.54GB

$ docker run -d -p 50001:50001 -it fbbc29139f5b bash
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                                                                    NAMES
a70cce08a261        fbbc29139f5b        "bash"              46 seconds ago      Up 45 seconds       0.0.0.0:50001->50001/tcp                                                 dreamy_lamarr
68a5f661093b        826e2f062fc         "bash"              6 minutes ago       Up 6 minutes        0.0.0.0:5044->5044/tcp, 0.0.0.0:5601->5601/tcp, 0.0.0.0:9200->9200/tcp   cranky_sinoussi

$ docker exec -it  vigilant_benz /grpc/telemetry/src/telemetry_receiver 50001 <Docker母艦IPアドレス> 9200 1
Server listening on 0.0.0.0:50001

最後の receiver プログラムを動かすやつは、公式手順では 末尾 "&" でバックグラウンド動作させています。
フォアグラウンドで動かしておくと、標準出力で以下のように NX-OS からの受信状況をリアルタイムで見るのに便利なので、そうしただけ。

Received GPB RPC with Data size is: 237550 Total RPC count:4926
Received GPB RPC with Data size is: 9043 Total RPC count:4927
Received GPB RPC with Data size is: 4627 Total RPC count:4928
Received GPB RPC with Data size is: 237550 Total RPC count:4929
Received GPB RPC with Data size is: 9043 Total RPC count:4930
Received GPB RPC with Data size is: 4627 Total RPC count:4931
Received GPB RPC with Data size is: 237550 Total RPC count:4932

Telemetry Sender 設定

NX-OS 側の設定を。
7.0(3)I6(1) では、エージェントインストールなど特にする必要なく、単純に CLI で feature 有効化~送信設定をするだけです。

torsw101a# show run section telemetry
show running-config | section telemetry

feature telemetry
telemetry
  destination-group 100
    ip address <Docker母艦IPアドレス> port 50001 protocol gRPC encoding GPB
  sensor-group 100
    path sys/bgp depth unbounded
    path sys/bd depth unbounded
    path sys/epId-1/nws depth unbounded
  subscription 600
    dst-grp 100
    snsr-grp 100 sample-interval 10000

sensor-group にて Cisco 公式 / Cisco Nexus 3000 and 9000 Series Telemetry Sources や実際に送信されるデータ内容を見比べながら、シコシコシコシコとカスタマイズをしていくことになります。

Kibana 初期設定(index)

ここまでで「NX-OS がデータを送信して、telemetry_receiverがデコード~Elasticsearchに格納」ってところまではできている筈です。
まあ必要に応じて、Elasticsearch に $ curl -XGET <Docker母艦IPアドレス>:9200/telemetry/_search -d '{"query" : { "match_all" : {} }}' | python3 -m json.tool とかすれば、格納されている情報がザッと(デフォルトは10件まで)見える。

なので、今度は Kibana の設定をば。
telemetry_receiver はデフォルトで telemetry という名前で index 作っているので、それを拾う設定をします。
以下のように、既に届いているデータ構造の中から Time-field name として postDate が選択肢に現れるので、それを選択します。(先の mapping 設定 at Elasticsearch がちゃんとしていれば)

f:id:kakkotetsu:20171009010547p:plain

これで画面上側の Discover タブを選んで、左上の index 選択で telemetry を選んでやれば、ザーッと右側にデータが並びます。

f:id:kakkotetsu:20171009010617p:plain

Kibana / NX-OS でカスタマイズ

ここからは、以下のようなカスタマイズをひたすらに繰り返していくことになります。

  • どんな情報を NX-OS から送信して
  • どんな情報をどんな条件で Kibana で Visualize して
  • どんな風に Kibana で Dashboard を見るか

既に Kibana の画面上で Settings > Objects を見ると各種 VisualizationDashboard が並んでいます。
が、これは別にそのまま使えるわけではなく(何かのデモで使ったものをベースにしているのか、開発中のものなのか)、少なくとも以下のようなカスタマイズが必要です。

  • KibanaVisualizations
    • indextelemetry 以外が指定されていたりするので、適宜変更
      • なお telemetry_receiverlatest では index も自分で指定できるようになっていたので、もう少しカスタマイズがきく筈
    • node_id_str という Field (要はNX-OS側のホスト名)がハードコードされていたりするので、適宜変更
    • 送信側である程度条件を絞っている、という前提がある(ものもある)ようで、送信側を雑に設定した時には期待通りの値を得られないので適宜変更
    • 仮想版では正常に得られないような情報もありそうなので適宜修正
    • etc etc
  • NX-OSsensor-group path
    • 送信したい情報を選択
      • どの階層にどの情報があるのか、マニュアルだけだとちゃんと分からないので試行錯誤

それでも、受信側をゼロから全てやっていくよりは大分マシだと思いますが。

例えば、以下のように事前定義されている Visualizations Object が並んでいますが

f:id:kakkotetsu:20171009010644p:plain

その一つを編集画面はこんな感じで、「あー、この情報を NX-OS 側から送信するのね」とかやっていきます。

f:id:kakkotetsu:20171009010718p:plain

んで、自分向けのカスタマイズをして Visualizations の一つが最低限の正しい情報が見られるようになって...

f:id:kakkotetsu:20171009010756p:plain

それを繰り返していけば、デフォルトの Dashboard も少しずつ情報が埋まっていくし、自分好みのものも作れるでしょう。(折角なので時系列なやつも見られるようにしたいですよね)

f:id:kakkotetsu:20171009010819p:plain

おしまい ~ここからが本当の地獄だ...!!~

最後に述べたような盆栽カスタマイズをしていて、メーカ謹製の受信側アプリケーションがあると楽が出来るのかなーと。(SNMPの時代から変わらないですが)
Kibana も Elasticsearch も大規模環境でお守りをしていく...ことを考えると、なかなかしんどい。この辺はまあ、自分で頑張るか金で何とかするかの話ですね...。
あと、特に受信側はコンピュータリソースをかなり食うので、得たい・得るべき情報とそのコストを天秤にかけて運用に乗せるには結構手をかけねばな、という感触です。

はい、グダグダ言ってないで盆栽弄りに戻ります...。