1229 lines
34 KiB
Go
1229 lines
34 KiB
Go
package gnmi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/openconfig/gnmi/proto/gnmi"
|
|
"github.com/openconfig/gnmi/proto/gnmi_ext"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/config"
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
|
"github.com/influxdata/telegraf/plugins/inputs/gnmi/extensions/jnpr_gnmi_extention"
|
|
"github.com/influxdata/telegraf/plugins/parsers/influx"
|
|
"github.com/influxdata/telegraf/testutil"
|
|
)
|
|
|
|
func TestParsePath(t *testing.T) {
|
|
path := "/foo/bar/bla[shoo=woo][shoop=/woop/]/z"
|
|
parsed, err := parsePath("theorigin", path, "thetarget")
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, "theorigin", parsed.Origin)
|
|
require.Equal(t, "thetarget", parsed.Target)
|
|
require.Equal(t, []*gnmi.PathElem{{Name: "foo"}, {Name: "bar"},
|
|
{Name: "bla", Key: map[string]string{"shoo": "woo", "shoop": "/woop/"}}, {Name: "z"}}, parsed.Elem)
|
|
|
|
parsed, err = parsePath("", "", "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, &gnmi.Path{}, parsed)
|
|
|
|
parsed, err = parsePath("", "/foo[[", "")
|
|
require.Nil(t, parsed)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
type mockServer struct {
|
|
subscribeF func(gnmi.GNMI_SubscribeServer) error
|
|
grpcServer *grpc.Server
|
|
}
|
|
|
|
func (*mockServer) Capabilities(context.Context, *gnmi.CapabilityRequest) (*gnmi.CapabilityResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*mockServer) Get(context.Context, *gnmi.GetRequest) (*gnmi.GetResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*mockServer) Set(context.Context, *gnmi.SetRequest) (*gnmi.SetResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockServer) Subscribe(server gnmi.GNMI_SubscribeServer) error {
|
|
return s.subscribeF(server)
|
|
}
|
|
|
|
func TestWaitError(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
grpcServer := grpc.NewServer()
|
|
gnmiServer := &mockServer{
|
|
subscribeF: func(gnmi.GNMI_SubscribeServer) error {
|
|
return errors.New("testerror")
|
|
},
|
|
grpcServer: grpcServer,
|
|
}
|
|
gnmi.RegisterGNMIServer(grpcServer, gnmiServer)
|
|
|
|
plugin := &GNMI{
|
|
Log: testutil.Logger{},
|
|
Addresses: []string{listener.Addr().String()},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
}
|
|
|
|
var acc testutil.Accumulator
|
|
require.NoError(t, plugin.Init())
|
|
require.NoError(t, plugin.Start(&acc))
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
acc.WaitError(1)
|
|
plugin.Stop()
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
|
|
// Check if the expected error text is among the errors
|
|
require.Len(t, acc.Errors, 1)
|
|
require.ErrorContains(t, acc.Errors[0], "aborted gNMI subscription: rpc error: code = Unknown desc = testerror")
|
|
}
|
|
|
|
func TestUsernamePassword(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
grpcServer := grpc.NewServer()
|
|
gnmiServer := &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
metadata, ok := metadata.FromIncomingContext(server.Context())
|
|
if !ok {
|
|
return errors.New("failed to get metadata")
|
|
}
|
|
|
|
username := metadata.Get("username")
|
|
if len(username) != 1 || username[0] != "theusername" {
|
|
return errors.New("wrong username")
|
|
}
|
|
|
|
password := metadata.Get("password")
|
|
if len(password) != 1 || password[0] != "thepassword" {
|
|
return errors.New("wrong password")
|
|
}
|
|
|
|
return errors.New("success")
|
|
},
|
|
grpcServer: grpcServer,
|
|
}
|
|
gnmi.RegisterGNMIServer(grpcServer, gnmiServer)
|
|
|
|
plugin := &GNMI{
|
|
Log: testutil.Logger{},
|
|
Addresses: []string{listener.Addr().String()},
|
|
Username: config.NewSecret([]byte("theusername")),
|
|
Password: config.NewSecret([]byte("thepassword")),
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
}
|
|
|
|
var acc testutil.Accumulator
|
|
require.NoError(t, plugin.Init())
|
|
require.NoError(t, plugin.Start(&acc))
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
acc.WaitError(1)
|
|
plugin.Stop()
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
|
|
// Check if the expected error text is among the errors
|
|
require.Len(t, acc.Errors, 1)
|
|
require.ErrorContains(t, acc.Errors[0], "aborted gNMI subscription: rpc error: code = Unknown desc = success")
|
|
}
|
|
|
|
func mockGNMINotification() *gnmi.Notification {
|
|
return &gnmi.Notification{
|
|
Timestamp: 1543236572000000000,
|
|
Prefix: &gnmi.Path{
|
|
Origin: "type",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "model",
|
|
Key: map[string]string{"foo": "bar"},
|
|
},
|
|
},
|
|
Target: "subscription",
|
|
},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "some"},
|
|
{
|
|
Name: "path",
|
|
Key: map[string]string{"name": "str", "uint64": "1234"}},
|
|
},
|
|
},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_IntVal{IntVal: 5678}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "other"},
|
|
{Name: "path"},
|
|
},
|
|
},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_StringVal{StringVal: "foobar"}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "other"},
|
|
{Name: "this"},
|
|
},
|
|
},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_StringVal{StringVal: "that"}},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestNotification(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
plugin *GNMI
|
|
server *mockServer
|
|
expected []telegraf.Metric
|
|
}{
|
|
{
|
|
name: "multiple metrics",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "alias",
|
|
Origin: "type",
|
|
Path: "/model",
|
|
SubscriptionMode: "sample",
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
notification := mockGNMINotification()
|
|
err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_Update{Update: notification}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
notification.Prefix.Elem[0].Key["foo"] = "bar2"
|
|
notification.Update[0].Path.Elem[1].Key["name"] = "str2"
|
|
notification.Update[0].Val = &gnmi.TypedValue{Value: &gnmi.TypedValue_JsonVal{JsonVal: []byte{'"', '1', '2', '3', '"'}}}
|
|
return server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_Update{Update: notification}})
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"alias",
|
|
map[string]string{
|
|
"path": "type:/model",
|
|
"source": "127.0.0.1",
|
|
"foo": "bar",
|
|
"name": "str",
|
|
"uint64": "1234",
|
|
},
|
|
map[string]interface{}{
|
|
"some/path": int64(5678),
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
testutil.MustMetric(
|
|
"alias",
|
|
map[string]string{
|
|
"path": "type:/model",
|
|
"source": "127.0.0.1",
|
|
"foo": "bar",
|
|
},
|
|
map[string]interface{}{
|
|
"other/path": "foobar",
|
|
"other/this": "that",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
testutil.MustMetric(
|
|
"alias",
|
|
map[string]string{
|
|
"path": "type:/model",
|
|
"foo": "bar2",
|
|
"source": "127.0.0.1",
|
|
"name": "str2",
|
|
"uint64": "1234",
|
|
},
|
|
map[string]interface{}{
|
|
"some/path": "123",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
testutil.MustMetric(
|
|
"alias",
|
|
map[string]string{
|
|
"path": "type:/model",
|
|
"source": "127.0.0.1",
|
|
"foo": "bar2",
|
|
},
|
|
map[string]interface{}{
|
|
"other/path": "foobar",
|
|
"other/this": "that",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "full path field key",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "PHY_COUNTERS",
|
|
Origin: "type",
|
|
Path: "/state/port[port-id=*]/ethernet/oper-speed",
|
|
SubscriptionMode: "sample",
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
response := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1543236572000000000,
|
|
Prefix: &gnmi.Path{
|
|
Origin: "type",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "state",
|
|
},
|
|
{
|
|
Name: "port",
|
|
Key: map[string]string{"port-id": "1"},
|
|
},
|
|
{
|
|
Name: "ethernet",
|
|
},
|
|
{
|
|
Name: "oper-speed",
|
|
},
|
|
},
|
|
Target: "subscription",
|
|
},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_IntVal{IntVal: 42},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return server.Send(response)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"PHY_COUNTERS",
|
|
map[string]string{
|
|
"path": "type:/state/port/ethernet/oper-speed",
|
|
"source": "127.0.0.1",
|
|
"port_id": "1",
|
|
},
|
|
map[string]interface{}{
|
|
"oper_speed": 42,
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "legacy tagged update pair",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "oc-intf-desc",
|
|
Origin: "openconfig-interfaces",
|
|
Path: "/interfaces/interface/state/description",
|
|
SubscriptionMode: "on_change",
|
|
TagOnly: true,
|
|
},
|
|
{
|
|
Name: "oc-intf-counters",
|
|
Origin: "openconfig-interfaces",
|
|
Path: "/interfaces/interface/state/counters",
|
|
SubscriptionMode: "sample",
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
tagResponse := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1543236571000000000,
|
|
Prefix: &gnmi.Path{},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Origin: "",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "interfaces",
|
|
},
|
|
{
|
|
Name: "interface",
|
|
Key: map[string]string{"name": "Ethernet1"},
|
|
},
|
|
{
|
|
Name: "state",
|
|
},
|
|
{
|
|
Name: "description",
|
|
},
|
|
},
|
|
Target: "",
|
|
},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "foo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := server.Send(tagResponse); err != nil {
|
|
return err
|
|
}
|
|
if err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil {
|
|
return err
|
|
}
|
|
taggedResponse := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1543236572000000000,
|
|
Prefix: &gnmi.Path{},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Origin: "",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "interfaces",
|
|
},
|
|
{
|
|
Name: "interface",
|
|
Key: map[string]string{"name": "Ethernet1"},
|
|
},
|
|
{
|
|
Name: "state",
|
|
},
|
|
{
|
|
Name: "counters",
|
|
},
|
|
{
|
|
Name: "in-broadcast-pkts",
|
|
},
|
|
},
|
|
Target: "",
|
|
},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_IntVal{IntVal: 42},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return server.Send(taggedResponse)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"oc-intf-counters",
|
|
map[string]string{
|
|
"source": "127.0.0.1",
|
|
"name": "Ethernet1",
|
|
"oc-intf-desc/description": "foo",
|
|
},
|
|
map[string]interface{}{
|
|
"in_broadcast_pkts": 42,
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "issue #11011",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
TagSubscriptions: []tagSubscription{
|
|
{
|
|
subscription: subscription{
|
|
Name: "oc-neigh-desc",
|
|
Origin: "openconfig",
|
|
Path: "/network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/state/description",
|
|
SubscriptionMode: "on_change",
|
|
},
|
|
Elements: []string{"network-instance", "protocol", "neighbor"},
|
|
},
|
|
},
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "oc-neigh-state",
|
|
Origin: "openconfig",
|
|
Path: "/network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/state/session-state",
|
|
SubscriptionMode: "on_change",
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
tagResponse := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1543236571000000000,
|
|
Prefix: &gnmi.Path{},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Origin: "",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "network-instances",
|
|
},
|
|
{
|
|
Name: "network-instance",
|
|
Key: map[string]string{"name": "default"},
|
|
},
|
|
{
|
|
Name: "protocols",
|
|
},
|
|
{
|
|
Name: "protocol",
|
|
Key: map[string]string{"name": "BGP", "identifier": "BGP"},
|
|
},
|
|
{
|
|
Name: "bgp",
|
|
},
|
|
{
|
|
Name: "neighbors",
|
|
},
|
|
{
|
|
Name: "neighbor",
|
|
Key: map[string]string{"neighbor_address": "192.0.2.1"},
|
|
},
|
|
{
|
|
Name: "state",
|
|
},
|
|
{
|
|
Name: "description",
|
|
},
|
|
},
|
|
Target: "",
|
|
},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "EXAMPLE-PEER"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := server.Send(tagResponse); err != nil {
|
|
return err
|
|
}
|
|
if err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil {
|
|
return err
|
|
}
|
|
taggedResponse := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1543236572000000000,
|
|
Prefix: &gnmi.Path{},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Origin: "",
|
|
Elem: []*gnmi.PathElem{
|
|
{
|
|
Name: "network-instances",
|
|
},
|
|
{
|
|
Name: "network-instance",
|
|
Key: map[string]string{"name": "default"},
|
|
},
|
|
{
|
|
Name: "protocols",
|
|
},
|
|
{
|
|
Name: "protocol",
|
|
Key: map[string]string{"name": "BGP", "identifier": "BGP"},
|
|
},
|
|
{
|
|
Name: "bgp",
|
|
},
|
|
{
|
|
Name: "neighbors",
|
|
},
|
|
{
|
|
Name: "neighbor",
|
|
Key: map[string]string{"neighbor_address": "192.0.2.1"},
|
|
},
|
|
{
|
|
Name: "state",
|
|
},
|
|
{
|
|
Name: "session-state",
|
|
},
|
|
},
|
|
Target: "",
|
|
},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "ESTABLISHED"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return server.Send(taggedResponse)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"oc-neigh-state",
|
|
map[string]string{
|
|
"source": "127.0.0.1",
|
|
"neighbor_address": "192.0.2.1",
|
|
"name": "default",
|
|
"oc-neigh-desc/description": "EXAMPLE-PEER",
|
|
"/network-instances/network-instance/protocols/protocol/name": "BGP",
|
|
"identifier": "BGP",
|
|
},
|
|
map[string]interface{}{
|
|
"session_state": "ESTABLISHED",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "issue #12257 Arista",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "interfaces",
|
|
Origin: "openconfig",
|
|
Path: "/interfaces/interface/state/counters",
|
|
SubscriptionMode: "sample",
|
|
SampleInterval: config.Duration(1 * time.Second),
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
if err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil {
|
|
return err
|
|
}
|
|
response := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1668762813698611837,
|
|
Prefix: &gnmi.Path{
|
|
Origin: "openconfig",
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "interfaces"},
|
|
{Name: "interface", Key: map[string]string{"name": "Ethernet1"}},
|
|
{Name: "state"},
|
|
{Name: "counters"},
|
|
},
|
|
Target: "OC-YANG",
|
|
},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "in-broadcast-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "in-discards"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "in-errors"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "in-fcs-errors"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "in-unicast-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-broadcast-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-discards"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-errors"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-multicast-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-octets"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{Elem: []*gnmi.PathElem{{Name: "out-unicast-pkts"}}},
|
|
Val: &gnmi.TypedValue{Value: &gnmi.TypedValue_UintVal{UintVal: 0}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return server.Send(response)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"interfaces",
|
|
map[string]string{
|
|
"path": "openconfig:/interfaces/interface/state/counters",
|
|
"source": "127.0.0.1",
|
|
"name": "Ethernet1",
|
|
},
|
|
map[string]interface{}{
|
|
"in_broadcast_pkts": uint64(0),
|
|
"in_discards": uint64(0),
|
|
"in_errors": uint64(0),
|
|
"in_fcs_errors": uint64(0),
|
|
"in_unicast_pkts": uint64(0),
|
|
"out_broadcast_pkts": uint64(0),
|
|
"out_discards": uint64(0),
|
|
"out_errors": uint64(0),
|
|
"out_multicast_pkts": uint64(0),
|
|
"out_octets": uint64(0),
|
|
"out_pkts": uint64(0),
|
|
"out_unicast_pkts": uint64(0),
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "issue #12257 Sonic",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(1 * time.Second),
|
|
EnforceFirstNamespaceAsOrigin: true,
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "temperature",
|
|
Origin: "openconfig-platform",
|
|
Path: "/components/component[name=TEMP 1]/state",
|
|
SubscriptionMode: "sample",
|
|
SampleInterval: config.Duration(1 * time.Second),
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
if err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil {
|
|
return err
|
|
}
|
|
response := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1668771585733542546,
|
|
Prefix: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "openconfig-platform:components"},
|
|
{Name: "component", Key: map[string]string{"name": "TEMP 1"}},
|
|
{Name: "state"},
|
|
},
|
|
Target: "OC-YANG",
|
|
},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "low-threshold"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_FloatVal{FloatVal: 0},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "timestamp"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "2022-11-18T11:39:26Z"},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "warning-status"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_BoolVal{BoolVal: false},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "name"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "CPU On-board"},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "critical-high-threshold"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_FloatVal{FloatVal: 94},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "current"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_FloatVal{FloatVal: 29},
|
|
},
|
|
},
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "temperature"},
|
|
{Name: "high-threshold"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_FloatVal{FloatVal: 90},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return server.Send(response)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"temperature",
|
|
map[string]string{
|
|
"path": "openconfig-platform:/components/component/state",
|
|
"source": "127.0.0.1",
|
|
"name": "TEMP 1",
|
|
},
|
|
map[string]interface{}{
|
|
"temperature/timestamp": "2022-11-18T11:39:26Z",
|
|
"temperature/low_threshold": float64(0),
|
|
"temperature/current": float64(29),
|
|
"temperature/high_threshold": float64(90),
|
|
"temperature/critical_high_threshold": float64(94),
|
|
"temperature/warning_status": false,
|
|
"name": "CPU On-board",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "Juniper Extension",
|
|
plugin: &GNMI{
|
|
Log: testutil.Logger{},
|
|
Encoding: "proto",
|
|
VendorSpecific: []string{"juniper_header"},
|
|
Redial: config.Duration(1 * time.Second),
|
|
EnforceFirstNamespaceAsOrigin: true,
|
|
Subscriptions: []subscription{
|
|
{
|
|
Name: "type",
|
|
Origin: "openconfig-platform",
|
|
Path: "/components/component[name=CHASSIS0:FPC0]/state",
|
|
SubscriptionMode: "sample",
|
|
SampleInterval: config.Duration(1 * time.Second),
|
|
},
|
|
},
|
|
},
|
|
server: &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
if err := server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil {
|
|
return err
|
|
}
|
|
response := &gnmi.SubscribeResponse{
|
|
Response: &gnmi.SubscribeResponse_Update{
|
|
Update: &gnmi.Notification{
|
|
Timestamp: 1668771585733542546,
|
|
Prefix: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "openconfig-platform:components"},
|
|
{Name: "component", Key: map[string]string{"name": "CHASSIS0:FPC0"}},
|
|
{Name: "state"},
|
|
},
|
|
Target: "OC-YANG",
|
|
},
|
|
Update: []*gnmi.Update{
|
|
{
|
|
Path: &gnmi.Path{
|
|
Elem: []*gnmi.PathElem{
|
|
{Name: "type"},
|
|
}},
|
|
Val: &gnmi.TypedValue{
|
|
Value: &gnmi.TypedValue_StringVal{StringVal: "LINECARD"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Extension: []*gnmi_ext.Extension{{
|
|
Ext: &gnmi_ext.Extension_RegisteredExt{
|
|
RegisteredExt: &gnmi_ext.RegisteredExtension{
|
|
// Juniper Header Extension
|
|
// EID_JUNIPER_TELEMETRY_HEADER = 1;
|
|
Id: 1,
|
|
Msg: func(jnprExt *jnpr_gnmi_extention.GnmiJuniperTelemetryHeaderExtension) []byte {
|
|
b, err := proto.Marshal(jnprExt)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return b
|
|
}(&jnpr_gnmi_extention.GnmiJuniperTelemetryHeaderExtension{ComponentId: 15, SubComponentId: 1, Component: "PICD"}),
|
|
},
|
|
},
|
|
}},
|
|
}
|
|
return server.Send(response)
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
testutil.MustMetric(
|
|
"type",
|
|
map[string]string{
|
|
"path": "openconfig-platform:/components/component/state",
|
|
"source": "127.0.0.1",
|
|
"name": "CHASSIS0:FPC0",
|
|
"component_id": "15",
|
|
"sub_component_id": "1",
|
|
"component": "PICD",
|
|
},
|
|
map[string]interface{}{
|
|
"type": "LINECARD",
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
tt.plugin.Addresses = []string{listener.Addr().String()}
|
|
|
|
grpcServer := grpc.NewServer()
|
|
tt.server.grpcServer = grpcServer
|
|
gnmi.RegisterGNMIServer(grpcServer, tt.server)
|
|
|
|
var acc testutil.Accumulator
|
|
require.NoError(t, tt.plugin.Init())
|
|
require.NoError(t, tt.plugin.Start(&acc))
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
acc.Wait(len(tt.expected))
|
|
tt.plugin.Stop()
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
|
|
testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(),
|
|
testutil.IgnoreTime())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedial(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
plugin := &GNMI{
|
|
Log: testutil.Logger{},
|
|
Addresses: []string{listener.Addr().String()},
|
|
Encoding: "proto",
|
|
Redial: config.Duration(10 * time.Millisecond),
|
|
Aliases: map[string]string{"dummy": "type:/model"},
|
|
}
|
|
|
|
grpcServer := grpc.NewServer()
|
|
gnmiServer := &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
notification := mockGNMINotification()
|
|
return server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_Update{Update: notification}})
|
|
},
|
|
grpcServer: grpcServer,
|
|
}
|
|
gnmi.RegisterGNMIServer(grpcServer, gnmiServer)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
var acc testutil.Accumulator
|
|
require.NoError(t, plugin.Init())
|
|
require.NoError(t, plugin.Start(&acc))
|
|
|
|
acc.Wait(2)
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
|
|
// Restart gNMI server at the same address
|
|
listener, err = net.Listen("tcp", listener.Addr().String())
|
|
require.NoError(t, err)
|
|
|
|
grpcServer = grpc.NewServer()
|
|
gnmiServer = &mockServer{
|
|
subscribeF: func(server gnmi.GNMI_SubscribeServer) error {
|
|
notification := mockGNMINotification()
|
|
notification.Prefix.Elem[0].Key["foo"] = "bar2"
|
|
notification.Update[0].Path.Elem[1].Key["name"] = "str2"
|
|
notification.Update[0].Val = &gnmi.TypedValue{Value: &gnmi.TypedValue_BoolVal{BoolVal: false}}
|
|
return server.Send(&gnmi.SubscribeResponse{Response: &gnmi.SubscribeResponse_Update{Update: notification}})
|
|
},
|
|
grpcServer: grpcServer,
|
|
}
|
|
gnmi.RegisterGNMIServer(grpcServer, gnmiServer)
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
acc.Wait(4)
|
|
plugin.Stop()
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestCases(t *testing.T) {
|
|
// Get all testcase directories
|
|
folders, err := os.ReadDir("testcases")
|
|
require.NoError(t, err)
|
|
|
|
// Register the plugin
|
|
inputs.Add("gnmi", func() telegraf.Input {
|
|
return &GNMI{
|
|
Redial: config.Duration(10 * time.Second),
|
|
EnforceFirstNamespaceAsOrigin: true,
|
|
}
|
|
})
|
|
|
|
for _, f := range folders {
|
|
// Only handle folders
|
|
if !f.IsDir() {
|
|
continue
|
|
}
|
|
|
|
t.Run(f.Name(), func(t *testing.T) {
|
|
testcasePath := filepath.Join("testcases", f.Name())
|
|
configFilename := filepath.Join(testcasePath, "telegraf.conf")
|
|
inputFilename := filepath.Join(testcasePath, "responses.json")
|
|
expectedFilename := filepath.Join(testcasePath, "expected.out")
|
|
expectedErrorFilename := filepath.Join(testcasePath, "expected.err")
|
|
|
|
// Load the input data
|
|
buf, err := os.ReadFile(inputFilename)
|
|
require.NoError(t, err)
|
|
var entries []json.RawMessage
|
|
require.NoError(t, json.Unmarshal(buf, &entries))
|
|
responses := make([]gnmi.SubscribeResponse, len(entries))
|
|
for i, entry := range entries {
|
|
require.NoError(t, protojson.Unmarshal(entry, &responses[i]))
|
|
}
|
|
|
|
// Prepare the influx parser for expectations
|
|
parser := &influx.Parser{}
|
|
require.NoError(t, parser.Init())
|
|
|
|
// Read the expected output if any
|
|
var expected []telegraf.Metric
|
|
if _, err := os.Stat(expectedFilename); err == nil {
|
|
var err error
|
|
expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Read the expected output if any
|
|
var expectedErrors []string
|
|
if _, err := os.Stat(expectedErrorFilename); err == nil {
|
|
var err error
|
|
expectedErrors, err = testutil.ParseLinesFromFile(expectedErrorFilename)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, expectedErrors)
|
|
}
|
|
|
|
// Configure the plugin
|
|
cfg := config.NewConfig()
|
|
require.NoError(t, cfg.LoadConfig(configFilename))
|
|
require.Len(t, cfg.Inputs, 1)
|
|
|
|
// Prepare the server response
|
|
responseFunction := func(server gnmi.GNMI_SubscribeServer) error {
|
|
for i := range responses {
|
|
if err := server.Send(&responses[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Setup a mock server
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
grpcServer := grpc.NewServer()
|
|
gnmiServer := &mockServer{
|
|
subscribeF: responseFunction,
|
|
grpcServer: grpcServer,
|
|
}
|
|
gnmi.RegisterGNMIServer(grpcServer, gnmiServer)
|
|
|
|
// Setup the plugin
|
|
plugin := cfg.Inputs[0].Input.(*GNMI)
|
|
plugin.Addresses = []string{listener.Addr().String()}
|
|
plugin.Log = testutil.Logger{}
|
|
|
|
// Start the server
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := grpcServer.Serve(listener); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
var acc testutil.Accumulator
|
|
require.NoError(t, plugin.Init())
|
|
require.NoError(t, plugin.Start(&acc))
|
|
|
|
require.Eventually(t,
|
|
func() bool {
|
|
return acc.NMetrics() >= uint64(len(expected))
|
|
}, 15*time.Second, 100*time.Millisecond)
|
|
plugin.Stop()
|
|
grpcServer.Stop()
|
|
wg.Wait()
|
|
|
|
// Check for errors
|
|
require.Len(t, acc.Errors, len(expectedErrors))
|
|
if len(acc.Errors) > 0 {
|
|
var actualErrorMsgs []string
|
|
for _, err := range acc.Errors {
|
|
actualErrorMsgs = append(actualErrorMsgs, err.Error())
|
|
}
|
|
require.ElementsMatch(t, actualErrorMsgs, expectedErrors)
|
|
}
|
|
|
|
// Check the metric nevertheless as we might get some metrics despite errors.
|
|
actual := acc.GetTelegrafMetrics()
|
|
testutil.RequireMetricsEqual(t, expected, actual, testutil.SortMetrics())
|
|
})
|
|
}
|
|
}
|