SDN開発エンジニアを目指した活動ブログ

〜SDNなオープンソース製品を実際に使って試してみる〜

Pythonベースで、GoBGPを制御してみる 〜gRPC活用編〜

[ 2016.4.30修正:GoBGP最新版への対応 ]
GoBGPは、golangベースのBGPエンジンであり、経路追加などの設定コマンド等も、golangで実装されております。
今回は、Ryu SDN Frameworkとの連携を見据えて、PythonベースでGoBGPを制御する手法を試してみたいと思います。
元ネタは、こちらになります。
github.com

◆ まずは、構築準備から ...

(1) GoBGP環境の準備
前回のブログ記事に従って、PythonからgRPCを扱えるように環境を準備します。
ttsubo.hatenablog.com

(2) BGPルータ環境の準備
GoBGPと相互接続するBGPルータを準備します。

f:id:ttsubo:20151018165000j:plain

この段階で、BGPルータ側のBGPテーブルの状態を確認しておきます。

R1#show bgp ipv4 unicast
BGP table version is 42, local router ID is 192.168.0.1
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter,
              x best-external, a additional-path, c RIB-compressed,
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
 *>  20.1.0.0/24      0.0.0.0                  0         32768 i

(3) GoBGP動作開始
GoBGPを起動します。

$ cd $HOME
$ cd golang/bin/
$ cat gobgpd.conf 
[global]
 [global.config]
   as = 65002
   router-id = "192.168.0.2"

[[neighbors]]
 [neighbors.config]
   peer-type = "external"
   neighbor-address = "192.168.0.1"
   peer-as = 65001
   local-as = 65002
[[neighbors.afi-safis]]
 [neighbors.afi-safis.config]
   afi-safi-name = "ipv4-unicast"

$ sudo ./gobgpd -f gobgpd.conf 
... (snip)

この時、BGPルータとGoBGPとの間のリンクで、BGP UPDATEメッセージにより"Prefix情報"が伝搬される様子を確認しておきます。
ここでの確認ポイントとしては、

  • "20.1.0.0/24"は、Network Layer reachability information(通称、nlri)のフィールドに設定されている。
  • "Nexthop"や、"ORIGIN"などのBGPアトリビュートは、Path attributesのフィールドに設定させている。

をチェックしておいてください。
f:id:ttsubo:20151018172216p:plain

(4) GoBGP側でのBGPテーブル確認
先ほどのBGP UPDATEメッセージが、GoBGP側で反映できたことを確認しておきます。

$ gobgp global rib
    Network             Next Hop             AS_PATH              Age        Attrs
*>  20.1.0.0/24         192.168.0.1          65001                00:03:22   [{Origin: i} {Med: 0}]

以上で、下準備が完了しました。

◆ gRPC基本動作の概要を理解する

GoBGPの内部構造として活用しているgRPCのインタフェース条件として、IDLファイルの内容を確認しておきます。

$ cd $GOPATH/src/github.com/osrg/gobgp/api
$ cat gobgp.proto 

syntax = "proto3";

package gobgpapi;

// Interface exported by the server.

service GobgpApi {
  rpc GetGlobalConfig(Arguments) returns (Global) {}
  rpc ModGlobalConfig(ModGlobalConfigArguments) returns (Error) {}
  rpc GetNeighbors(Arguments) returns (stream Peer) {}
  rpc GetNeighbor(Arguments) returns (Peer) {}
  rpc ModNeighbor(ModNeighborArguments) returns(Error) {}
  rpc GetRib(Table) returns (Table) {}
  rpc Reset(Arguments) returns (Error) {}
  rpc SoftReset(Arguments) returns (Error) {}
  rpc SoftResetIn(Arguments) returns (Error) {}
  rpc SoftResetOut(Arguments) returns (Error) {}
  rpc Shutdown(Arguments) returns (Error) {}
  rpc Enable(Arguments) returns (Error) {}
  rpc Disable(Arguments) returns (Error) {}
  rpc ModPath(ModPathArguments) returns (ModPathResponse) {}
  rpc ModPaths(stream ModPathsArguments) returns (Error) {}
  rpc MonitorRib(Table) returns (stream Destination) {}
  rpc MonitorBestChanged(Arguments) returns (stream Destination) {}
  rpc MonitorPeerState(Arguments) returns (stream Peer) {}
  rpc GetMrt(MrtArguments) returns (stream MrtMessage) {}
  rpc ModMrt(ModMrtArguments) returns (Error) {}
  rpc ModBmp(ModBmpArguments) returns (Error) {}
  rpc GetRPKI(Arguments) returns (stream RPKI) {}
  rpc ModRPKI(ModRpkiArguments) returns (Error) {}
  rpc GetROA(Arguments) returns (stream ROA) {}
  rpc GetVrfs(Arguments) returns (stream Vrf) {}
  rpc ModVrf(ModVrfArguments) returns (Error) {}
  rpc GetDefinedSet(DefinedSet) returns (DefinedSet) {}
  rpc GetDefinedSets(DefinedSet) returns (stream DefinedSet) {}
  rpc ModDefinedSet(ModDefinedSetArguments) returns (Error) {}
  rpc GetStatement(Statement) returns (Statement) {}
  rpc GetStatements(Statement) returns (stream Statement) {}
  rpc ModStatement(ModStatementArguments) returns (Error) {}
  rpc GetPolicy(Policy) returns (Policy) {}
  rpc GetPolicies(Policy) returns (stream Policy) {}
  rpc ModPolicy(ModPolicyArguments) returns (Error) {}
  rpc GetPolicyAssignment(PolicyAssignment) returns (PolicyAssignment) {}
  rpc ModPolicyAssignment(ModPolicyAssignmentArguments) returns (Error) {}
}

