Skip to content

Application Instance#

At the end of the main function we create the instance of the greeter application by calling greeter.NewApp(ctx, &logger):

main.go
    app := greeter.NewApp(ctx, &logger)
    app.Start(ctx)

The NewApp function is defined in the greeter/app.go file and instantiates the App struct.

greeter/app.go
package greeter

type App struct {
    Name  string
    AppID uint32

    // configState holds the application configuration and state.
    configState *ConfigState
    // configReceivedCh chan receives the value when the full config
    // is received by the stream client.
    configReceivedCh chan struct{}

    gRPCConn     *grpc.ClientConn
    logger       *zerolog.Logger
    retryTimeout time.Duration

    gNMITarget *target.Target

    // NDK Service clients
    SDKMgrServiceClient       ndk.SdkMgrServiceClient
    NotificationServiceClient ndk.SdkNotificationServiceClient
    TelemetryServiceClient    ndk.SdkMgrTelemetryServiceClient
}

The App struct is the main structure of the greeter application. It holds the application config, state, logger instance, gNMI client and the NDK clients to communicate with the NDK services.

Creating the App Instance#

The NewApp function is the constructor of the App struct. It takes the context and the logger as arguments and returns the pointer to the App struct.

greeter/app.go
func NewApp(ctx context.Context, logger *zerolog.Logger) *App {
    // connect to NDK socket
    conn, err := connect(ctx, ndkSocket)
    if err != nil {
        logger.Fatal().
            Err(err).
            Msg("gRPC connect failed")
    }

    sdkMgrClient := ndk.NewSdkMgrServiceClient(conn)
    notifSvcClient := ndk.NewSdkNotificationServiceClient(conn)
    telemetrySvcClient := ndk.NewSdkMgrTelemetryServiceClient(conn)

    logger.Info().Msg("creating gNMI Client")
    target, err := newGNMITarget(ctx)
    if err != nil {
        logger.Fatal().Err(err).Msg("gNMI target creation failed")
    }

    // register agent
    // http://learn.srlinux.dev/ndk/guide/dev/go/#register-the-agent-with-the-ndk-manager
    r, err := sdkMgrClient.AgentRegister(ctx, &ndk.AgentRegistrationRequest{})
    if err != nil || r.Status != ndk.SdkMgrStatus_kSdkMgrSuccess {
        logger.Fatal().
            Err(err).
            Str("status", r.GetStatus().String()).
            Msg("Agent registration failed")
    }

    logger.Info().
        Uint32("app-id", r.GetAppId()).
        Str("name", AppName).
        Msg("Application registered successfully!")

    return &App{
        Name:  AppName,
        AppID: r.GetAppId(), //(1)!

        configState:      &ConfigState{},
        configReceivedCh: make(chan struct{}),

        logger:       logger,
        retryTimeout: 5 * time.Second,
        gRPCConn:     conn,

        gNMITarget: target,

        SDKMgrServiceClient:       sdkMgrClient,
        NotificationServiceClient: notifSvcClient,
        TelemetryServiceClient:    telemetrySvcClient,
    }
}

Connecting to NDK Socket#

As stated in the NDK Operations, the first thing we need to do is to connect to the NDK socket. This is what we do with the helper connect function inside the NewApp constructor:

greeter/app.go
package greeter

const (
    ndkSocket            = "unix:///opt/srlinux/var/run/sr_sdk_service_manager:50053"
    grpcServerUnixSocket = "unix:///opt/srlinux/var/run/sr_gnmi_server"
    AppName              = "greeter"
)

func connect(ctx context.Context, socket string) (*grpc.ClientConn, error) {
    conn, err := grpc.Dial(ndkSocket,
        grpc.WithTransportCredentials(insecure.NewCredentials()))

    return conn, err
}

The connection is made to the NDK manager's unix socket using unsecured transport. The insecure transport is justifiable in this case as the NDK manager is running on the same host as the application.

Creating NDK Clients#

Recall, that NDK is a collection of gRPC services, and each service requires a client to communicate with it.

The NewApp function creates the clients for the following services:

Creating clients is easy. We just leverage the Generated NDK Bindings and the ndk package contained in the github.com/nokia/srlinux-ndk-go module.

greeter/app.go
package greeter

import (
    // snip
    "github.com/nokia/srlinux-ndk-go/ndk"
    // snip
)

func NewApp(ctx context.Context, logger *zerolog.Logger) *App {
    // snip
    sdkMgrClient := ndk.NewSdkMgrServiceClient(conn)
    notifSvcClient := ndk.NewSdkNotificationServiceClient(conn)
    telemetrySvcClient := ndk.NewSdkMgrTelemetryServiceClient(conn)
    // snip
}

We pass to each client constructor function the gRPC connection we just created and off we go.

gNMI Client#

