Help us understand the problem. What is going on with this article?

GoBGPの共有ライブラリ関数を利用するPython版gobgpdクライアント

More than 1 year has passed since last update.

2019/2/15追記

昨年前半ころからAPI周りも含めてGoBGPのコード構成が大幅に変更されて、昨年末に2.0へとメジャーバージョンアップされました。APIに関しては、こちらで議論されてますが、そもそもメッセージがバイナリ形式ではなくprotobufで定義されるようになり、Cの関数も廃止されました。:grimacing:
というわけで、当初こちらの記事で紹介したメッセージの処理はもう使えなくなってます。

こちらのリポジトリに、PythonのgRPCでGoBGP2.0のAPIを利用する場合の例をあげておきました。
https://github.com/tamihiro/gobgp2_grpc_demo

2017/8/2追記

久々に諸々実行環境(GoBGP, gRPC, protobuf)をカレントバージョンにアップデートしてサンプルコードを実行したところ、案の定こちらの更新が必要でした。

2017/1/18追記

gRPCのPythonクライアント実行環境を用意するまでの手順は、もうこの記事の記載通りではなくなってますよー
という指摘をしてくれた方(隣席の同僚)が、諸々アップデートしてまとめてくれました。これから試される方は、こちらのブログを参考にして実行環境を準備してください。

2016/5/26追記

この記事を公開した後、GoBGPでgRPCのAPIが変更されたため一部のコードは記載通りに動作しなくなりました。
GitHubに公開しているサンプルコードは最新のGoBGPで動作するようAPI変更に対応していますので、記事中のコードに必要な修正はこちらのコミットをご確認ください。

きっかけ

ソフトウェアBGPデーモンのニューブリードとして巷で話題のGoBGP、最近は利用事例を公開してくれるユーザも増えているようです。
自分もコーダンス小島さんの記事を拝見して俄かに試したくなりました。さらに本気で使いたくなった動機のひとつとして、特殊経路を生成するのにずっと使っているquaggaのbgpdをexpect的な処理でアプリから操作しているわけですが、管理する経路が多くなってくると処理が滞るケースが発生してきている、という事情があります。
小島さんがJANOG37のLTで発表されたgobgp-nodeのように、gRPCでクライアントから制御する手法をテストしてみることにしました。gobgp-nodeはいうまでもなくjsですが(後述)、こちらの方はPython Tornadoのアプリから制御することになるので、Python版のクライアント処理を書いてみました。
この記事を読んだひとがすぐ試せるように、grpcの実行環境を用意するところから手順を紹介します。

gRPCクライアントを実行できるようにするまで

動作確認したホストのOSはUbuntu-14.04 LTS amd64、Pythonは2.7.6です。
バイナリのgo1.6をダウンロードして手順通りGOROOTとGOPATHの環境変数を設定しておきます。

  • まだの場合はコンパイルに必要な諸々をインストールしておきます
sudo apt-get install build-essential autoconf libtool python-dev
  • Protocol Buffer

テスト時点で最新のv3.0.0-beta-2をインストールします

curl -L -O https://github.com/google/protobuf/archive/v3.0.0-beta-2.tar.gz
tar zxf v3.0.0-beta-2.tar.gz
cd protobuf-3.0.0-beta-2/
./autogen.sh
./configure && make
sudo make install
sudo ldconfig

// Pythonライブラリをインストール
cd python/
sudo python setup.py install

// サンプルファイルからPythonクラスファイル(addressbook_pb2.py)が生成できることを確認
protoc --proto_path=$HOME/protobuf-3.0.0-beta-2/examples --python_out=. $HOME/protobuf-3.0.0-beta-2/examples/addressbook.proto
  • gRPC
git clone https://github.com/grpc/grpc.git
cd grpc/
git submodule update --init
make
sudo make install

// gRPC-Pythonライブラリのコンパイル
sudo pip install -rrequirements.txt
sudo GRPC_PYTHON_BUILD_WITH_CYTHON=1 python setup.py install
  • サンプルファイルで動作テスト
cd examples/python/helloworld/

// hellowworld_pb2.pyが作成されることを確認
./run_codegen.sh

// grpcサーバを起動
python greeter_server.py &

// grpcクライアントを起動、メッセージの受信を確認
python greeter_client.py
Greeter client received: Hello, you!
  • GoBGP インストールは超簡単です。経路生成、削除とローカルRIBからの取得を実行するクライアントプログラムのテストですので、ピアセッションを開始する必要はありません。 こちらの手順を参考に、インストールしたら適当なコンフィグでgobgpdを実行しておきます。 そして、GoBGPのgRPCサーバと通信するPythonクライアント用のスタブを作成しておきます