… (snip)

今回のブログ記事では、二つのAPIの挙動に着目します。

  • "MonitorBestChanged" : GoBGPで保持しているBGPテーブルの経路情報の変化の有無を監視する
  • "ModPath" : GoBGPへの経路情報の設定を行う


ちなみに、これらのAPIを動作させる際には、事前に、引数/戻り値パラメータ定義を紐解く必要があります。
引数/戻り値パラメータとして入れ子定義されているパラメータを追っかけていくと、”Path"フィールドにたどり着きます。

message Path {
    bytes nlri = 1;
    repeated bytes pattrs = 2;
    int64 age = 3;
    bool best = 4;
    bool is_withdraw = 5;
    int32 validation = 6;
    bool no_implicit_withdraw = 7;
    uint32 family = 8;
    uint32 source_asn = 9;
    string source_id = 10;
    bool filtered = 11;
    bool stale = 12;
    bool is_from_external = 13;
    string neighbor_ip = 14;
}

ここでの確認ポイントは、

  • Pathフィールドのメンバとして、"nlri"および、"pattrs"が定義されている。
  • "nlri"および、"pattrsの型は、"bytes"と定義されている。

すなわち、Pathフィールドの"nlri"および、"pattrs"は、BGP UPDATEメッセージのBGPアトリビュートに対応しております。
よって、"pattrs"は、ORIGIN等のBGPアトリビュートによって構成されることになります。
また、BGPアトリビュートに対応する各パラメータに値を設定する際には、"bytes"型に対応したバイナリ変換が必要になります。
今回は、RyuBGPのPacketパーサを活用してバイナリ変換に対応しました。

◆ BGPテーブル監視用サンプルアプリを試してみる

まずは、Pythonスクリプトから、RPCメソッド:”MonitorBestChanged”を動作させてみます。
任意のディレクトリを作成して、次のサンプルアプリを配備してください。

$ cd $HOME
$ mkdir sample
$ cd sample/
$ vi monitor_BestPathChange.py
-----------------
import gobgp_pb2
import sys
import signal
import time
import os
from threading import Thread
from ryu.lib.packet.bgp import IPAddrPrefix
from ryu.lib.packet.bgp import _PathAttribute
from ryu.lib.packet.bgp import BGPPathAttributeOrigin
from ryu.lib.packet.bgp import BGPPathAttributeAsPath
from ryu.lib.packet.bgp import BGPPathAttributeMultiExitDisc
from ryu.lib.packet.bgp import BGPPathAttributeNextHop
from ryu.lib.packet.bgp import BGPPathAttributeCommunities
from grpc.beta import implementations

_TIMEOUT_SECONDS = 1000

AFI_IP = 1
SAFI_UNICAST = 1
RF_IPv4_UC = AFI_IP<<16 | SAFI_UNICAST

