Developing agents with NDK in Go#
This guide explains how to consume the NDK service when developers write the agents in a Go1 programming language.
Note
This guide provides code snippets for several operations that a typical agent needs to perform according to the NDK Service Operations Flow chapter.
Where applicable, the chapters on this page will refer to the NDK Architecture section to provide more context on the operations.
In addition to the publicly available protobuf files, which define the NDK Service, Nokia also provides generated Go bindings for data access classes of NDK in a nokia/srlinux-ndk-go
repo.
The github.com/nokia/srlinux-ndk-go
package provided in that repository enables developers of NDK agents to immediately start writing NDK applications without the need to generate the Go package themselves.
Establish gRPC channel with NDK manager and instantiate an NDK client#
To call service methods, a developer first needs to create a gRPC channel to communicate with the NDK manager application running on SR Linux.
This is done by passing the NDK server address - localhost:50053
- to grpc.Dial()
as follows:
import (
"google.golang.org/grpc"
)
conn, err := grpc.Dial("localhost:50053", grpc.WithInsecure())
if err != nil {
...
}
defer conn.Close()
Once the gRPC channel is setup, we need to instantiate a client (often called stub) to perform RPCs. The client is obtained using the NewSdkMgrServiceClient
method provided.
Register the agent with the NDK manager#
Agent must be first registered with SR Linux by calling the AgentRegister
method available on the returned SdkMgrServiceClient
interface. The initial agent state is created during the registration process.
Agent's context#
Go context is a required parameter for each RPC service method. Contexts provide the means of enforcing deadlines and cancellations as well as transmitting metadata within the request.
During registration, SR Linux will be expecting a key-value pair with the agent_name
key and a value of the agent's name passed in the context of an RPC. The agent name is defined in the agent's YAML file.
Warning
Not including this metadata in the agent ctx
would result in an agent registration failure. SR Linux would not be able to differentiate between two agents both connected to the same NDK manager.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// appending agent's name to the context metadata
ctx = metadata.AppendToOutgoingContext(ctx, "agent_name", "ndkDemo")
Agent registration#
AgentRegister
method takes in the context ctx
that is by now has agent name as its metadata and an AgentRegistrationRequest
.
AgentRegistrationRequest
structure can be passed in with its default values for a basic registration request.
import "github.com/nokia/srlinux-ndk-go/v21/ndk"
r, err := client.AgentRegister(ctx, &ndk.AgentRegistrationRequest{})
if err != nil {
log.Fatalf("agent registration failed: %v", err)
}
AgentRegister
method returns AgentRegistrationResponse
and an error. Response can be additionally checked for status and error description.
Register notification streams#
Create subscription stream#
A subscription stream needs to be created first before any of the subscription types can be added.
SdkMgrServiceClient
first creates the subscription stream by executing NotificationRegister
method with a NotificationRegisterRequest
only field Op
set to a value of const NotificationRegisterRequest_Create
. This effectively creates a stream which is identified with a StreamID
returned inside the NotificationRegisterResponse
.
StreamId
must be associated when subscribing/unsubscribing to certain types of router notifications.
req := &ndk.NotificationRegisterRequest{
Op: ndk.NotificationRegisterRequest_Create,
}
resp, err := client.NotificationRegister(ctx, req)
if err != nil {
log.Fatalf("Notification Register failed with error: %v", err)
} else if resp.GetStatus() == ndk.SdkMgrStatus_kSdkMgrFailed {
r.log.Fatalf("Notification Register failed with status %d", resp.GetStatus())
}
log.Debugf("Notification Register was successful: StreamID: %d SubscriptionID: %d", resp.GetStreamId(), resp.GetSubId())
Add notification subscriptions#
Once the StreamId
is acquired, a client can register notifications of a particular type to be delivered over that stream.
Different types of notifications types can be subscribed to by calling the same NotificationRegister
method with a NotificationRegisterRequest
having Op
field set to NotificationRegisterRequest_AddSubscription
and certain SubscriptionType
selected.
In the example below we would like to receive notifications from the Config
service, hence we specify NotificationRegisterRequest_Config
subscription type.
subType := &ndk.NotificationRegisterRequest_Config{ // This is unique to each notification type (Config, Intf, etc.).
Config: &ndk.ConfigSubscriptionRequest{},
}
req := &ndk.NotificationRegisterRequest{
StreamId: resp.GetStreamId(), // StreamId is retrieved from the NotificationRegisterResponse
Op: ndk.NotificationRegisterRequest_AddSubscription,
SubscriptionTypes: subType,
}
resp, err := r.mgrStub.NotificationRegister(r.ctx, req)
if err != nil {
log.Fatalf("Agent could not subscribe for config notification")
} else if resp.GetStatus() == ndk.SdkMgrStatus_kSdkMgrFailed {
log.Fatalf("Agent could not subscribe for config notification with status %d", resp.GetStatus())
}
log.Infof("Agent was able to subscribe for config notification with status %d", resp.GetStatus())
Streaming notifications#
Actual streaming of notifications is a task for another service - SdkNotificationService
. This service requires developers to create its own client, which is done with NewSdkNotificationServiceClient
function.
The returned SdkNotificationServiceClient
interface has a single method NotificationStream
that is used to start streaming notifications.
NotificationsStream
is a server-side streaming RPC which means that SR Linux (server) will send back multiple event notification responses after getting the agent's (client) request.
To tell the server to start streaming notifications that were subscribed to before the NewSdkNotificationServiceClient
executes NotificationsStream
method where NotificationStreamRequest
struct has its StreamId
field set to the value that was obtained at subscription stage.
req := &ndk.NotificationStreamRequest{
StreamId: resp.GetStreamId(),
}
streamResp, err := notifClient.NotificationStream(ctx, req)
if err != nil {
log.Fatal("Agent failed to create stream client with error: ", err)
}
Handle the streamed notifications#
Handling notifications starts with reading the incoming notification messages and detecting which type this notification is exactly. When the type is known the client reads the fields of a certain notification. Here is the pseudocode that illustrates the flow:
func HandleNotifications(stream ndk.SdkNotificationService_NotificationStreamClient) {
for { // loop until stream returns io.EoF
notification stream response (nsr) := stream.Recv()
for notif in nsr.Notification { // nsr.Notification is a slice of `Notification`
if notif.GetConfig() is not nil {
1. config notif = notif.GetConfig()
2. handle config notif
} else if notif.GetIntf() is not nil {
1. intf notif = notif.GetIntf()
2. handle intf notif
} ... // Do this if statement for every notification type the agent is subscribed to
}
}
}
NotificationStream
method of the SdkNotificationServiceClient
interface will return a stream client SdkNotificationService_NotificationStreamClient
.
SdkNotificationService_NotificationStreamClient
contains a Recv()
to retrieve notifications one by one. At the end of a stream Rev()
will return io.EOF
.
Recv()
returns a *NotificationStreamResponse
which contains a slice of Notification
.
Notification
struct has GetXXX()
methods defined which retrieve the notification of a specific type. For example: GetConfig
returns ConfigNotification
.
Note
ConfigNotification
is returned only if Notification
struct has a certain subscription type set for its SubscriptionType
field. Otherwise, GetConfig
returns nil
.
Once the specific XXXNotification
has been extracted using the GetXXX()
method, users can access the fields of the notification and process the data contained within the notification using GetKey()
and GetData()
methods.
Exiting gracefully#
Agent needs to handle SIGTERM signal that is sent when a user invokes stop
command via SR Linux CLI. The following is the required steps to cleanly stop the agent:
- Remove any agent's state if it was set using
TelemetryDelete
method of a Telemetry client. - Delete notification subscriptions stream
NotificationRegister
method withOp
set toNotificationRegisterRequest_Delete
. - Invoke use
AgentUnRegister()
method of aSdkMgrServiceClient
interface. - Close gRPC channel with the
sdk_mgr
.
Logging#
To debug an agent, the developers can analyze the log messages that the agent produced. If the agent's logging facility used stdout/stderr to write log messages, then these messages will be found at /var/log/srlinux/stdout/
directory.
The default SR Linux debug messages are found in the messages directory /var/log/srlinux/buffer/messages
; check them when something went wrong within the SR Linux system (agent registration failed, IDB server warning messages, etc.).
Logrus is a popular structured logger for Go that can log messages of different levels of importance, but developers are free to choose whatever logging package they see fit.
-
Make sure that you have set up the dev environment as explained on this page. Readers are also encouraged to first go through the gRPC basic tutorial to get familiar with the common gRPC workflows when using Go. ↩