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

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

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


### 2017/8/2追記
久々に諸々実行環境(GoBGP, gRPC, protobuf)をカレントバージョンにアップデートしてサンプルコードを実行したところ、案の定[こちらの更新](https://github.com/tamihiro/gobgp_grpc_demo/commit/cfaa4ad8cdd89ffa234ff6700925a6074f0347c3)が必要でした。

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

### 2016/5/26追記
この記事を公開した後、GoBGPで[gRPCのAPIが変更](https://github.com/osrg/gobgp/commit/f9a9c410c3b221b515bad9fcf154990be7bb70fa)されたため一部のコードは記載通りに動作しなくなりました。
GitHubに公開しているサンプルコードは最新のGoBGPで動作するようAPI変更に対応していますので、記事中のコードに必要な修正は[こちらのコミット](https://github.com/tamihiro/gobgp_grpc_demo/commit/57edbb806776098e01f909e67f7360c0c3727827)をご確認ください。

# きっかけ

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

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

動作確認したホストのOSはUbuntu-14.04 LTS amd64、Pythonは2.7.6です。
[バイナリのgo1.6](https://golang.org/dl/)をダウンロードして[手順](https://golang.org/doc/install)通り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からの取得を実行するクライアントプログラムのテストですので、ピアセッションを開始する必要はありません。
[こちらの手順](https://github.com/osrg/gobgp/blob/master/docs/sources/getting-started.md)を参考に、インストールしたら適当なコンフィグで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の仕組みをちょっと読んでからやりはじめてみると、経路を操作するためのリクエストは以下のかんじで作成することがわかります。
<a name="modpathargs"></a>

```py
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メッセージ](https://github.com/osrg/gobgp/blob/master/api/gobgp.proto#L78)、そのpathフィールドのタイプである[Pathメッセージ](https://github.com/osrg/gobgp/blob/master/api/gobgp.proto#L191)の各フィールドの値、**`nlri`** と **`pattrs`** をシリアライズする処理が必要になります。

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

使用するパス属性とかけっこう決まってるのでチマチマやってもいいか、、ということでやってみました。
たとえばコロン区切り表記コミュニティのリストは、[GoBGPのサーバ側でやってる処理](https://github.com/osrg/gobgp/blob/master/packet/bgp/bgp.go#L4305)とかを参考にして以下のようにしてました。

```py
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](https://github.com/osrg/gobgp/blob/master/api/gobgp.proto#L214)->[Destination](https://github.com/osrg/gobgp/blob/master/api/gobgp.proto#L208)->Pathにデコードする処理は、適当にやるとかなり荒れそうだし、がんばってちゃんとやるのもナニかなぁ、と思いました。

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

1. 小島さんのgobgp-nodeのようにGoBGPの共有ライブラリを使うか、
2. [Ryu](https://github.com/osrg/ryu/blob/master/ryu/lib/packet/bgp.py)や[ExaBGP](https://github.com/Exa-Networks/exabgp/blob/master/lib/exabgp/bgp/message/update/__init__.py)とかのモジュールをインポートして使うか、

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

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

まずは$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からの利用方法

<a name="node-addon"></a>gobgp-nodeでは、C++で書いた[Nodeアドオンからリンク](https://github.com/codeout/gobgp-node/blob/master/src/gobgp.cc)してGoBGPのシリアライズ処理を利用していて、[本体](https://github.com/codeout/gobgp-node/blob/master/index.js)からはアドオンのファンクションを実行するような作りになっています。（詳しくはJANOG発表資料をご参照）

Pythonの場合は、標準ライブラリの[ctypes](http://docs.python.jp/2/library/ctypes.html#ctypes-loading-dynamic-link-libraries)を使って直接ロードして、GoBGPの関数をメソッドとして利用できます。

```py
>>> 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`](https://github.com/osrg/gobgp/blob/master/gobgp/lib/path.go) をみて確認しますが、まず共有ライブラリ関数とPython側のデータの受け渡し用に、Pythonでも[対応する構造体](https://github.com/osrg/gobgp/blob/master/gobgp/lib/path.go#L18)をctypes.Structureのサブクラスとして定義しておきます。

```py
>>> 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),
...              ]
...
>>>
```

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

```py
>>> 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と[全く同じシンタックス](https://github.com/osrg/gobgp/blob/master/docs/sources/cli-command-syntax.md#more-examples)でいいのです。たとえば、

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

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

```py
>>> 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` の中身を確認：

```py
>>> 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メッセージの内容（[上記の`nlri`と`pattrs`](#modpathargs)）は以下の４つの値を使って渡すことができます。

```py
>>> 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](https://atnd.org/events/74772)で石田さんから紹介された「gRPCによるAPI firstな設計」という謳い文句通りであることを実感しました。その他の機能や性能においても進化をつづけているので今後ますます楽しみです。