def run(gobgpd_addr, routefamily):
    channel = implementations.insecure_channel(gobgpd_addr, 50051)
    with gobgp_pb2.beta_create_GobgpApi_stub(channel) as stub:

        ribs = stub.MonitorBestChanged(gobgp_pb2.Arguments(family=routefamily),
                                                           _TIMEOUT_SECONDS)

        for rib in ribs:
            paths_target = rib.paths
            for path_target in paths_target:
                nlri = IPAddrPrefix.parser(path_target.nlri)
                print "----------------------------"
                print (" Rib.prefix     : %s" % nlri[0].prefix)
                for pattr in path_target.pattrs:
                    path_attr = _PathAttribute.parser(pattr)
                    if isinstance(path_attr[0], BGPPathAttributeOrigin):
                        print (" Rib.origin     : %s" % path_attr[0].value)
                    elif isinstance(path_attr[0], BGPPathAttributeAsPath):
                        if path_attr[0].type == 2:
                            print(" Rib.aspath     : %s" % path_attr[0].value)
                        else:
                            print(" Rib.aspath     : ???")
                    elif isinstance(path_attr[0], BGPPathAttributeMultiExitDisc):
                        print (" Rib.med        : %s" % path_attr[0].value)
                    elif isinstance(path_attr[0], BGPPathAttributeNextHop):
                        print (" Rib.nexthop    : %s" % path_attr[0].value)
                    elif isinstance(path_attr[0], BGPPathAttributeCommunities):
                        for community in path_attr[0].communities:
                            print(" Rib.community  : %s" % community)

                print (" Rib.is_withdraw : %s" % path_target.is_withdraw)


def receive_signal(signum, stack):
    print('signal received:%d' % signum)
    print('exit')
    os._exit(0)

if __name__ == '__main__':
    gobgp = sys.argv[1]
    family = sys.argv[2]
    if family == "ipv4":
        routefamily = RF_IPv4_UC 
    else:
        exit(1)

    signal.signal(signal.SIGINT, receive_signal)

    t = Thread(target=run, args=(gobgp, routefamily))
    t.daemon = True
    t.start()

    # sleep 1 sec forever to keep main thread alive
    while True:
        time.sleep(1)

続いて、IDLファイルをコンパイルします。
すると、"gobgp_pb2.py"が自動生成されます。

