何でもかんでもOpenConfigで操作できたらよいじゃないですか。 というわけでFRRoutingがOepnConfigに対応したらよいのにと思ったりもしてみてOpenConfigdとかも少し見てみたのですが、イマイチ使い方がわからない。 なので自分で勉強がてらプログラムを書いてみるかと思いました。というわけでGW中に取り組んだ内容です。

さて、OpenConfigのyangデータモデルをもとにプログラミング言語のひな型を作ろうと思うとOpenConfig純正のygotを使うのが良いかなということでygotを使います。goyangでもいいんですが、より複雑なことをしたければygotって書いてあるのでいきなりygotを使います。

今回はprotocol bufferといいますか、grpcベースでOpenConfigの変数を操作できることを目標に作業始めました。

https://github.com/openconfig/ygot/blob/master/docs/protobuf_getting_started.md を参考にしていきましょう。

ygotのprotogeneraotrを入れる

サンプルが

go run $GOPATH/src/github.com/openconfig/ygot/proto_generator/protogenerator.go 

となっていたので、~/go/src/github.com/openconfig/ygotにgit cloneした後、 ~/go/src/github.com/openconfig/ygot/proto_generator/go installしました。~/go/bin/proto_generatorが作られていればOKです。

protocol bufferを生成する

今回パッケージ名はocdcとしています。OpenConfig Data Converterの意図だったのですが、実際今できていることはOpenConfig Data Storeという感じでしょうか。 元々FRRoutingがOpenConfigで設定できたらよいなと思ったので、OpenConfigのBGPやOSPFのデータモデルを扱いたいと思ったのでですが、それらBGPやOSPFはすべてnetwork-instanceに内包されている(network-instanceからuseで呼び出されている)ので、openconfig-network-instance.yangから生成しようとすると、依存関係が足りないエラーを延々出され、最終的に、以下のyangを呼び出さないといけないことになりました。 なおpublilcは https://github.com/openconfig/public/ をcloneしたディレクトリです。

https://github.com/golang-standards/project-layout に従って、protobufをapiディレクトリ以下に作るようにしました。

proto_generator -generate_fakeroot \
                -base_import_path="go_src_dir/ocdc/api" \
                --go_package_base="go_src_dir/ocdc/api" \
                -path=public/release/models/ -output_dir=api \
                -package_name=ocdc \
                -exclude_modules=ietf-interfaces \
                public/release/models/network-instance/openconfig-network-instance.yang \
                public/release/models/openconfig-extensions.yang \
                public/release/models/interfaces/openconfig-interfaces.yang \
                public/third_party/ietf/ietf-interfaces.yang \
                public/release/models/types/openconfig-yang-types.yang \
                public/release/models/optical-transport/openconfig-transport-types.yang \
                public/release/models/bgp/openconfig-bgp-types.yang \
                public/release/models/platform/openconfig-platform-types.yang \
                public/release/models/policy/openconfig-policy-types.yang \
                public/release/models/local-routing/openconfig-local-routing.yang \
                public/release/models/bfd/openconfig-bfd.yang \
                public/release/models/rib/openconfig-rib-bgp.yang \
                public/release/models/segment-routing/openconfig-segment-routing-types.yang \
                public/release/models/mpls/openconfig-mpls.yang \
                public/release/models/mpls/openconfig-mpls-types.yang \
                public/release/models/pcep/openconfig-pcep.yang \
                public/release/models/vlan/openconfig-vlan.yang \
                public/release/models/keychain/openconfig-keychain.yang \
                public/release/models/aft/openconfig-aft.yang \
                public/release/models/acl/openconfig-packet-match-types.yang \
                public/release/models/ospf/openconfig-ospfv2.yang \
                public/release/models/policy-forwarding/openconfig-policy-forwarding.yang \
                public/release/models/isis/openconfig-isis.yang \
                public/release/models/defined-sets/openconfig-defined-sets.yang \
                public/release/models/multicast/openconfig-pim.yang

生成されたprotobufファイルからgoを生成する

後は、マニュアルの通り、go言語ファイルを生成するだけです。

proto_imports=".:${GOPATH}/src"
find $GOPATH/src/github.com/openconfig/ygot/demo/protobuf_getting_started/ribproto -name "*.proto" | while read l; do
  protoc -I=$proto_imports --go_out=. --go_opt=paths=source_relative $l
done

と思ったらenumのいくつかでエラーになってうまく生成できないので、正しいかわかりませんが一部sedでいじりました。

sed -i "s/enum ocdc.enums.OpenconfigMplsLdpMplsLdpAfi/enum ocdc_enums_OpenconfigMplsLdpMplsLdpAfi/g" api/ocdc/openconfig_network_instance/openconfig_network_instance.proto
sed -i "s/OCDC.ENUMS.OPENCONFIGMPLSLDPMPLSLDPAFI/OCDC_ENUMS_OPENCONFIGMPLSLDPMPLSLDPAFI/g" api/ocdc/openconfig_network_instance/openconfig_network_instance.proto
sed -i "s/enum ocdc.enums.OpenconfigSegmentRoutingTypesSrteProtocolType/enum ocdc_enums_OpenconfigSegmentRoutingTypesSrteProtocolType/g" api/ocdc/openconfig_network_instance/openconfig_network_instance.proto
sed -i "s/OCDC.ENUMS.OPENCONFIGSEGMENTROUTINGTYPESSRTEPROTOCOLTYPE/OCDC_ENUMS_OPENCONFIGSEGMENTROUTINGTYPESSRTEPROTOCOLTYPE/g" api/ocdc/openconfig_network_instance/openconfig_network_instance.proto

RPCがないからgNMI

さて、これをprotobufで外から設定するプログラムが作れる!!!と思いきや、どうやってこの値を外から設定するんだ?で躓きました。

考えてみると、OpenConfigのyangには一切rpc定義はありません。すなわちprotocol bufferに変換してもservice定義があるわけではありません。

protobufのservice/messageを作ればよいわけですが、それは何ベースで作るのって話になってきます。オレオレrpcのためのオレオレserviceを作るか?もうちょっと標準的な方法はないのか?って考えていくとgNMIが登場してます。

gNMIのprotoファイルを見ると、

  • Capabilities
  • Get
  • Set
  • Subsribe

に対して、RequestとResponseを返すrpcが定義されています。

service gNMI {
  rpc Capabilities(CapabilityRequest) returns (CapabilityResponse);
  rpc Get(GetRequest) returns (GetResponse);
  rpc Set(SetRequest) returns (SetResponse);
  rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeResponse);
}

いわゆるTelemetryに使うSubscribeを除くとCpabailitiesとGetとSetってNETCONFでできる事とあまり変わりませんね。2 Phase Commitを除くと。というよりかはHTTPベースだしって思うとRESTCONFの親戚って思ったほうが良いかもしれません。

https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md を引用すると以下文章が書いてあります。

All messages within the gRPC service definition are defined as protocol buffers (specifically proto3). gRPC service definitions are expected to be described using the relevant features of the protobuf IDL. The protobuf definition of gNMI is maintained in the openconfig/gnmi GitHub repository.

Google翻訳すると以下。

gRPC サービス定義内のすべてのメッセージは、プロトコル バッファー (特に proto3) として定義されます。 gRPC サービス定義は、protobuf IDL の関連機能を使用して記述されることが期待されます。 gNMI の protobuf 定義は、openconfig/gnmi GitHub リポジトリで維持されます。

なのでgRPCのプロトコル(HTTP/2のトランスポート)上でgNMI定義のprotobufを用いてOpenConfig Data Modelを操作するプログラムを書けばgNMIに対応したといえそうです。

というわけで、gNMIのprotoファイルも準備されているし、同じディレクトリに変換されたpb.goファイルも準備されているので、後はプログラムを書くだけですね。

gNMIプログラム

以下がひな型です。ChatGPTが作ってくれました。 要はmainでgrpcサーバを立ち上げてRegisterGNMIServerでgrpcServerとGNMIServer用の構造体ポインタを括り付けるだけです。 GNMIServerの構造体は、ygotで生成したdevice構造体を入れているだけです。 だけですとか言っていますが、ChatGPTに聞かないと最初作り方よくわかっていませんでした。 ただChatGPTが途中から平然と嘘を言ってくれるので使うのをやめました。

package main

import (
        "context"
        "fmt"
        "go_src_dir/ocdc/api/ocdc"
        "go_src_dir/ocdc/api/ocdc/enums"
        "github.com/openconfig/gnmi/proto/gnmi"
        "github.com/openconfig/gnmi/value"
        "google.golang.org/grpc"
        "google.golang.org/grpc/reflection"
        "net"
        "ywrapper "github.com/openconfig/ygot/proto/ywrapper"
        "strconv"
        "time"
)

type GNMIServer struct {
    device ocdc.Device
}

func (s *GNMIServer) Capabilities(ctx context.Context, req *gnmi.CapabilityRequest) (*gnmi.CapabilityResponse, error) {
}

func (s *GNMIServer) Get(ctx context.Context, req *gnmi.GetRequest) (*gnmi.GetResponse, error) {
}

func (s *GNMIServer) Set(ctx context.Context, req *gnmi.SetRequest) (*gnmi.SetResponse, error) {
}

func (s *GNMIServer) Subscribe(stream gnmi.GNMI_SubscribeServer) error {
}

func main() {
        port := 50051
        listenPort, err := net.Listen("tcp", ":"+strconv.Itoa(port))
        if err != nil {
                fmt.Errorf("failed to listen: %v", err)
        }
        grpcServer := grpc.NewServer()
        reflection.Register(grpcServer)
        gnmi.RegisterGNMIServer(grpcServer, &GNMIServer{})
        grpcServer.Serve(listenPort)
}

で、このGet関数やSet関数が重要なわけですが、長くなるので今回は割愛。