Greeter application#
Application lifecycle#
Let's have a look at how our main
function ends:
We initialize the greeter application struct by passing a logger and the pointer to the bond agent instance, and call the app.Start(ctx)
function. The Start
function is a place where we start the application's lifecycle.
greeter/app.go | |
---|---|
The Start
function is composed of the following parts:
- Start reading from the
a.NDKAgent.Notifications.FullConfigReceived
channel, that Bond agent will write to when the full configuration is received by the agent. It is purely a semaphore signal to the application that the configuration has been received and can be processed in full. - When the configuration notification signal is received, proceed with loading the configuration into the application' internal data structure.
- Process the received configuration by computing the
greeting
value - Update application's state with
name
andgreeting
values - Stop the application when the context is cancelled
When the app is stopped by a user (or even killed with a SIGKILL
) Bond will gracefully stop the application and de-register it on your behalf. Everything that needs to happen will happen behind the scenes, you don't need to do anything special unless you want to perform some custom cleaning steps.
Configuration load#
Time to have a closer look at why and how we load the application configuration. Starting with the "why" first. When we start the greeter application it doesn't hold any state of its own, besides the desired name. The application configuration is done via any of the SR Linux interfaces - CLI, gNMI, JSON-RPC, etc. Like it should be.
But then how does the greeter app get to know what a user has configured? Correct, it needs to receive the application configuration from SR Linux somehow. And NDK does provide this function.
First, our application has to have a structure that would hold its configuration and state data. For the greeter
app the structure that holds this data is named ConfigState
and it only has two fields - Name
and Greeting
. But the more complex the application configuration or state data becomes, the richer your struct would be.
And then
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"`
}
And then we need to populate this structure with the configuration that NDK sends our way. Here, again, Bond saves us quite a few cycles by providing us with the full configuration that Bond accumulated in the background via a.NDKAgent.Notifications.FullConfig
byte slice.
We just need to unmarshal this byte slice into the ConfigState
struct and we will receive the full configuration a user passed for our application via any of the SR Linux interfaces.
func (a *App) loadConfig() {
a.configState = &ConfigState{} // clear the configState
if a.NDKAgent.Notifications.FullConfig != nil {
err := json.Unmarshal(a.NDKAgent.Notifications.FullConfig, a.configState)
if err != nil {
a.logger.Error().Err(err).Msg("Failed to unmarshal config")
}
}
}
With this little function our application reliably receives its own configuration and can perform its business logic based on the configuration passed to it.
Application logic#
With the configuration loaded, the app can now perform its business logic. The business logic of the greeter
app is very simple:
- Take the
name
a user has configured the app with - Fetch the last-booted time from the SR Linux state and compute the uptime of the device
- Use the two values to compose a greeting message
As you can see, the logic is straightforwards, but it is a good example of how the application can use the configured values along the values received from the SR Linux state.
func (a *App) processConfig() {
if a.configState.Name == "" { // config is empty
return
}
uptime, err := a.getUptime()
if err != nil {
a.logger.Info().Msgf("failed to get uptime: %v", err)
return
}
a.configState.Greeting = "👋 Hi " + a.configState.Name +
fmt.Sprintf(", I am SR Linux and my uptime is %s!", uptime)
}
Fetching data with gNMI#
As we already mentioned, the greeter
app uses two data points to create the greeting message. The first one is the name
a user has configured the app with. The second one is the last-booted time from the SR Linux state. Our app gets the name
from the configuration, and the last-booted time we need to fetch from the SR Linux state as this is not part of the app's config.
An application developer can choose different ways to fetch data from SR Linux, but since Bond already provides a gNMI client, it might be the easiest way to fetch the data.
func (a *App) getUptime() (string, error) {
a.logger.Info().Msg("Fetching SR Linux last-booted time value")
// create a GetRequest
getReq, err := bond.NewGetRequest("/system/information/last-booted", api.EncodingPROTO())
if err != nil {
return "", err
}
getResp, err := a.NDKAgent.GetWithGNMI(getReq)
if err != nil {
return "", err
}
a.logger.Info().Msgf("GetResponse: %+v", getResp)
bootTimeStr := getResp.GetNotification()[0].GetUpdate()[0].GetVal().GetStringVal()
bootTime, err := time.Parse(time.RFC3339Nano, bootTimeStr)
if err != nil {
return "", err
}
currentTime := time.Now()
uptime := currentTime.Sub(bootTime).Round(time.Second)
return uptime.String(), nil
}
Using the bond.NewGetRequest
we construct a gNMI Get request by providing a path for the last-booted
state data. Then a.NDKAgent.GetWithGNMI(getReq)
sends the request to the SR Linux and receives the response.
All we have to do is parse the response, extract the last-booted value and calculate the uptime by subtracting the last-booted time from the current time.
Now we have all ingredients to compose the greeting message, which we save in the application' configState
structure:
a.configState.Greeting = "👋 Hi " + a.configState.Name +
fmt.Sprintf(", I am SR Linux and my uptime is %s!", uptime)
Posting app's state#
Ok, we've completed 90% of our greeter application. The last 10% is sending the computed greeting value back to SR Linux. This is what we call "updating the application state".
Right now the greeting message is nothing more than a string value in the application's configState
structure. But SR Linux doesn't know anything about it, only application does. Let's fix it.
Applications can post their state to SR Linux via NDK, this way the application state becomes visible to SR Linux and therefore the data can be fetched through any of the available interfaces. The greeter app has the updateState
function defined that does exactly that.
func (a *App) updateState() {
jsData, err := json.Marshal(a.configState)
if err != nil {
a.logger.Info().Msgf("failed to marshal json data: %v", err)
return
}
err = a.NDKAgent.UpdateState(AppRoot, string(jsData))
if err != nil {
a.logger.Error().Msgf("failed to update state: %v", err)
}
}
The updateState logic is rather straightforward. We have to convert the application's configState
structure into a json-serialized byte slice and then use Bond's UpdateState
function to post it to SR Linux.
We provide the application's YANG path (AppRoot
) and the string formatted json blob of the application's configState
structure.
This will populate the application's state in the SR Linux state and will become available for query over any of the supported management interfaces.
Summary#
That's it! We have successfully created a simple application that uses SR Linux's NetOps Development Kit and srl-labs/bond library that assists in the development process.
We hope that this guide has helped you understand the high-level steps every application needs to take out in order successfully use NDK to register itself with SR Linux, get its configuration, and update its state. You can now apply the core concepts you learned here to build your own applications that extend SR Linux functionality and tailor it to your needs.
Now let's see how we can package our app and make it installable on SR Linux.
Building and packaging the application