$ GOBGP_API=$GOPATH/src/github.com/osrg/gobgp/api
$ protoc -I=$GOPATH/src/github.com/osrg/gobgp/api --python_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_python_plugin` $GOPATH/src/github.com/osrg/gobgp/api/gobgp.proto
$ ls -l
total 184
-rw-rw-r-- 1 tsubo tsubo 180906 Apr 30 04:34 gobgp_pb2.py
-rw-rw-r-- 1 tsubo tsubo   2768 Apr 30 04:34 monitor_BestPathChange.py

いよいよ、サンプルアプリを動かしてみます。
(1) 対向BGPルータ側で、Peerを切断しておく。
ここでは、物理インタフェースを停止されることで、Peerを切断しております。

R1(config)#int f1/0
R1(config-if)#shutdown

(2) GoBGPを起動する。
GoBGPを起動します。

$ sudo ./gobgpd -f gobgpd.conf 
{"level":"info","msg":"gobgpd started","time":"2016-04-30T05:17:37+09:00"}
{"level":"info","msg":"finished reading the config file","time":"2016-04-30T05:17:37+09:00"}
{"level":"info","msg":"Peer 192.168.0.1 is added","time":"2016-04-30T05:17:37+09:00"}
{"level":"info","msg":"Add a peer configuration for 192.168.0.1","time":"2016-04-30T05:17:37+09:00"}

(3) サンプルアプリを起動する。
ちなみに、最後の引数"ipv4"は、AddressFamily: ipv4のみを抽出することを意図します。

$ python ./monitor_BestPathChange.py 192.168.0.2 ipv4

(4) 対向BGPルータ側で、Peerを開設する。
ここでは、物理インタフェースを起動されることで、Peerを開設しております。

R1(config-if)#no shutdown

(5) サンプルアプリ起動結果を確認する。
すると、BGP UPDATEメッセージの受信した様子が確認できます。

$ python ./monitor_BestPathChange.py 192.168.0.2 ipv4
----------------------------
 Rib.prefix     : 20.1.0.0/24
 Rib.origin     : 0
 Rib.aspath     : [[65001]]
 Rib.nexthop    : 192.168.0.1
 Rib.med        : 0
 Rib.is_withdraw : False

(6) GoBGPのBGPテーブル確認する。
BGP UPDATEメッセージの受信に伴い、BGPテーブルが更新された様子が確認できます。

$ gobgp global rib
    Network             Next Hop             AS_PATH              Age        Attrs
*>  20.1.0.0/24         192.168.0.1          65001                00:00:27   [{Origin: i} {Med: 0}]

◆ BGPテーブルへの経路追加用サンプルアプリを試してみる

続いて、Pythonスクリプトから、RPCメソッド:”ModPath”を動作させてみます。
先ほどのディレクトリに移動して、次のサンプルアプリを配備してください。

$ cd $HOME
$ cd sample/
$ vi add_prefix.py
-----------------
import gobgp_pb2
import sys
from netaddr.ip import IPNetwork
from ryu.lib.packet.bgp import BGPPathAttributeOrigin
from ryu.lib.packet.bgp import IPAddrPrefix
from ryu.lib.packet.bgp import BGPPathAttributeNextHop
from grpc.beta import implementations


_TIMEOUT_SECONDS = 10
Resource_GLOBAL  = 0

def run(gobgpd_addr, prefix, nexthop):
    channel = implementations.insecure_channel(gobgpd_addr, 50051)
    with gobgp_pb2.beta_create_GobgpApi_stub(channel) as stub:

        subnet = IPNetwork(prefix)
        ipaddr = subnet.ip
        masklen = subnet.prefixlen

        nlri = IPAddrPrefix(addr=ipaddr, length=masklen)
        bin_nlri = nlri.serialize()

        nexthop = BGPPathAttributeNextHop(value=nexthop)
        bin_nexthop = nexthop.serialize()

        origin = BGPPathAttributeOrigin(value=2)
        bin_origin = origin.serialize()

        pattrs = []
        pattrs.append(str(bin_nexthop))
        pattrs.append(str(bin_origin))

        path = {}
        path['nlri'] = str(bin_nlri)
        path['pattrs'] = pattrs

        uuid = stub.ModPath(gobgp_pb2.ModPathArguments(resource=Resource_GLOBAL, path=path)                           ,_TIMEOUT_SECONDS)

        if uuid:
            print "Success!"
        else:
            print "Error!"


if __name__ == '__main__':
    gobgp = sys.argv[1]
    prefix = sys.argv[2]
    nexthop = sys.argv[3]
    run(gobgp, prefix, nexthop)

早速、サンプルアプリを起動してみます。
(1) GoBGPを起動する。

$ sudo ./gobgpd -f gobgpd.conf 
{"level":"info","msg":"gobgpd started","time":"2016-04-30T07:33:50+09:00"}
{"level":"info","msg":"finished reading the config file","time":"2016-04-30T07:33:50+09:00"}
{"level":"info","msg":"Peer 192.168.0.1 is added","time":"2016-04-30T07:33:50+09:00"}
{"level":"info","msg":"Add a peer configuration for 192.168.0.1","time":"2016-04-30T07:33:50+09:00"}
{"Key":"192.168.0.1","State":"BGP_FSM_OPENCONFIRM","Topic":"Peer","level":"info","msg":"Peer Up","time":"2016-04-30T07:33:54+09:00"}

(2) サンプルアプリを起動する。
うまく動作すれば、"Success!"と表示されます。

$ python ./add_prefix.py 192.168.0.2 30.1.0.0/24 0.0.0.0
Success!

(3) GoBGPのBGPテーブルを確認する。
先ほど、追加した経路"30.1.0.0/24"がエントリされているはずです。

$ gobgp global rib
    Network             Next Hop             AS_PATH              Age        Attrs
*>  20.1.0.0/24         192.168.0.1          65001                00:03:05   [{Origin: i} {Med: 0}]
*>  30.1.0.0/24         0.0.0.0                                   00:00:25   [{Origin: ?}]

(4) 対向BGPルータのBGPテーブルを確認する。

R1#show bgp ipv4 unicast
BGP table version is 9, local router ID is 192.168.0.1
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal, 
              r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter, 
              x best-external, a additional-path, c RIB-compressed, 
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

     Network          Next Hop            Metric LocPrf Weight Path
 *>  20.1.0.0/24      0.0.0.0                  0         32768 i
 *>  30.1.0.0/24      192.168.0.2                            0 65002 ?

pythonスクリプトのサンプルアプリから、GoBGPを制御することができました。

◆ その他、留意事項

今回のサンプルアプリは、RyuBGPで保有しているBGPパケットライブラリを活用することが必須になります。
GoBGPでは、"EVPN", "Mrt", "FlowSpec"など最新BGP機能が利用できますが、これらの機能に関わるBGPメッセージ拡張に対応したPython版パケットライブラリは、RyuBGPには存在しておりません。よって、PythonによるGoBGP制御の適用範囲を、事前に吟味することが必要になります。

◆ 終わりに

今後も、大規模ネットワークにおけるBGPの役割は、重要になってまいります。
そこで、スケール性や耐障害性を担保するためには、BGPエンジンでの円滑な挙動が必要になってくるわけですが、従来のPythonベースのRyuBGPだと、Python特有な課題(GILなど)が顕在化してしまいます。その課題解決のアプローチとして、golangベースでBGPエンジンに焼き直したものが"GoBGP"だと理解しております。
GoBGPの特徴を活かしつつ、既存SDN基盤との連携を実現する手段として、PythonによるgRPC経由でのGoBGP制御は、様々な局面で有用だと思います。