今回やること
ここまで、YANG データモデルをつかって操作対象に与えたいデータを作るとか、操作対象からデータをひっぱり出すといった話をしてきました。これは、操作対象をサーバ、自分をクライアントとする RPC (Remote Procedure Call) の話でもあります。もともと YANG の話は Netconf といっしょに XML で RPC をやろうという話で進んできたものですしね。いまはここに Web からの技術が入ってきて、RFC 8040 - RESTCONF Protocol といったプロトコルも標準化されています。また、最近はもうひとつの方向性として、gRPC を使うというのがあります。
- ONF (元は google か) → ネットワークスイッチをプログラマブルでgRPC対応にする「Stratum」をONFが発表。ホワイトボックススイッチのリファレンス実装に、Googleがオープンソース提供 - Publickey
- ルーティングソフトウェアが提供する API として → Zebra開発者の石黒氏、Go言語で全面的に書き換えた「Zebra 2.0」を開発中 | 日経 xTECH(クロステック)
いろいろなものが登場してきて何がどうなってるのかわかりにくくなるんですが、IETF95 の資料にわかりやすい図があったのでちょっと持ってきてみます。
図: https://www.ietf.org/proceedings/95/slides/slides-95-hackathon-8.pdf
goyang というツールを使うと、YANG データモデル定義から protocol buffer (-like) のインタフェース定義が出力できるので、これを使って実際にアプリ (これまで YANG チュートリアルをやるために使っていた Turing Machine サーバ/クライアント) を作ってみました。上の図で言うと
- Apps : Turing Machine Server/Client
- APIs : goyang generated protocol buffer
- Protocol : gRPC
- ...
というカタチですね。今回はどうやって YANG から protobuf 出すのか、それを作ってどうアプリを作ったのか、という話をします。
注意事項
言い訳っぽく聞こえますが事実として……筆者は Golang 初心者です1。今回 YANG 周りの話を勉強するついでに Golang/gRPC の話も覚えようと思っていろいろやってみました、という程度。そのため、Golang 詳しい人から見ると、変なことや間違えていることがあるかもしれません。その場合は教えてください。(特に、もっと良いやり方がある、というのは十分あり得る気がしています。)
YANG から gRPC API をつくる
YANG からベースになる protobuf をつくる
まずは goyang のインストール
corestate55@ytut:~$ go get github.com/openconfig/goyang
YANG から protocol buffer をつくります。
corestate55@ytut:~/yang-tutorial$ make goyang
中の処理については Makefile を参照……実際には goyang --format=proto
で data/turing-machine.yang
から proto/turing-machine.orig.proto
を作ります。これは直接には使用していません。というのも、用途に合わせてちょっと修正が必要だったからです。Goyang のヘルプでも書いてあるとおり、あくまでも protocol buffer "-like" であって、それ単品で完全に動くコードではありません。2
Protobuf から Golang API code をつくる
ベースになる protocol buffer file に、以降で説明する変更を加えて、実際に使う protocol buffer file を書きます。ここから Golang のコード (API) を生成します。 コード生成のためには protoc (proto ファイルを golang code に変換するコンパイラ) が必要になります。そこのインストール等については割愛。下記参照。
後述しますが protoc-go-inject-tag というツールを使っているのでそれも入れておきます。
corestate55@ytut:~$ go get https://github.com/favadi/protoc-go-inject-tag
ビルドは make
または make proto
で実行します。
つまったところ
Protocol Buffer の一般的な話を最初からしてしまうときりがないので、YANG から作る時に引っかかった点をあげておきます。
名前がかぶる
早速ですが、サービス名と型名が被ってエラーになるので名前を変えます。
Type の名前が長い…
Protobuf message が入れ子になっているせいで自動生成される型名が非常に長い……。TuringMachine_TransitionFunction_Delta_Input
とかね。エディタで補完が効かないと死にます。YANG → protobuf → pb.go で何がどう変換されて、どんなメソッドが使えるようになっているのか、パターンがわかればなんとかはなるんですが。ただ、typo がね……3。
入れ子にならないようにメッセージ定義をバラせばいいんでしょうけど。今回は「なるべく goyang が作る protobuf に手を入れない」方針だったので。
XML Marshal/Unmarshal のためのタグをつける
goyang で YANG から出力した proto ファイルは、YANG で定義されているデータ構造が message に変換されてるんですよね。データ構造がもう定義されているので、これを使ってファイルからデータを読み込むとか終わらせてしまいたいわけです。
XML ファイルの parse には encoding/xml を使います。Parse するためには、構造体の各フィールドに、どの XML 要素に対応するものなのかを xml:"hoge"
形式でタグ付けする必要があります。Proto ファイルを生成する際、protoc はデータ(message)定義に対して、XML タグは付けてくれないのでどうにかする必要がありました。(JSON タグは付けてくれるものの、これはこれでちょっと…というのは後述。)
今回これは favadi/protoc-go-inject-tag: Inject custom tags to protobuf golang struct を使うことで後から足してあります。Proto ファイル中、messsage の各要素に対してコメントで // @inject_tag: xml:"hoge"
しておいて、protoc
した後でさらに protoc-go-inject-tag
するとタグを追加してくれます。(くわしくは Makefile の proto target 参照)
見た目のデータ構造を合わせる
gRPC では、Netconf 想定時に使っていた <config>
とか <rpc>
といった operation を示すようなタグは本来なら不要なんですよね。しかし、今回はチュートリアルをなるべく変えずに使いたいので、クライアント側は既存の XML をそのまま使えることにしました。となると、protobuf でこれらを含めたデータ構造を定義して XML 読めるようにしてしまった方が楽なので、冗長な message を (これまで使っていた XML parse をするためだけに) 定義してあります。
たとえばこれとか。
// Config data
message Config {
// @inject_tag: xml:"xmlns,attr"
string xmlns = 1;
// @inject_tag: xml:"turing-machine"
TuringMachine turing_machine = 2;
}
<config>
で受け取ったデータを送るのは
service TuringMachineRpc {
rpc Configure (TuringMachine) returns (Empty);
}
ですが、サーバに渡すのは <config>
ではなく、その中のデータ (<turing-machine>
) だけ。
Enum の定義が YANG と Protobuf でずれる
上にも書いたように、goyang でつくった proto ファイルをなるべくそのまま使ってなんとかしたい。でもそのままだと YANG チュートリアルで使っているサンプル XML が読めない (parse できない) という問題がありました。何が読めないかというと、enum のところで parse に失敗する。
Turing Machine の head-dir
は YANG では "left"/"right" のいずれかを取る enum になっています。
typedef head-dir {
type enumeration {
enum left;
enum right;
}
default "right";
description
"Possible directions for moving the read/write head, one cell
to the left or right (default).";
}
ところが出力した proto ファイルの定義だと値は string ではなく int32 になる。
// Move the head one cell to the left or right
type HeadMove int32
const (
HeadMove_LEFT HeadMove = 0
HeadMove_RIGHT HeadMove = 1
)
したがって、読みたい XML ファイルも
<output>
<head-move>left</head-move>
</output>
だと parse できなくて、下記のように数字に変えてやらないといけない。
<output>
<head-move>0</head-move>
</output>
ただ、これを変えてしまうと、そもそもの XML validation に通らなくなってしまう……となると本末転倒なので、今回は enum ではなく単なる string に変更しています。
message Output {
// HeadMove head_move = 1;
// @inject_tag: xml:"head-move"
string head_move = 1;
XML 側で validate しておくか、値を読んだ後プログラムの内部処理でチェックを入れるかで、protobuf の機能ではチェックしていません。(もうちょっと上手いことやれる方法がないのか……と思いつつ、いまの golang 力だとどうにもならず。)
nil value の取り扱い
結構困ったのがこれ。Turing Machine の状態遷移表から次ステップの「状態・テープに書き込むシンボル・ヘッドどう動かすか」を出します。これはまとめて Output
という struct で定義しています。状態 (state) を取得する際には GetState()
メソッドを呼ぶのですが、実際の golang code はこんな感じ。
func (m *TuringMachine_TransitionFunction_Delta_Output) GetState() uint32 {
if m != nil {
return m.State
}
return 0
}
YANG 定義上は .../delta/output/state
は省略可能 (省略した場合は状態を変更しない) なんですよね。で、問題は、Turing Machine State が 0 から始まるってことなんですよ。GetState()
したときに 0
が返ってきたとして、省略されて存在していなかったから 0 なのか、明示的に状態 0 にするように書かれていたのかというのがわからなくなってしまう。
ということで、今回 Turing Machine 実装するにあたってちょっと仮定を入れています。いまはアプリ(サーバ)側動作をいじって問題回避していますが、こういった認識のずれや見落としについては要注意かと。
- Turing Machine の状態は 0 (S0) から始まる
- 状態遷移にともなって、状態値は増加し、初期状態 0 (S0) には戻らない
- 終了状態は状態遷移表にある状態値の最大値とする
Pyang のつける tag と Goyang のつける tag の名前がずれる
proto ファイルを golang コードに変換する際、protoc はデフォルトで JSON tag 付けてくれるんだから tag を後付けしてまで XML 使わないでも、JSON 使えば楽なんじゃないのという話なんですが……。(4) JSONも使いたい でも説明したように pyang/goyang が付けてくる名前がずれていてそのままだと NG でした。どうずれるかというと例えばこんな感じ。
yang →(goyang)→ proto →(protoc)→ pb.go
type InitializeRequest struct {
TapeContent string `protobuf:"bytes,1,opt,name=tape_content,json=tapeContent" json:"tape_content,omitempty"
}
yang →(pyang)→xslt + xml →(xsltproc)→json
{
"turing-machine:input": {
"tape-content": "111011"
}
}
ここが同じルールで名前付けてくれてたら、そのまま使えたのに…! (4) JSONも使いたい でも書きましたが、いまのところ pyang が出力する XSLをいじる感じですかね……。
「gRPCアプリを作る」では何をやったのか
YANG データモデルを元に API を ((半)自動で) 作る、という話を試してみました。今回は Golang/gRPC ですが、探すと他のツールもいろいろあります。ぱっと試せそうなのは python 関連かな。
- robshakir/pyangbind: A plugin for pyang that creates Python bindings for a YANG model.
- YANG Development Kit (YDK) - DevNet
ほかにも OpenDaylight/ONOS でも java ベースですが YANG モデルを元に API やモジュールコードを作るような話があります。(全然フォローしてないので、そういうのがあるらしい…というだけですが)
こんな形で、モデルを定義して API etc を自動生成してプログラムを書いていく、それを元にネットワークを操作・制御していく、という話 ("Model-Driven" Network Operation/Automation/Development etc) が今後は広がっていくんじゃないかなと。でも、こういう話最初の一歩をやるのが割とハードルが高いんですよね。ODL や ONOS はそもそも「それが動かせる環境」を調達・セットアップすること自体のハードルが高いデスヨ。じゃあ Cisco や Juniper の Router VM 立てるところからスタートします? まあ、相対的にはまだ VM 立てるほうが楽ですけど…、YANG の使い方とか概念をつかむところからと思うとそれもやっぱり Fat じゃないかなと。今回作ってみた Turing Machine 実装を通して、もうちょっと手軽に YANG ってなんぞやという話に手を付けられるといいなあ、と思っております。