mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
As we are not constrained by time that much in the outlet, we can simplify the fetching of metadata by doing it synchronously. We still keep the breaker design to avoid continously polling a source that is not responsive, so we still can loose some data if we are not able to poll metadata. We also keep the background cache refresh. We also introduce a grace time of 1 minute to avoid loosing data during start. For the static provider, we wait for the remote data sources to be ready. For the gNMI provider, there are target windows of availability during which the cached data can be polled. The SNMP provider is loosing its ability to coalesce requests.
849 lines
28 KiB
Go
849 lines
28 KiB
Go
// SPDX-FileCopyrightText: 2023 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package gnmi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"akvorado/common/helpers"
|
|
"akvorado/common/reporter"
|
|
"akvorado/outlet/metadata/provider"
|
|
|
|
"github.com/openconfig/gnmi/proto/gnmi"
|
|
"github.com/openconfig/gnmic/pkg/api"
|
|
"github.com/scrapli/scrapligo/driver/network"
|
|
"github.com/scrapli/scrapligo/driver/options"
|
|
"github.com/scrapli/scrapligo/logging"
|
|
"github.com/scrapli/scrapligo/platform"
|
|
"github.com/scrapli/scrapligo/transport"
|
|
)
|
|
|
|
func waitSRLManagementServerReady(t *testing.T, d *network.Driver) {
|
|
const (
|
|
mgmtServerRdyCmd = "info from state system app-management application mgmt_server state | grep running"
|
|
readyForConfigCmd = "file cat /etc/opt/srlinux/devices/app_ephemeral.mgmt_server.ready_for_config"
|
|
)
|
|
retryTimer := time.Second
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("SR Linux management server not ready in time")
|
|
return
|
|
default:
|
|
resp, err := d.SendCommand(mgmtServerRdyCmd)
|
|
if err != nil || resp.Failed != nil {
|
|
time.Sleep(retryTimer)
|
|
continue
|
|
}
|
|
|
|
if !strings.Contains(resp.Result, "running") {
|
|
t.Log("SR Linux did not start management server yet")
|
|
time.Sleep(retryTimer)
|
|
continue
|
|
}
|
|
|
|
resp, err = d.SendCommand(readyForConfigCmd)
|
|
if err != nil || resp.Failed != nil {
|
|
time.Sleep(retryTimer)
|
|
continue
|
|
}
|
|
|
|
if !strings.Contains(resp.Result, "loaded initial configuration") {
|
|
t.Log("SR Linux did not load configuration yet")
|
|
time.Sleep(retryTimer)
|
|
continue
|
|
}
|
|
|
|
t.Log("SR Linux ready")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSRLinux(t *testing.T) {
|
|
const (
|
|
srLinuxUsername = "admin"
|
|
srLinuxPassword = "NokiaSrl1!"
|
|
)
|
|
// Connect to SR Linux container
|
|
srLinux := helpers.CheckExternalService(t, "SR Linux",
|
|
[]string{"srlinux:22", "127.0.0.1:57401"})
|
|
srLinuxHostname, srLinuxPortStr, _ := net.SplitHostPort(srLinux)
|
|
srLinuxPort, _ := strconv.Atoi(srLinuxPortStr)
|
|
t.Logf("SR Linux is listening at %s:%d", srLinuxHostname, srLinuxPort)
|
|
logger, err := logging.NewInstance(
|
|
logging.WithLogger(t.Log),
|
|
logging.WithLevel(logging.Critical), // can be changed if needed
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewInstance() error:\n%+v", err)
|
|
}
|
|
plat, err := platform.NewPlatform(
|
|
"nokia_srl", srLinuxHostname,
|
|
options.WithPort(srLinuxPort),
|
|
options.WithAuthNoStrictKey(),
|
|
options.WithAuthUsername(srLinuxUsername),
|
|
options.WithAuthPassword(srLinuxPassword),
|
|
options.WithTransportType(transport.StandardTransport),
|
|
options.WithLogger(logger),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewPlatform() error:\n%+v", err)
|
|
}
|
|
driver, err := plat.GetNetworkDriver()
|
|
if err != nil {
|
|
t.Fatalf("GetNetworkDriver() error:\n%+v", err)
|
|
}
|
|
for remaining := 3; remaining >= 0; remaining++ {
|
|
err = driver.Open()
|
|
if err != nil {
|
|
if remaining == 0 {
|
|
t.Fatalf("Open() error:\n%+v", err)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
defer driver.Close()
|
|
waitSRLManagementServerReady(t, driver)
|
|
resp, err := driver.SendCommand("show version")
|
|
if err != nil {
|
|
t.Fatalf("SendCommand(show version) error:\n%+v", err)
|
|
}
|
|
t.Logf(
|
|
"sent command '%s', output received (SendCommand):\n %s",
|
|
resp.Input,
|
|
resp.Result,
|
|
)
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendCommand(show version) error:\n%+v", resp.Failed)
|
|
}
|
|
|
|
// Initial configuration
|
|
resp, err = driver.SendConfig(`
|
|
load factory
|
|
|
|
/ system gnmi-server
|
|
set admin-state enable network-instance mgmt admin-state enable
|
|
|
|
commit now
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", err)
|
|
}
|
|
t.Logf(
|
|
"sent command '%s', output received (SendCommand):\n %s",
|
|
resp.Input,
|
|
resp.Result,
|
|
)
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", resp.Failed)
|
|
}
|
|
|
|
// gNMI setup
|
|
srLinuxGNMI := helpers.CheckExternalService(t, "SR Linux gNMI",
|
|
[]string{"srlinux:57400", "127.0.0.1:57400"})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
tg, err := api.NewTarget(
|
|
api.Address(srLinuxGNMI),
|
|
api.Username(srLinuxUsername),
|
|
api.Password(srLinuxPassword),
|
|
api.Timeout(time.Second),
|
|
api.Insecure(true),
|
|
)
|
|
tg.Config.RetryTimer = 10 * time.Millisecond
|
|
if err != nil {
|
|
t.Fatalf("api.NewTarget() error:\n%+v", err)
|
|
}
|
|
err = tg.CreateGNMIClient(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateGNMIClient() error:\n%+v", err)
|
|
}
|
|
defer tg.Close()
|
|
|
|
resetConfig := func(t *testing.T) {
|
|
t.Helper()
|
|
resp, err := driver.SendConfig(`
|
|
set / system name host-name "srlinux"
|
|
|
|
/ interface ethernet-1/1
|
|
set admin-state enable
|
|
set description "1st interface"
|
|
|
|
/ interface ethernet-1/2
|
|
set admin-state enable
|
|
set description "2nd interface"
|
|
set ethernet aggregate-id lag1
|
|
/ interface ethernet-1/3
|
|
set admin-state enable
|
|
set description "3rd interface"
|
|
set ethernet aggregate-id lag1
|
|
/ interface lag1
|
|
set admin-state enable
|
|
set description "lag interface"
|
|
set lag lag-type static
|
|
|
|
/ interface ethernet-1/4 subinterface 1
|
|
set admin-state enable
|
|
set description "4th interface"
|
|
|
|
commit now
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", err)
|
|
}
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", resp.Failed)
|
|
}
|
|
}
|
|
|
|
stopSubscription := func(t *testing.T, name string) {
|
|
t.Helper()
|
|
tg.StopSubscription(name)
|
|
subRspChan, subErrChan := tg.ReadSubscriptions()
|
|
endSubscription:
|
|
for {
|
|
select {
|
|
case resp := <-subRspChan:
|
|
t.Logf("Subscribe() extra response:\n%+v", resp)
|
|
case err := <-subErrChan:
|
|
t.Logf("Subscribe() error:\n%+v", err)
|
|
default:
|
|
break endSubscription
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, encoding := range []string{"json", "json_ietf"} {
|
|
// Test a "once" subscription
|
|
t.Run(fmt.Sprintf("subscribe once %s", encoding), func(t *testing.T) {
|
|
resetConfig(t)
|
|
subscribeReq, err := api.NewSubscribeRequest(
|
|
api.Subscription(api.Path("/system/name/host-name")),
|
|
api.Subscription(api.Path("/interface/name")),
|
|
api.Subscription(api.Path("/interface/description")),
|
|
api.Subscription(api.Path("/interface/subinterface/name")),
|
|
api.Subscription(api.Path("/interface/subinterface/description")),
|
|
api.Subscription(api.Path("/interface/ethernet/port-speed")),
|
|
api.Subscription(api.Path("/interface/lag/lag-speed")),
|
|
api.SubscriptionListModeONCE(),
|
|
api.Encoding(encoding),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSubscribeRequest() error:\n%+v", err)
|
|
}
|
|
subscribeResp, err := tg.SubscribeOnce(ctx, subscribeReq)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeOnce() error:\n%+v", err)
|
|
}
|
|
got := subscribeResponsesToEvents(subscribeResp)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
expected := []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface"},
|
|
{"/interface/description", "name=ethernet-1/2", "2nd interface"},
|
|
{"/interface/description", "name=ethernet-1/3", "3rd interface"},
|
|
{"/interface/description", "name=lag1", "lag interface"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/1", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/10", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/11", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/12", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/13", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/14", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/15", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/16", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/17", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/18", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/19", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/2", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/20", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/21", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/22", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/23", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/24", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/25", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/26", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/27", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/28", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/29", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/3", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/30", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/31", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/32", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/33", "10G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/34", "10G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/4", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/5", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/6", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/7", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/8", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=ethernet-1/9", "100G"},
|
|
{"/interface/ethernet/port-speed", "name=mgmt0", "1G"},
|
|
{"/interface/lag/lag-speed", "name=lag1", "0"},
|
|
// Despite being subscribed, we don't get these...
|
|
// {"/interface/name", "name=ethernet-1/1", "ethernet-1/1"},
|
|
// {"/interface/name", "name=ethernet-1/2", "ethernet-1/2"},
|
|
// {"/interface/name", "name=ethernet-1/3", "ethernet-1/3"},
|
|
// {"/interface/name", "name=lag1", "lag1"},
|
|
{"/interface/subinterface/description", "name=ethernet-1/4,index=1", "4th interface"},
|
|
{"/interface/subinterface/name", "name=ethernet-1/4,index=1", "ethernet-1/4.1"},
|
|
{"/interface/subinterface/name", "name=mgmt0,index=0", "mgmt0.0"},
|
|
{"/system/name/host-name", "", "srlinux"},
|
|
}
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Get() (-got, +want):\n%s", diff)
|
|
}
|
|
})
|
|
|
|
// Test a regular subscription with onchange
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
t.Run(fmt.Sprintf("subscribe changes %s", encoding), func(t *testing.T) {
|
|
resetConfig(t)
|
|
subscribeReq, err := api.NewSubscribeRequest(
|
|
api.Subscription(api.Path("/system/name/host-name"), api.SubscriptionModeON_CHANGE()),
|
|
api.Subscription(api.Path("/interface/description"), api.SubscriptionModeON_CHANGE()),
|
|
api.Subscription(api.Path("/interface/subinterface/name"), api.SubscriptionModeON_CHANGE()),
|
|
api.SubscriptionListModeSTREAM(),
|
|
api.Encoding(encoding),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSubscribeRequest() error:\n%+v", err)
|
|
}
|
|
subscriptionName := fmt.Sprintf("changes-%s", encoding)
|
|
go tg.Subscribe(ctx, subscribeReq, subscriptionName)
|
|
defer stopSubscription(t, subscriptionName)
|
|
subRspChan, subErrChan := tg.ReadSubscriptions()
|
|
|
|
// Wait for first set of answers
|
|
timer := time.NewTimer(time.Second)
|
|
responses := []*gnmi.SubscribeResponse{}
|
|
outer1:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
t.Fatalf("Subscribe(): no sync response")
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
case *gnmi.SubscribeResponse_SyncResponse:
|
|
break outer1
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
t.Fatalf("Subscribe() error:\n%+v", err)
|
|
}
|
|
}
|
|
}
|
|
got := subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
expected := []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface"},
|
|
{"/interface/description", "name=ethernet-1/2", "2nd interface"},
|
|
{"/interface/description", "name=ethernet-1/3", "3rd interface"},
|
|
{"/interface/description", "name=lag1", "lag interface"},
|
|
{"/interface/subinterface/name", "name=ethernet-1/4,index=1", "ethernet-1/4.1"},
|
|
{"/interface/subinterface/name", "name=mgmt0,index=0", "mgmt0.0"},
|
|
{"/system/name/host-name", "", "srlinux"},
|
|
}
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() initial sync (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
// Change the configuration and check for a change.
|
|
resp, err = driver.SendConfig(`
|
|
set / system name host-name "srlinux-new"
|
|
|
|
/ interface ethernet-1/1
|
|
set description "1st interface new"
|
|
|
|
/ interface ethernet-1/4
|
|
delete subinterface 1
|
|
|
|
commit now
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", err)
|
|
}
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", resp.Failed)
|
|
}
|
|
|
|
responses = []*gnmi.SubscribeResponse{}
|
|
expected = []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface new"},
|
|
// They should happen but they do not...
|
|
// {"/interface/subinterface/description", "name=ethernet-1/4,index=1", ""},
|
|
// {"/interface/subinterface/name", "name=ethernet-1/4,index=1", ""},
|
|
{"/system/name/host-name", "", "srlinux-new"},
|
|
}
|
|
timer = time.NewTimer(time.Second)
|
|
outer2:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
break outer2
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
t.Fatalf("Subscribe() error:\n%+v", err)
|
|
}
|
|
}
|
|
got := subscribeResponsesToEvents(responses)
|
|
if len(got) >= len(expected) {
|
|
break
|
|
}
|
|
}
|
|
got = subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() after change (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
// Test what happens when we disconnect
|
|
if encoding == "json" {
|
|
resp, err = driver.SendConfig(`
|
|
/ system gnmi-server admin-state enable network-instance mgmt
|
|
set admin-state disable
|
|
commit stay
|
|
set admin-state enable
|
|
commit now
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", err)
|
|
}
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", resp.Failed)
|
|
}
|
|
|
|
responses = []*gnmi.SubscribeResponse{}
|
|
errors := []string{}
|
|
timer = time.NewTimer(time.Second)
|
|
outer3:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
t.Fatalf("Subscribe(): no sync response")
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
case *gnmi.SubscribeResponse_SyncResponse:
|
|
break outer3
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
errors = append(errors, err.Err.Error())
|
|
}
|
|
}
|
|
}
|
|
got = subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
expected := []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface new"},
|
|
{"/interface/description", "name=ethernet-1/2", "2nd interface"},
|
|
{"/interface/description", "name=ethernet-1/3", "3rd interface"},
|
|
{"/interface/description", "name=lag1", "lag interface"},
|
|
{"/interface/subinterface/name", "name=mgmt0,index=0", "mgmt0.0"},
|
|
{"/system/name/host-name", "", "srlinux-new"},
|
|
}
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() after disconnect (-got, +want):\n%s", diff)
|
|
}
|
|
expectedErrors := []string{
|
|
"rpc error: code = Unavailable desc = Cancelling all calls",
|
|
"retrying in 10ms",
|
|
}
|
|
if diff := helpers.Diff(errors, expectedErrors); diff != "" {
|
|
t.Fatalf("Subscribe() errors after disconnect (-got, +want):\n%s", diff)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test a regular subscription with sampling
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
t.Run(fmt.Sprintf("subscribe sampling %s", encoding), func(t *testing.T) {
|
|
resetConfig(t)
|
|
subscribeReq, err := api.NewSubscribeRequest(
|
|
api.Subscription(
|
|
api.Path("/system/name/host-name"),
|
|
api.SubscriptionModeSAMPLE(),
|
|
api.SampleInterval(time.Second)),
|
|
api.Subscription(
|
|
api.Path("/interface/description"),
|
|
api.SubscriptionModeSAMPLE(),
|
|
api.SampleInterval(time.Second)),
|
|
api.SubscriptionListModeSTREAM(),
|
|
api.Encoding(encoding),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSubscribeRequest() error:\n%+v", err)
|
|
}
|
|
subscriptionName := fmt.Sprintf("sampling-%s", encoding)
|
|
go tg.Subscribe(ctx, subscribeReq, subscriptionName)
|
|
defer stopSubscription(t, subscriptionName)
|
|
subRspChan, subErrChan := tg.ReadSubscriptions()
|
|
|
|
// Wait for first set of answers
|
|
responses := []*gnmi.SubscribeResponse{}
|
|
timer := time.NewTimer(time.Second)
|
|
outer4:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
t.Fatal("Subscribe(): no sync response")
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
case *gnmi.SubscribeResponse_SyncResponse:
|
|
break outer4
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
t.Fatalf("Subscribe() error:\n%+v", err)
|
|
}
|
|
}
|
|
}
|
|
got := subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
expected := []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface"},
|
|
{"/interface/description", "name=ethernet-1/2", "2nd interface"},
|
|
{"/interface/description", "name=ethernet-1/3", "3rd interface"},
|
|
{"/interface/description", "name=lag1", "lag interface"},
|
|
{"/system/name/host-name", "", "srlinux"},
|
|
}
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() initial sync (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
// Get a second batch
|
|
timer = time.NewTimer(1200 * time.Millisecond)
|
|
responses = []*gnmi.SubscribeResponse{}
|
|
outer5:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
break outer5
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
t.Fatalf("Subscribe() error:\n%+v", err)
|
|
}
|
|
}
|
|
if len(subscribeResponsesToEvents(responses)) >= len(expected) {
|
|
break
|
|
}
|
|
}
|
|
got = subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() after sampling (-got, +want):\n%s", diff)
|
|
}
|
|
})
|
|
|
|
// Test a regular subscription with polling
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
t.Run(fmt.Sprintf("subscribe polling %s", encoding), func(t *testing.T) {
|
|
t.Skip("subscribe with polling does not work on SR Linux")
|
|
resetConfig(t)
|
|
subscribeReq, err := api.NewSubscribeRequest(
|
|
api.Subscription(api.Path("/system/name/host-name")),
|
|
api.Subscription(api.Path("/interface/description")),
|
|
api.SubscriptionListModePOLL(),
|
|
api.Encoding(encoding),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSubscribeRequest() error:\n%+v", err)
|
|
}
|
|
subscriptionName := fmt.Sprintf("polling-%s", encoding)
|
|
go tg.Subscribe(ctx, subscribeReq, subscriptionName)
|
|
defer stopSubscription(t, subscriptionName)
|
|
subRspChan, subErrChan := tg.ReadSubscriptions()
|
|
|
|
expected := []event{
|
|
{"/interface/description", "name=ethernet-1/1", "1st interface"},
|
|
{"/interface/description", "name=ethernet-1/2", "2nd interface"},
|
|
{"/interface/description", "name=ethernet-1/3", "3rd interface"},
|
|
{"/interface/description", "name=lag1", "lag interface"},
|
|
{"/system/name/host-name", "", "srlinux"},
|
|
}
|
|
|
|
// Get a few batches
|
|
time.Sleep(100 * time.Millisecond)
|
|
for i := range 3 {
|
|
if err := tg.SubscribePoll(ctx, subscriptionName); err != nil {
|
|
t.Fatalf("SubscribePoll() error:\n%+v", err)
|
|
}
|
|
timer := time.NewTimer(time.Second)
|
|
responses := []*gnmi.SubscribeResponse{}
|
|
outer6:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-timer.C:
|
|
break outer6
|
|
case resp := <-subRspChan:
|
|
if resp.SubscriptionName == subscriptionName {
|
|
switch resp.Response.Response.(type) {
|
|
case *gnmi.SubscribeResponse_Update:
|
|
responses = append(responses, resp.Response)
|
|
default:
|
|
t.Fatalf("Subscribe(): unknown type: %v", reflect.TypeOf(resp.Response.Response))
|
|
}
|
|
}
|
|
case err := <-subErrChan:
|
|
if err.SubscriptionName == subscriptionName {
|
|
t.Fatalf("Subscribe() error:\n%+v", err)
|
|
}
|
|
}
|
|
if len(subscribeResponsesToEvents(responses)) >= len(expected) {
|
|
break
|
|
}
|
|
}
|
|
got := subscribeResponsesToEvents(responses)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
if got[i].Path != got[j].Path {
|
|
return got[i].Path < got[j].Path
|
|
}
|
|
return got[i].Keys < got[j].Keys
|
|
})
|
|
if diff := helpers.Diff(got, expected); diff != "" {
|
|
t.Fatalf("Subscribe() after polling %d (-got, +want):\n%s", i+1, diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test the provider
|
|
if t.Failed() {
|
|
return
|
|
}
|
|
t.Run("provider", func(t *testing.T) {
|
|
resetConfig(t)
|
|
got := []string{}
|
|
lo := netip.MustParseAddr("::ffff:127.0.0.1")
|
|
config := DefaultConfiguration()
|
|
configP := config.(Configuration)
|
|
configP.MinimalRefreshInterval = time.Second
|
|
configP.AuthenticationParameters = helpers.MustNewSubnetMap(map[string]AuthenticationParameter{
|
|
"::/0": {
|
|
Username: srLinuxUsername,
|
|
Password: srLinuxPassword,
|
|
Insecure: true,
|
|
},
|
|
})
|
|
configP.Targets = helpers.MustNewSubnetMap(map[string]netip.Addr{
|
|
"::/0": netip.MustParseAddrPort(srLinuxGNMI).Addr(),
|
|
})
|
|
configP.Ports = helpers.MustNewSubnetMap(map[string]uint16{
|
|
"::/0": netip.MustParseAddrPort(srLinuxGNMI).Port(),
|
|
})
|
|
formatUpdate := func(exporter netip.Addr, iface string, answer provider.Answer) string {
|
|
return fmt.Sprintf("%s %v %s %s %s %s %d",
|
|
exporter.Unmap().String(), answer.Found, answer.Exporter.Name,
|
|
iface, answer.Interface.Name, answer.Interface.Description, answer.Interface.Speed)
|
|
}
|
|
r := reporter.NewMock(t)
|
|
p, err := configP.New(r)
|
|
if err != nil {
|
|
t.Fatalf("New() error:\n%+v", err)
|
|
}
|
|
|
|
// We need the indexes
|
|
subscribeReq, err := api.NewSubscribeRequest(
|
|
api.Subscription(api.Path("/interface/ifindex")),
|
|
api.Subscription(api.Path("/interface/subinterface/ifindex")),
|
|
api.SubscriptionListModeONCE(),
|
|
api.EncodingJSON(),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSubscribeRequest() error:\n%+v", err)
|
|
}
|
|
subscribeResp, err := tg.SubscribeOnce(ctx, subscribeReq)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeOnce() error:\n%+v", err)
|
|
}
|
|
indexes := map[string]uint{}
|
|
for _, event := range subscribeResponsesToEvents(subscribeResp) {
|
|
index, err := strconv.ParseUint(event.Value, 10, 32)
|
|
if err != nil {
|
|
t.Fatalf("ParseUint(%q) error:\n%+v", event.Value, err)
|
|
}
|
|
indexes[event.Keys] = uint(index)
|
|
}
|
|
t.Logf("indexes: %v", indexes)
|
|
|
|
// Wait a bit
|
|
answer, _ := p.Query(context.Background(), provider.Query{ExporterIP: lo, IfIndex: indexes["name=ethernet-1/1"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/1", answer))
|
|
answer, _ = p.Query(context.Background(), provider.Query{ExporterIP: lo, IfIndex: indexes["name=ethernet-1/2"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/2", answer))
|
|
answer, _ = p.Query(context.Background(), provider.Query{ExporterIP: lo, IfIndex: indexes["name=lag1"]})
|
|
got = append(got, formatUpdate(lo, "lag1", answer))
|
|
answer, _ = p.Query(context.Background(), provider.Query{ExporterIP: lo, IfIndex: indexes["name=ethernet-1/3"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/3", answer))
|
|
answer, _ = p.Query(context.Background(), provider.Query{ExporterIP: lo, IfIndex: 5})
|
|
got = append(got, formatUpdate(lo, "idx5", answer))
|
|
answer, _ = p.Query(context.Background(), provider.Query{ExporterIP: lo,
|
|
IfIndex: indexes["name=ethernet-1/4,index=1"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/4,index=1", answer))
|
|
|
|
if diff := helpers.Diff(got, []string{
|
|
"127.0.0.1 true srlinux ethernet-1/1 ethernet-1/1 1st interface 100000",
|
|
"127.0.0.1 true srlinux ethernet-1/2 ethernet-1/2 2nd interface 100000",
|
|
"127.0.0.1 true srlinux lag1 lag1 lag interface 0",
|
|
"127.0.0.1 true srlinux ethernet-1/3 ethernet-1/3 3rd interface 100000",
|
|
"127.0.0.1 false idx5 0",
|
|
"127.0.0.1 true srlinux ethernet-1/4,index=1 ethernet-1/4.1 4th interface 100000",
|
|
}); diff != "" {
|
|
t.Fatalf("Query() (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_provider_gnmi_", "-collector_seconds")
|
|
expectedMetrics := map[string]string{
|
|
`collector_count`: "1",
|
|
`collector_ready_info{exporter="127.0.0.1"}`: "1",
|
|
`encoding_info{encoding="json_ietf",exporter="127.0.0.1"}`: "1",
|
|
`model_info{exporter="127.0.0.1",model="Nokia SR Linux"}`: "1",
|
|
`paths_count{exporter="127.0.0.1"}`: "82",
|
|
`updates_total{exporter="127.0.0.1"}`: "1",
|
|
}
|
|
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
|
|
t.Errorf("Metrics (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
// Change configuration and check again
|
|
t.Log("modify and check again")
|
|
got = []string{}
|
|
resp, err = driver.SendConfig(`
|
|
/ interface ethernet-1/1
|
|
set description "1st interface new"
|
|
|
|
/ interface ethernet-1/4
|
|
delete subinterface 1
|
|
|
|
commit now
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", err)
|
|
}
|
|
if resp.Failed != nil {
|
|
t.Fatalf("SendConfig() error:\n%+v", resp.Failed)
|
|
}
|
|
time.Sleep(time.Second) // We should exceed the second now and next request will trigger a refresh
|
|
t.Log("start queries")
|
|
answer, _ = p.Query(context.Background(),
|
|
provider.Query{ExporterIP: lo, IfIndex: indexes["name=ethernet-1/1"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/1", answer))
|
|
answer, _ = p.Query(context.Background(),
|
|
provider.Query{ExporterIP: lo, IfIndex: indexes["name=ethernet-1/4,index=1"]})
|
|
got = append(got, formatUpdate(lo, "ethernet-1/4,index=1", answer))
|
|
if diff := helpers.Diff(got, []string{
|
|
// Fresh value
|
|
"127.0.0.1 true srlinux ethernet-1/1 ethernet-1/1 1st interface new 100000",
|
|
// Removed value
|
|
"127.0.0.1 false ethernet-1/4,index=1 0",
|
|
}); diff != "" {
|
|
t.Fatalf("Query() (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
gotMetrics = r.GetMetrics("akvorado_outlet_metadata_provider_gnmi_", "-collector_seconds")
|
|
expectedMetrics = map[string]string{
|
|
`collector_count`: "1",
|
|
`collector_ready_info{exporter="127.0.0.1"}`: "1",
|
|
`encoding_info{encoding="json_ietf",exporter="127.0.0.1"}`: "1",
|
|
`model_info{exporter="127.0.0.1",model="Nokia SR Linux"}`: "1",
|
|
`paths_count{exporter="127.0.0.1"}`: "79",
|
|
`updates_total{exporter="127.0.0.1"}`: "2",
|
|
}
|
|
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
|
|
t.Errorf("Metrics (-got, +want):\n%s", diff)
|
|
}
|
|
|
|
})
|
|
}
|