🧭

GoでgRPCを使ってJunosからTelemetryを収集する

2020/09/26に公開

今回は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ではreflectstringsreflectionsを用いて同様のことが実現できた(他に簡単な方法があればコメントで教えて頂けると助かります)。

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