cd $GOPATH/src/github.com/osrg/gobgp/tools/grpc/python
GOBGP_API=$GOPATH/src/github.com/osrg/gobgp/api 
protoc  -I $GOBGP_API --python_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_python_plugin` $GOBGP_API/gobgp.proto

gobgp_pb2.pyが生成されていれば大丈夫で、これで準備完了です。

gRPCリクエスト送信データのシリアライズとレスポンスのデコードをどうするか

protobufやgRPCの仕組みをちょっと読んでからやりはじめてみると、経路を操作するためのリクエストは以下のかんじで作成することがわかります。

import gobgp_pb2
from grpc.beta import implementations

channel = implementations.insecure_channel('localhost', 50051)
stub = gobgp_pb2.beta_create_GobgpApi_stub(channel)
res = stub.ModPath( gobgp_pb2.ModPathArguments(path=dict([('nlri', nlri)), ('pattrs', pattr), ]),), _TIMEOUT_SECONDS )

これを実行するために、ModPath()メソッドの引数となるModPathArgumentsメッセージ、そのpathフィールドのタイプであるPathメッセージの各フィールドの値、nlripattrs をシリアライズする処理が必要になります。

まずは自前でせっせとやる

使用するパス属性とかけっこう決まってるのでチマチマやってもいいか、、ということでやってみました。
たとえばコロン区切り表記コミュニティのリストは、GoBGPのサーバ側でやってる処理とかを参考にして以下のようにしてました。

def pattr_comms_atob(comms):
  buf = bytearray()
  for c in comms:
    for v in c.split(':'):
      buf += struct.pack('!H', int(v))
  return buf

def serialize_pattr_comms(comms):
  buf = bytearray()
  comms_b = pattr_comms_atob(comms)
  buf.append(192) # attr. optional, transitive
  buf.append(8)   # type communities
  buf.append(len(comms_b))
  buf += comms_b
  return str(buf)

こっちはまぁこれでもよかったのですが、、

逆にRIBのエントリを取得するGetRib()メソッドの戻り値のバイト文字列をTable->Destination->Pathにデコードする処理は、適当にやるとかなり荒れそうだし、がんばってちゃんとやるのもナニかなぁ、と思いました。

では自前でやらずに何を利用するか、ということになりますが、

  1. 小島さんのgobgp-nodeのようにGoBGPの共有ライブラリを使うか、
  2. RyuExaBGPとかのモジュールをインポートして使うか、

の選択になります。
後者の方は、シリアライズとデコードのためだけにわざわざ追加でインストールするのもナニかなぁという気がしたのと、共有ライブラリの関数の戻り値の扱いがけっこう簡単なことを教えていただいたので、前者の方法でいくことにしました。

共有ライブラリを利用するためにコンパイル

まずは$GOPATH配下に設置されているgogbpのソースからビルドしておきます。

cd $GOPATH/src/github.com/osrg/gobgp/gobgp/lib
go build --buildmode=c-shared -o libgobgp.so *go

libgobgp.soとlibgobgp.hが生成されます。

Pythonからの利用方法

gobgp-nodeでは、C++で書いたNodeアドオンからリンクしてGoBGPのシリアライズ処理を利用していて、本体からはアドオンのファンクションを実行するような作りになっています。(詳しくはJANOG発表資料をご参照)

Pythonの場合は、標準ライブラリのctypesを使って直接ロードして、GoBGPの関数をメソッドとして利用できます。

>>> import os, ctypes
>>> libgobgp = ctypes.cdll.LoadLibrary(os.environ["GOPATH"] +"/src/github.com/osrg/gobgp/gobgp/lib/libgobgp.so")
>>> libgobgp
<CDLL '/home/tamihiro/work/src/github.com/osrg/gobgp/gobgp/lib/libgobgp.so', handle 2093bb0 at 7f43d7b994d0>
>>> libgobgp.serialize_path
<_FuncPtr object at 0x7f43d7bbd390>
>>> libgobgp.decode_path
<_FuncPtr object at 0x7f43d7bbd460>

利用する関数の実体は $GOPATH/src/github.com/osrg/gobgp/gobgp/lib/path.go をみて確認しますが、まず共有ライブラリ関数とPython側のデータの受け渡し用に、Pythonでも対応する構造体をctypes.Structureのサブクラスとして定義しておきます。

>>> class Buf(ctypes.Structure):
...   _fields_ = [("value", ctypes.POINTER(ctypes.c_char)),
...               ("len", ctypes.c_int),
...              ]
...
>>> class Path(ctypes.Structure):
...   _fields_ = [("nlri", Buf),
...               ("path_attributes", ctypes.POINTER(ctypes.POINTER(Buf) * 32)),
...               ("path_attributes_len", ctypes.c_int),
...               ("path_attributes_cap", ctypes.c_int),
...              ]
...
>>>

そして利用する関数の引数と戻り値のタイプを定義しておきます。

>>> libgobgp.serialize_path.restype = ctypes.POINTER(Path)
>>> libgobgp.serialize_path.argtypes = (ctypes.c_int, ctypes.c_char_p)
>>> libgobgp.decode_path.restype = ctypes.c_char_p
>>> libgobgp.decode_path.argtypes = (ctypes.POINTER(Path), )

ここまでやっておくと共有ライブラリの関数を使えるようになります。
便利なところのひとつは serialized_path() の引数を渡し方で、じつはgobgpのCLIと全く同じシンタックスでいいのです。たとえば、

gobgp global rib add -a ipv4 10.10.0.0/16 origin igp nexthop 192.168.1.1 local-pref 300 community no-export

と同等のことをしたければ、以下のようにしてシリアライズ処理ができちゃいます。

>>> serialized_path = libgobgp.serialize_path(
...     libgobgp.get_route_family("ipv4-unicast"),
...     "10.10.0.0/16 origin igp nexthop 192.168.1.1 local-pref 300 community no-export",
...     )

serialized_path の中身を確認:

>>> serialized_path
<__main__.LP_Path object at 0x7f43d7c06440>
>>> dir(serialized_path)
['__class__', '__ctypes_from_outparam__', '__delattr__', '__delitem__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getitem__', '__getslice__', '__hash__', '__init__', '__module__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_b_base_', '_b_needsfree_', '_objects', '_type_', 'contents']
>>> serialized_path.contents
<__main__.Path object at 0x7f43d7c064d0>
>>> dir(serialized_path.contents)
['__class__', '__ctypes_from_outparam__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_b_base_', '_b_needsfree_', '_fields_', '_objects', 'nlri', 'path_attributes', 'path_attributes_cap', 'path_attributes_len']

となってます。そして、Pathメッセージの内容(上記のnlripattrs)は以下の4つの値を使って渡すことができます。

>>> for a in [ a for a in dir(serialized_path.contents) if not a.startswith('_') ]:
...   print "{} : {}".format(a, getattr(serialized_path.contents, a) )
...
nlri : <__main__.Buf object at 0x7f43d7c064d0>
path_attributes : <__main__.LP_LP_Buf_Array_32 object at 0x7f43d7c06560>
path_attributes_cap : 32
path_attributes_len : 4

デモ用スクリプト

というかんじで経路生成、取得、削除の処理を把握できたので、各処理を実行するスクリプトにして公開してみました。

https://github.com/tamihiro/gobgp_grpc_demo

この記事で紹介したようにgRPCが動作する環境を用意すれば、すぐに試せるはずです。

python modpath.py --help
usage: modpath.py [-h] [-4 | -6] [-d] [-o ORIGIN] [-n NEXTHOP] [-m MED]
                  [-p LOCAL-PREF] [-c [COMMS [COMMS ...]]]
                  prefix

positional arguments:
  prefix

optional arguments:
  -h, --help            show this help message and exit
  -4                    Address-family ipv4-unicast
  -6                    Address-family ipv6-unicast
  -d                    Withdraw route
  -o ORIGIN             Origin
  -n NEXTHOP            Next-hop、
  -m MED                MED
  -p LOCAL-PREF         Local-preference
  -c [COMMS [COMMS ...]]
                        Community

試したりソースをチェックして気づいたことなどご指摘いただけると助かります。

まとめ

共有ライブラリを利用することによって、Pythonのクライアントでもこれほど簡潔に処理を記述することができました。
やはり盛り上がってるだけにこのGoBGP、先日のNetOpsCoding#2で石田さんから紹介された「gRPCによるAPI firstな設計」という謳い文句通りであることを実感しました。その他の機能や性能においても進化をつづけているので今後ますます楽しみです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした