GoでgRPCを使ってJunosからTelemetryを収集する
今回はJuniperのJunosにgRPCで接続し、Telemetryを収集する方法を紹介する。タイプとしては、Dial inとなる。まず、protoファイルはtelegrafのリポジトリから取得する。また、protoc
を用いてprotoファイルからGoのpbファイルとgrpcファイルを生成しておく。
protoc --go_out=. --go-grpc_out=. oc.proto
準備として、Junos側に以下の設定を入れておく(clear-textは補完できないので注意)。
set system services extension-service request-response grpc clear-text port 50051
set system services extension-service notification allow-clients address 0.0.0.0/0
元のoc.proto
ファイルをみると、次のようにservice
が定義されていることがわかる(コメントや空行は適宜省略する)。
service OpenConfigTelemetry {
rpc telemetrySubscribe(SubscriptionRequest) returns (stream OpenConfigData) {}
rpc cancelTelemetrySubscription(CancelSubscriptionRequest) returns (CancelSubscriptionReply) {}
rpc getTelemetrySubscriptions(GetSubscriptionsRequest) returns (GetSubscriptionsReply) {}
rpc getTelemetryOperationalState(GetOperationalStateRequest) returns (GetOperationalStateReply) {}
rpc getDataEncodings(DataEncodingRequest) returns (DataEncodingReply) {}
}
というわけで、rpc telemetrySubscribe(SubscriptionRequest)
を実行してやればOpenConfigData
が取得できるということが理解できると思う。
Dial inなので、Cisco IOS XRのshowコマンド実行やconfig投入の例と同じように、認証情報はmetadataとしてcontextに埋め込んでおく。
md := metadata.Pairs(
"username", "junos",
"password", "Passw0rd",
)
ctx := metadata.NewOutgoingContext(context.Background(), md)
gRPCコネクションの作り方もこれまで通り。
conn, _ := grpc.Dial("192.168.1.20:50051", grpc.WithInsecure())
client := oc.NewOpenConfigTelemetryClient(conn)
あとはrpc telemetrySubscribe()
の引数SubscriptionRequest
を組み上げて、
subscribe_path := &oc.Path{Path: "/interfaces", SampleFrequency: 5000}
paths := []*oc.Path{subscribe_path}
collector := &oc.Collector{Address: "192.168.1.100", Port: 2104}
collectors := []*oc.Collector{collector}
subscription_input := &oc.SubscriptionInput{CollectorList: collectors}
subscription_request := &oc.SubscriptionRequest{
Input: subscription_input,
PathList: paths,
}
メソッドを実行してやれば良い。
responses, err := client.TelemetrySubscribe(ctx, subscription_request)
問題はこのresponses
の扱い方で、この中に含まれるKeyValueのValueが特殊な形式をしている。まずは最低限のエラーハンドリングをしつつ、Keyの表示のみを書くと以下のようになる。
if err != nil {
fmt.Println(err)
} else {
for {
response, res_err := responses.Recv()
if response != nil {
for _, kv := range response.Kv {
key := kv.Key
fmt.Println(key)
}
} else {
fmt.Println(res_err)
}
}
}
問題のValueについて、oc.proto
では以下のように定義されている。
message KeyValue {
string key = 1;
oneof value {
double double_value = 5;
int64 int_value = 6;
uint64 uint_value = 7;
sint64 sint_value = 8;
bool bool_value = 9;
string str_value = 10;
bytes bytes_value = 11;
}
}
どいうことかというと、valueについては、値の型によって変数名が変わる。そこで、やり方の1つにswitch文で場合分けして、型に応じて処理を変えてやるという方法がある。
switch x := kv.Value.(type) {
case *oc.KeyValue_DoubleValue:
value := x.DoubleValue
fmt.Println(value)
case *oc.KeyValue_IntValue:
value := x.IntValue
fmt.Println(value)
case *oc.KeyValue_UintValue:
value := x.UintValue
fmt.Println(value)
case *oc.KeyValue_SintValue:
value := x.SintValue
fmt.Println(value)
case *oc.KeyValue_BoolValue:
value := x.BoolValue
fmt.Println(value)
case *oc.KeyValue_StrValue:
value := x.StrValue
fmt.Println(value)
case *oc.KeyValue_BytesValue:
value := x.BytesValue
fmt.Println(value)
case nil:
value := "nil"
fmt.Println(value)
default:
value := "unknown"
fmt.Println(value)
}
もちろん、この方法でうまくハンドリングすることができる。しかし、値によって同じ処理をするのであれば、pythonでは以下のように書けるので、もう少し簡潔にしたい。
key = kv.key
value = getattr(kv, kv.WhichOneof("value"))
print(key, value)
Goではreflect
とstrings
、reflectionsを用いて同様のことが実現できた(他に簡単な方法があればコメントで教えて頂けると助かります)。
value_type := reflect.TypeOf(kv.Value).String()
type_split := strings.Split(value_type, "_")
value_name := type_split[len(type_split)-1]
value, _ := reflections.GetField(kv.Value, value_name)
fmt.Println(value)
全体像は以下のようになる。今回はこのプログラムでinterfaceのカウンタ等を取得できる。
package main
import (
"context"
"fmt"
"reflect"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"gopkg.in/oleiade/reflections.v1"
oc "./oc"
)
func main() {
md := metadata.Pairs(
"username", "junos",
"password", "Passw0rd",
)
ctx := metadata.NewOutgoingContext(context.Background(), md)
conn, _ := grpc.Dial("192.168.1.20:50051", grpc.WithInsecure())
client := oc.NewOpenConfigTelemetryClient(conn)
subscribe_path := &oc.Path{Path: "/interfaces", SampleFrequency: 5000}
paths := []*oc.Path{subscribe_path}
collector := &oc.Collector{Address: "192.168.1.100", Port: 2104}
collectors := []*oc.Collector{collector}
subscription_input := &oc.SubscriptionInput{CollectorList: collectors}
subscription_request := &oc.SubscriptionRequest{
Input: subscription_input,
PathList: paths,
}
responses, err := client.TelemetrySubscribe(ctx, subscription_request)
if err != nil {
fmt.Println(err)
} else {
for {
response, res_err := responses.Recv()
if response != nil {
for _, kv := range response.Kv {
key := kv.Key
fmt.Println(key)
value_type := reflect.TypeOf(kv.Value).String()
type_split := strings.Split(value_type, "_")
value_name := type_split[len(type_split)-1]
value, _ := reflections.GetField(kv.Value, value_name)
fmt.Println(value)
}
} else {
fmt.Println(res_err)
}
}
}
}
今回の方法を押さえておくと、gNMIの実装方法も理解しやすくなる。というわけで、次回はgNMIについて書きます。
Discussion