JSON RPC client library implementation for SR Linux aimed to simplify the ways talking to SR Linux devices via JSON RPC.
Besides just self explanatory naming it's good to create a sort of the workflow to describe semantics of provided API. Giving some outlook on available methods and options is quite important as well, while working on the first implementation in GO is was not so obvious how provide simple interface at the same time hiding overall complexity via the exposure of well understandable interface elements. This document try to explore available without sophisticated scenarios like service activation, but rather focus on elementary steps and actions to build them.
For the sake of demo, we will use Containerlab as brilliant network simulation tool and dedicated clab setup published together with JSON RPC package lab.
The same virtual lab is used to perform integration testing as well as for client sample implementation.
The setup can be ramped-up by using cd _clab
followed by sudo clab deploy
command.
Our simple program will grow up each and every steps allowing to have necessary grip with the subject API.
It does not pretend to be comprehensive, but overall should be easy to learn. Over the next releases we will further extend it based on users feedback.
As you can see we have very simple code to start with JSON RPC client. While client is created necessary pre-flight checks are done in background to elicit system hostname and software version. That serving two objectives: verifying immediately and get HTTP client ready, giving minimum information about system in order to take decision about necessary YANG modules to use further if you would like to support ENV with mix of SR Linux versions.
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/azyablov/srljrpc"
"github.com/azyablov/srljrpc/apierr"
"github.com/azyablov/srljrpc/formats"
"github.com/azyablov/srljrpc/yms"
)
var (
host = "clab-evpn-leaf1"
user = "admin"
pass = "NokiaSrl1!"
port = 443
hostOC = "clab-evpn-spine3"
)
func main() {
// Create a new JSON RPC client with credentials and port (used 443 as default for the sake of demo).
c, err := srljrpc.NewJSONRPCClient(&host, srljrpc.WithOptCredentials(&user, &pass), srljrpc.WithOptPort(&port))
if err != nil {
panic(err)
}
fmt.Printf("Target hostname: %s\nTarget system version: %s\n", c.GetHostname(), c.GetSysVer())
}
Finally, our simple program is printing hostname and system version.
[azyablov@ecartman srljrpc_client_example]$ go run client.go
Target hostname: leaf1
Target system version: v23.3.2-106-g4490a15b16
[azyablov@ecartman srljrpc_client_example]$
Worth to mention that JSON RPC client has a few options available:
WithOptPort(port *int)
WithOptTimeout(t time.Duration)
WithOptCredentials(u, p *string)
WithOptTLS(t *TLSAttr)
All of them are quite self-descriptive, but WithOptTLS
should be a bit more explained to give 100% confidence.
First of all, JSON file to TLSAttr object looks like the following (taken from real lab):
{
"tls_attr": {
"skip_verify": false,
"cert_file": "/home/azyablov/clab/nokia-evpn-lab/clab-evpn/ca/spine1/spine1.pem",
"key_file": "/home/azyablov/clab/nokia-evpn-lab/clab-evpn/ca/spine1/spine1-key.pem",
"ca_file": "/home/azyablov/clab/nokia-evpn-lab/clab-evpn/ca/root/root-ca.pem"
}
}
The last could be read from file / string / everything implements Read interface, so basically nothing new:
var ta TLSAttr
err := json.Unmarshal([]byte(jsonStr), &ta)
if err != nil {
panic(err)
}
Now, let's read something from running configuration by using the next two xpaths:
/network-instance[name="MAC-VRF 1"]
/system/lldp
As you can see code is quite trivial:
// GET method example.
getResp, err := c.Get(`/network-instance[name="MAC-VRF 1"]`, `/system/lldp`)
if err != nil {
panic(err)
}
rStr, err := json.MarshalIndent(getResp.Result, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("Response from GET: %s\n", string(rStr))
As soon as we submitted two xpath, we are getting two elements in the list of getResp.Result
:
Target hostname: leaf1
Target system version: v23.3.2-106-g4490a15b16
================================================================================
Get() example:
================================================================================
Response: [
{
"type": "srl_nokia-network-instance:mac-vrf",
"interface": [
{
"name": "ethernet-1/1.1"
}
],
"vxlan-interface": [
{
"name": "vxlan0.1"
}
],
"protocols": {
"bgp-evpn": {
"srl_nokia-bgp-evpn:bgp-instance": [
{
"id": 1,
"admin-state": "enable",
"vxlan-interface": "vxlan0.1",
"evi": 1,
"ecmp": 2
}
]
},
"srl_nokia-bgp-vpn:bgp-vpn": {
"bgp-instance": [
{
"id": 1,
"route-distinguisher": {
"rd": "1:11"
},
"route-target": {
"export-rt": "target:65011:1",
"import-rt": "target:65011:1"
}
}
]
}
}
},
{
"admin-state": "enable"
}
]
Let's image we have to have some stats/operational state alongside with configuration info, so you need to use STATE datastore in order to get it.
Well, one shoot of Stats() methods is resolving it in quite convenient way.
In the example below we are using /system/json-rpc-server
xpath.
// Getting stats.
fmt.Println("State() example:")
stateResp, err := c.State("/system/json-rpc-server")
if err != nil {
panic(err)
}
outHelper(stateResp.Result)
In order to read code more readable outHelper() function was introduced, which is unmarshalling and printing results:
func outHelper(v any) {
rStr, err := json.MarshalIndent(v, "", " ")
if err != nil {
panic(err)
}
fmt.Printf("%s\n", string(rStr))
}
So, you should see something more on top of already mentioned output.
State() example:
================================================================================
[
{
"admin-state": "enable",
"commit-confirmed-timeout": 0,
"network-instance": [
{
"name": "mgmt",
"http": {
"admin-state": "enable",
"oper-state": "up",
"use-authentication": true,
"session-limit": 10,
"port": 80,
"source-address": [
"::"
]
},
"https": {
"admin-state": "enable",
"oper-state": "up",
"use-authentication": true,
"session-limit": 10,
"port": 443,
"tls-profile": "clab-profile",
"source-address": [
"::"
]
}
}
],
"unix-socket": {
"admin-state": "disable",
"oper-state": "down",
"use-authentication": true,
"socket-path": ""
}
}
]
================================================================================
Example below is reading values before UPDATE/DELETE/REPLACE operations and executing them respectively.
Validate()
and Tools()
methods are available as well,
first is used to validate configuration updates w/o applying it, the second one used to perform /tools operations like /tools interface ethernet-1/1 statistics clear
.
Confirmation timeout (ct
) must be set to 0
to apply changes immediately, or positive int to allow additional verification checks before explicit confirmation OR rolling it back automatically.
// Updating/Replacing/Deleting config
fmt.Println(strings.Repeat("=", 80))
fmt.Println("Update()/Delete()/Replace() example:")
fmt.Println(strings.Repeat("=", 80))
pvs := []srljrpc.PV{
{Path: `/interface[name=ethernet-1/51]/subinterface[index=0]/description`, Value: "UPDATE"},
{Path: `/system/banner`, Value: "DELETE"},
{Path: `/interface[name=mgmt0]/description`, Value: "REPLACE"},
}
// Getting existing config for the sake of demo.
for _, pv := range pvs {
getResp, err := c.Get(pv.Path)
if err != nil {
panic(err)
}
outHelper(getResp.Result)
}
mdmResp, err := c.Update(0, pvs[0]) // setting 0 as confirmation timeout to apply changes immediately.
if err != nil {
panic(err)
}
outHelper(mdmResp.Result)
mdmResp, err = c.Delete(0, pvs[1].Path) // setting 0 as confirmation timeout to apply changes immediately.
if err != nil {
panic(err)
}
outHelper(mdmResp.Result)
mdmResp, err = c.Replace(0, pvs[2]) // setting 0 as confirmation timeout to apply changes immediately.
if err != nil {
panic(err)
}
outHelper(mdmResp.Result)
Effectively one operation is just four lines of code:
mdmResp, err = c.Replace(0, pvs[2]) // setting 0 as confirmation timeout to apply changes immediately.
if err != nil {
panic(err)
}
Console output should be altered by the following contents:
Update()/Delete()/Replace() example:
[
"to_spine1"
]
[
{
"login-banner": "................................................................\n: Welcome to Nokia SR Linux! :\n: Open Network OS for the NetOps era. :\n: :\n: This is a freely distributed official container image. :\n: Use it - Share it :\n: :\n: Get started: https://learn.srlinux.dev :\n: Container: https://go.srlinux.dev/container-image :\n: Docs: https://doc.srlinux.dev/22-6 :\n: Rel. notes: https://doc.srlinux.dev/rn22-6-2 :\n: YANG: https://yang.srlinux.dev/v22.6.2 :\n: Discord: https://go.srlinux.dev/discord :\n: Contact: https://go.srlinux.dev/contact-sales :\n................................................................\n"
}
]
[
{}
]
[
{}
]
[
{}
]
[
{}
]
If we would again query it we should get the following output, since commit was applied before (automatically by JSON RPC servers) and running configuration updated:
================================================================================
Update()/Delete()/Replace() example:
================================================================================
[
"UPDATE"
]
================================================================================
[
{}
]
================================================================================
[
"REPLACE"
]
================================================================================
[
{}
]
================================================================================
[
{}
]
================================================================================
[
{}
]
Worth to mention, BulkSet(delete []PV, replace []PV, update []PV, ym yms.EnumYmType, ct int) (*Response, error)
method.
It allows you to combine delete, replace and update actions into the one SET request, so you can combine operations efficiently.
YANG models namespace could be specified as well, where SRL corresponds to native models and OC to OpenConfig models.
Confirmation timeout (ct
) must be set to 0
to apply changes immediately, or positive int to allow additional verification checks before explicit confirmation OR rolling it back automatically.
Well, let's imagine you need to clear BGP session or reset counters on interface.
An example below provides with how to do it using Tools()
method.
toolsResp, err := c.Tools(srljrpc.PV{
Path: "/interface[name=ethernet-1/1]/ethernet/statistics/clear",
Value: srljrpc.CommandValue("")})
if err != nil {
panic(err)
}
outHelper(toolsResp.Result)
If request is correct and no mistakes you should not see anything special in output.
c.Tools() example:
================================================================================
[
{}
]
================================================================================
Here we will consider number of examples to demonstrate ways to use diff
method, OpenConfig models namespace and improved error handling.
We will use OpenCOnfig, because by default RPC interface assumes SRL, and as such we got number of examples already.
The first example demonstrates typical case of JSON RPC Error.
// Then for the sake of example we will use DIFF method with Bulk update: TestBulkDiffCandidate.
// DiffCandidate method is more simple and intended to use in cases you require only one action out of three: UPDATE, DELETE, REPLACE.
// That's essentially Bulk update with different operations: UPDATE, DELETE, REPLACE, while using yang-models of OpenConfig.
fmt.Println(strings.Repeat("=", 80))
fmt.Println("BulkDiff() example with error:")
fmt.Println(strings.Repeat("=", 80))
pvs = []srljrpc.PV{
{Path: `/system/config/login-banner`, Value: "DELETE"},
{Path: `/interfaces/interface[name=mgmt0]/config/description`, Value: "REPLACE"},
{Path: `/interfaces/interface[name=ethernet-1/11]/subinterfaces/subinterface[index=0]/config/description`, Value: "UPDATE"},
}
bulkDiffResp, err := c.BulkDiff(pvs[0:1], pvs[1:2], pvs[2:], yms.OC)
if err != nil {
if cerr, ok := err.(apierr.ClientError); ok {
fmt.Printf("ClientError error: %s\n", cerr) // ClientError
if cerr.Code == apierr.ErrClntJSONRPC { // We expect JSON RPC error here and checking via the message code.
outHelper(bulkDiffResp)
// Output supposed to be something like this:
// {
// "jsonrpc": "2.0",
// "id": 568258505525892051,
// "error": {
// "id": 0,
// "message": "Server down or restarting"
// }
// }
// This is an indication OC is not supported on the target system, so we will use another target system spine3.
}
} else {
panic(err) // Unexpected outcome.
}
} else {
outHelper(bulkDiffResp.Result)
}
In the console you should see something similar to:
================================================================================
c.TestBulkDiffCandidate() example with error:
================================================================================
ClientError error: do: JSON-RPC error
{
"jsonrpc": "2.0",
"id": 3198004165524188886,
"error": {
"id": 0,
"message": "Server down or restarting"
}
}
================================================================================
Out of the error message we can figure out that out target is not serving OpenConfig namespace, so we need to switch target (spine3 in our example).
The next case demonstrates how to approach error handling provided by the module.
In order to keep necessary abstraction level, but allow diving deep into the root cause and allows extensive error handling automation.
Idiomatic golang approach implemented with Unwrap() error
method, while decision can be take based on the codes provided by ClientError
or MessageError
object.
apierr
package is self-documented and provides quite extensive error codes footprint.
const (
ErrClntUndefined EnumCltErr = iota
ErrClntNoHost
ErrClntTargetVerification
ErrClntMarshalling
ErrClntHTTPReqCreation
ErrClntHTTPSend
ErrClntHTTPStatus
// <omitted for brevity>
)
Coming to our example...
fmt.Println(strings.Repeat("=", 80))
fmt.Println("BulkDiff() example with error:")
fmt.Println(strings.Repeat("=", 80))
pvs = []srljrpc.PV{
{Path: `/system/config/login-banner`, Value: "DELETE"},
{Path: `/interfaces/interface[name=mgmt0]/config/description`, Value: ""}, // Empty value will cause an error.
{Path: `/interfaces/interface[name=ethernet-1/11]/subinterfaces/subinterface[index=0]/config/description`, Value: "UPDATE"},
}
// Change target hostname to spine3, which supports OC.
// Create a new JSON RPC client with credentials and port (used 443 as default for the sake of demo).
cOC, err := srljrpc.NewJSONRPCClient(&hostOC, srljrpc.WithOptCredentials(&user, &pass), srljrpc.WithOptPort(&port))
if err != nil {
panic(err)
}
bulkDiffResp, err = cOC.BulkDiff(pvs[0:1], pvs[1:2], pvs[2:], yms.OC)
if err != nil {
// Unwrapping error to investigate a root cause.
if cerr, ok := err.(apierr.ClientError); ok {
fmt.Printf("ClientError error: %s\n", cerr) // ClientError
for uerr := err.(apierr.ClientError).Unwrap(); uerr != nil; { // We expect ClientError here, so we can unwrap it.
fmt.Printf("Underlaying error: %s\n", uerr.Error())
if u2err, ok := uerr.(interface{ Unwrap() error }); ok {
uerr = u2err.Unwrap()
} else {
break
}
}
}
// }
} else {
outHelper(bulkDiffResp.Result)
}
And finally output demonstrates two nested errors...
================================================================================
BulkDiff() example with error:
================================================================================
ClientError error: bulkDiffCandidate: RPC request creation error
Underlaying error: newRequest(): error adding commands in request
Underlaying error: value isn't specified or not found in the path for method diff
After corrections made, we should have our code executed without errors.
fmt.Println(strings.Repeat("=", 80))
fmt.Println("BulkDiff() example w/o error:")
fmt.Println(strings.Repeat("=", 80))
// Adding changes into PV pairs to fix our artificial error and do things right ))
pvs = []srljrpc.PV{
{Path: `/system/config/login-banner`, Value: "DELETE"},
{Path: `/interfaces/interface[name=mgmt0]/config/description`, Value: "REPLACE"},
{Path: `/interfaces/interface[name=ethernet-1/11]/subinterfaces/subinterface[index=0]/config/description`, Value: "UPDATE"},
}
bulkDiffResp, err = cOC.BulkDiff(pvs[0:1], pvs[1:2], pvs[2:], yms.OC)
if err != nil {
outHelper(bulkDiffResp)
panic(err)
}
// Parsing JSON response to get the message.
var data []interface{}
err = json.Unmarshal(bulkDiffResp.Result, &data)
if err != nil {
fmt.Println("Error parsing JSON:", err)
}
message := data[0].(string)
fmt.Println(message)
================================================================================
BulkDiff() example w/o error:
================================================================================
{
"interfaces": {
"interface": [
{
"name": "ethernet-1/11",
"subinterfaces": {
"subinterface": [
{
"index": 0,
"config": {
- "description": "to_leaf1"
+ "description": "UPDATE"
}
}
]
}
},
{
"name": "mgmt0",
"config": {
+ "description": "REPLACE"
}
}
]
},
"system": {
"config": {
- "login-banner": "................................................................\n: Welcome to Nokia SR Linux! :\n: Open Network OS for the NetOps era. :\n: :\n: This is a freely distributed official container image. :\n: Use it - Share it :\n: :\n: Get started: https://learn.srlinux.dev :\n: Container: https://go.srlinux.dev/container-image :\n: Docs: https://doc.srlinux.dev/23-3 :\n: Rel. notes: https://doc.srlinux.dev/rn23-3-1 :\n: YANG: https://yang.srlinux.dev/v23.3.1 :\n: Discord: https://go.srlinux.dev/discord :\n: Contact: https://go.srlinux.dev/contact-sales :\n................................................................\n"
}
}
}
================================================================================
As it was mentioned before Update()/Replace()/Delete()
function provide ct
parameter, which was set to 0
before.
Setting it to something >0
must trigger rollback on the switch, if changes aren't confirmed on time via TOOLS datastore /system/configuration/confirmed-accept
.
Library provides a bit more advanced function BulkSetCallBack()
allowing to encapsulate your verification logic inside your function, which must satisfy exposed interface.
// CallBackConfirm type to represent a callback function to confirm a request.
// In case of confirm commit must return true, otherwise false.
type CallBackConfirm func(req *Request, resp *Response) (bool, error)
In the example below BulkSetCallBack()
called to apply interface description. The provided call back function just prints our RPC request / response and
returns false
to allow changes roll-back on SR Linux switch automatically, i.e. not confirming them.
func confirmCallBack(req *srljrpc.Request, resp *srljrpc.Response) (bool, error) {
// This is a callback function to be called after confirmation timeout is expired.
// It is supposed to be used to confirm or cancel changes as per logic of the implementation.
// In this example we will just print out request and response to console and confirm changes - for the sake ot example that's replace sophisticated logic.
fmt.Println("Request:")
outHelper(req)
fmt.Println("Response:")
outHelper(resp)
return false, nil
}
Client implementation example with BulkSetCallBack()
runs it in separate thread, as such allowing parallel executions against several targets(switches).
At the same time CallBack function has all necessary information (request and response) to implement verification logic as part of CI pipeline
in order to take necessary decision whether roll back or not roll back changes on the target.
fmt.Println(strings.Repeat("=", 80))
fmt.Println("BulkSetCallBack() with cancellation:")
fmt.Println(strings.Repeat("=", 80))
empty := []srljrpc.PV{}
sysInfPath := "/interface[name=system0]/description"
initVal := []srljrpc.PV{{Path: sysInfPath, Value: srljrpc.CommandValue("INITIAL")}}
_, err = c.Update(0, initVal[0]) // should be no error and system0 interface description should be set to "INITIAL".
if err != nil {
panic(err)
}
getResp, err = c.Get(sysInfPath)
if err != nil {
panic(err)
}
fmt.Printf("Get() against %s before BulkSetCallBack():\n", sysInfPath)
fmt.Println(strings.Repeat("=", 80))
outHelper(getResp.Result)
chResp := make(chan *srljrpc.Response) // Channel for response.
chErr := make(chan error) // Channel for error.
go func() {
newValueToConfirm := []srljrpc.PV{{Path: sysInfPath, Value: srljrpc.CommandValue("System Loopback")}}
// setting confirmation timeout to 30 seconds to allow comfortable time to verify changes. Setting 27 seconds as time to exec call back function.
// confirmCallBack is a function to be called after confirmation timeout is expired to confirm or cancel changes as per logic of the implementation.
resp, err := c.BulkSetCallBack(empty, empty, newValueToConfirm, yms.SRL, 8, 5, confirmCallBack)
// sending response and error to channels back to main thread.
chResp <- resp
chErr <- err
}()
// Meanwhile we can do something else in main thread.
// For example, we can get current value of the interface.
time.Sleep(2 * time.Second) // Allow 3 seconds to apply changes.
getResp, err = c.Get(sysInfPath)
if err != nil {
panic(err)
}
fmt.Printf("Get() against %s:\n", sysInfPath)
fmt.Println(strings.Repeat("=", 80))
outHelper(getResp.Result)
// Waiting for response and error from channel.
resp := <-chResp
err = <-chErr
if err != nil {
panic(err)
}
// We expect response to be nil, as we set confirmation timeout to 30 seconds and call back function to 27 seconds.
if resp != nil {
fmt.Println("Unexpected response. Expected nil.")
outHelper(resp) // Unexpected outcome.
}
time.Sleep(30 * time.Second) // Allow enough time to rollback changes.
getResp, err = c.Get(sysInfPath)
if err != nil {
panic(err)
}
fmt.Printf("Get() against %s after confirmation timeout expired:\n", sysInfPath)
fmt.Println(strings.Repeat("=", 80))
outHelper(getResp.Result)
Output should look similar to the following one:
================================================================================
BulkSetCallBack() with cancellation:
================================================================================
Get() against /interface[name=system0]/description before BulkSetCallBack():
================================================================================
[
"INITIAL"
]
================================================================================
Get() against /interface[name=system0]/description:
================================================================================
[
"System Loopback"
]
================================================================================
Request:
{
"jsonrpc": "2.0",
"id": 1560953543165558821,
"method": "set",
"params": {
"commands": [
{
"path": "/interface[name=system0]/description",
"value": "System Loopback",
"action": "update"
}
],
"output-format": "json",
"datastore": "candidate",
"yang-models": "srl",
"confirm-timeout": 8
}
}
================================================================================
Response:
{
"jsonrpc": "2.0",
"id": 1560953543165558821,
"result": [
{}
]
}
================================================================================
Get() against /interface[name=system0]/description after confirmation timeout expired:
================================================================================
[
"INITIAL"
]
================================================================================
Sending CLI commands is one of the main methods to interact with network devices, even industry is rapidly adopting MDM interfaces. Lab builds, validations, troubleshooting and many other operational tasks would require interaction with CLI. JSON RPC interface of SR Linux is providing very convenient way to automate and use CLI commands, especially we you are using number of CLI plug-ins to elicit essential information, but still giving you full flexibility to utilize structured data outputs.
In CLI example below two commands are executed with output format JSON and one with out format TABLE.
For the output format TABLE []string
type were used to marshal it correctly into string and print in nice form in STDOUT.
As easy to see number of line of code remains +/- stable in terms of getting necessary results. Of course, assuming logic of your application is not counted here.
// CLI
fmt.Println("c.CLI() example:")
cliResp, err := c.CLI([]string{"show version", "show network-instance summary"}, formats.JSON)
if err != nil {
panic(err)
}
outHelper(cliResp.Result)
cliResp, err = c.CLI([]string{"show system lldp neighbor"}, formats.TABLE)
if err != nil {
panic(err)
}
type Table []string
var t Table
b, _ := cliResp.Result.MarshalJSON()
err = json.Unmarshal(b, &t)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", t[0])
================================================================================
c.CLI() example:
================================================================================
[
{
"basic system info": {
"Hostname": "leaf1",
"Chassis Type": "7220 IXR-D2",
"Part Number": "Sim Part No.",
"Serial Number": "Sim Serial No.",
"System HW MAC Address": "1A:0B:01:FF:00:00",
"Software Version": "v23.3.1",
"Build Number": "343-gab924f2e64",
"Architecture": "x86_64",
"Last Booted": "2023-05-29T09:07:32.174Z",
"Total Memory": "23640339 kB",
"Free Memory": "7898847 kB"
}
},
{
"Network Instance": [
{
"Name": "MAC-VRF 1",
"Type": "mac-vrf",
"Admin state": "enable",
"Oper state": "up",
"Router id": "N/A"
},
{
"Name": "default",
"Type": "default",
"Admin state": "enable",
"Oper state": "up"
},
{
"Name": "mgmt",
"Type": "ip-vrf",
"Admin state": "enable",
"Oper state": "up",
"Description": "Management network instance"
}
]
}
]
================================================================================
+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+
| Name | Neighbor | Neighbor System Name | Neighbor Chassis ID | Neighbor First Message | Neighbor Last Update | Neighbor Port |
+===========================+===========================+===========================+===========================+===========================+===========================+===========================+
| ethernet-1/51 | 1A:35:06:FF:00:00 | spine1 | 1A:35:06:FF:00:00 | 3 hours ago | now | ethernet-1/11 |
| ethernet-1/52 | 1A:CA:07:FF:00:00 | spine2 | 1A:CA:07:FF:00:00 | 3 hours ago | now | ethernet-1/11 |
| mgmt0 | 1A:0F:04:FF:00:00 | leaf3 | 1A:0F:04:FF:00:00 | 3 hours ago | now | mgmt0 |
| mgmt0 | 1A:35:06:FF:00:00 | spine1 | 1A:35:06:FF:00:00 | 3 hours ago | now | mgmt0 |
| mgmt0 | 1A:95:03:FF:00:00 | leaf2 | 1A:95:03:FF:00:00 | 3 hours ago | now | mgmt0 |
| mgmt0 | 1A:B4:05:FF:00:00 | leaf4 | 1A:B4:05:FF:00:00 | 3 hours ago | now | mgmt0 |
| mgmt0 | 1A:CA:07:FF:00:00 | spine2 | 1A:CA:07:FF:00:00 | 3 hours ago | now | mgmt0 |
+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+
All examples provided in this document can be found in repository with SR Linux JSON RPC library samples.