The NDK service collection allows your application to receive notifications from different SR Linux apps and services. But when it comes to changing SR Linux configuration or reading it your application needs to utilize one of the management interfaces.

Since it is very common to have the application either reading existing configuration or changing it, we wanted our greeter app to demonstrate how to do it.

Note

When your application needs to read its own config, it can do so by leveraging the Config notifications and NDK Notification Client. It is only when the application needs to configure SR Linux or read the configuration outside of its own config that it needs to use the management interfaces.

When the greeter app creates the greeting message it uses the following template:

👋 Hi ${name}, SR Linux was last booted at ${last-boot-time}

Since name value belongs to the greeter' application config, we can get this value later with the help of the NDK Notification Client. But the last-boot-time value is not part of the greeter app config and we need to get it from the SR Linux configuration. This is where we need greeter to use the management interface.

We opted to use the gNMI interface in this tutorial powered by the awesome gNMIc project. gNMIc project has lots of subcomponents revolving around gNMI, but we are going to use its API package to interact with the SR Linux's gNMI server.

In the NewApp function right after we created the NDK clients we create the gNMI client:

greeter/app.go
    logger.Info().Msg("creating gNMI Client")
    target, err := newGNMITarget(ctx)
    if err != nil {
        logger.Fatal().Err(err).Msg("gNMI target creation failed")
    }

The newGNMITarget function creates the gNMI Target using the gnmic API package. We provide the gRPC server unix socket as the address to establish the connection as well as hardcoded default credentials for SR Linux.

greeter/app.go
const (
    ndkSocket            = "unix:///opt/srlinux/var/run/sr_sdk_service_manager:50053"
    grpcServerUnixSocket = "unix:///opt/srlinux/var/run/sr_gnmi_server"
    AppName              = "greeter"
)

func newGNMITarget(ctx context.Context) (*target.Target, error) {
    // create a target
    tg, err := api.NewTarget(
        api.Name("srl"),
        api.Address(grpcServerUnixSocket),
        api.Username("admin"),
        api.Password("NokiaSrl1!"),
        api.Insecure(true),
    )
    if err != nil {
        return nil, err
    }

    // create a gNMI client
    err = tg.CreateGNMIClient(ctx)
    if err != nil {
        return nil, err
    }

    return tg, nil
}
gNMI Configuration on SR Linux

When you're using Containerlab-based lab environment, the gNMI server is configured to run over the unix socket as well, but when you run the greeter app in a production environment, you will have to make sure the relevant configuration is in place.

Once the target is created we create the gNMI client for it and returning the pointer to the target struct.

Registering the Agent#

Next task is to register the agent with the NDK manager. At this step NDK initializes the state of our agent, creates the IDB tables and assigns an ID to our application.

Registration is carried out by calling the AgentRegister function of the NDK manager client.

greeter/app.go
    r, err := sdkMgrClient.AgentRegister(ctx, &ndk.AgentRegistrationRequest{})
    if err != nil || r.Status != ndk.SdkMgrStatus_kSdkMgrSuccess {
        logger.Fatal().
            Err(err).
            Str("status", r.GetStatus().String()).
            Msg("Agent registration failed")
    }

We pass the empty &ndk.AgentRegistrationRequest{} as this is all we need to do to register the agent.

The AgentRegister function returns the AgentRegistrationResponse that contains the agent ID assigned by the NDK manager. We store this response in a variable, since we will need it later.

App Config and State#

The last bit is to initialize the structure for our app's config and state. This struct will hold the configured name, the computed greeting value. Here is how our ConfigState struct looks:

greeter/config.go
type ConfigState struct {
    // Name is the name to use in the greeting.
    Name string `json:"name,omitempty"`
    // Greeting is the greeting message to be displayed.
    Greeting string `json:"greeting,omitempty"`
}

The role of the receivedCh channel is explained in the Receiving Configuration section.

Finally, we return the pointer to the App struct from the NewApp function with struct fields initialized with the respective values.

greeter/app.go
    return &App{
        Name:  AppName,
        AppID: r.GetAppId(), //(1)!

        configState:      &ConfigState{},
        configReceivedCh: make(chan struct{}),

        logger:       logger,
        retryTimeout: 5 * time.Second,
        gRPCConn:     conn,

        gNMITarget: target,

        SDKMgrServiceClient:       sdkMgrClient,
        NotificationServiceClient: notifSvcClient,
        TelemetryServiceClient:    telemetrySvcClient,
    }
  1. Storing application ID received from the NDK manager when we registered the agent.

Next Step#

Once we initialized the app struct with the necessary clients we go back to the main function where app.Start(ctx) is called to start our application.

main.go
    app := greeter.NewApp(ctx, &logger)
    app.Start(ctx)

Let's see what happens there in the Notification Stream section.

Comments