Site icon Karneliuk

From Python to Go 018. Interaction With Network Devices Using GNMI.

Hello my friend,

Within past blog posts we covered how to interact with network devices (in fact, many servers support that as well) using SSH/CLI and NETCONF/YANG. Those two protocols allow you to confidently cover almost all cases for managing devices, whether you prefer more human-like approach, that is templating CLI commands and pushing them via SSH or using structured XML data and send it via NETCONF. However, there are more protocols and today we are going to talk about the most modern to the date, which is called GNMI (generalized network management interface).

How Does Automation Help Real Business?

Talking to students at our trainings and with customers and peers at various events, there is often a concern arise that small and medium businesses don’t have time and/or need to invest in automation. There is no time as engineers are busy solving “real” problems, like outages, customer experience degradation, etc. There is no need because why to bother, we have engineers to tackle issue. From my experience automation allows to save so much time on doing operational tasks and to free it up for improving user experience and developing new problems, that is difficult to overestimate. It really is that important and the sooner you start using it, the better it will be for your business

And our trainings are designed to teach you how to solve real world problems with automation. Enroll today:

We offer the following training programs in network automation for you:

During these trainings you will learn the following topics:

Moreover, we put all mentions technologies in the context of real use cases, which our team has solved and are solving in various projects in the service providers, enterprise and data center networks and systems across the Europe and USA. That gives you opportunity to ask questions to understand the solutions in-depth and have discussions about your own projects. And on top of that, each technology is provided with online demos and labs to master your skills thoroughly. Such a mixture creates a unique learning environment, which all students value so much. Join us and unleash your potential.

Start your automation training today.

What Are We Going To Talk Today?

GNMI is the newest management protocol, which is used in networking world. It was developed originally by Google to manage their network infrastructure. In today’s blog post we’ll answer the following questions:

  1. How GNMI is different to other management protocols?
  2. In which use cases you shall consider using GNMI?
  3. How to configure network devices using GNMI?

Explanation

GNMI has an interesting story. All the management protocols, whether it is SSH, NETCONF or any other (e.g., RESTCONF, which we don’t include in our blog series due to limited real-world implementations) are IETF standards and have associated RFCs. GNMI doesn’t have RFC covering its operation; yet its adoption across network vendors is very high. Cisco IOS-XE / IOS XR / NX-OS, Arista EOS, Juniper JUNOS, Nokia SR OS and many more supports GNMI. There are multiple reasons why this happened; what matters though, you can use it configure network devices, collect operational data, and many more.

In our network automation trainings, we cover reasons behind GNMI popularity as well as its operation in-depth.

From the perspective of configuration or data retrieval, GNMI relies on YANG modules, the very same YANG modules we’ve introduced in the previous blogpost. However, it doesn’t use XML serialisation. Instead, it uses Protobuf, which is another important Google’s invention. However, the real killer feature of GNMI is support of streaming telemetry, the mechanism which revolutionized the world of collecting operational data from network and infrastructure devices.

Join our Zero-to-Hero Network Automation Training to learn how us streaming telemetry in a programmable way.

Let’s briefly compare GNMI to NETCONF to get some sense of it:

NETCONF RPCGNMI RPCDescription
get-configGetCollect configuration
getGetCollect operational data
edit-configSetPerform configuration
commitApply configuration, i.e. merger candidate and running data store. In GNMI config is pushed straight to running typically.
SubscribeCreate a telemetry subscription, where the device starts sending data to telemetry collector at per-defined intervals or instantly

RPC stands for remote procedure call

If you can see our video overview of GNMI from UKNOF conference.

Since Protobuf is not really human-readable, we’ll show you how to use GNMI in code.

Example

Same as in the previous blog, we are going to use the very same logic of execution production grade change:

  1. Collection of the operational state of the network device before the change.
  2. Execution of the change.
  3. Collection of the operational state of the network device after the change.
  4. Comparing the difference of the operational state before and after the change.

Python

Let’s start with Python. You need to install the following two libraries:


1
$ pip install pyyaml pygnmi

By the way, pygnmi library was written by myself to address the lack of solid Python library for managing network devices with GNMI. It is great to see its wide adoption across the industry.

And now the code of Python application to manage network device with GNMI:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""From Python to Go: Python: 017 - NETCONF."""

# Modules
import argparse
import datetime
import os
import sys
from dataclasses import dataclass
from typing import List
import difflib
import pygnmi.client
import yaml
import pygnmi
# from scrapli_netconf.driver import NetconfDriver
# import xmltodict


# Classes
@dataclass
class Credentials:
    """Class to store credentials."""
    username: str
    password: str


@dataclass
class Instruction:
    """Class to store instructions."""
    command: List[str]
    config: List[tuple]


@dataclass
class Result:
    """Class to store command execution data."""
    instruction: Instruction
    diff: str
    timestamp: datetime.datetime


class Device:
    """Class to interact with netowrk device."""
    def __init__(self, hostname: str, ip_address: str, port: int, platform: str, credentials: Credentials):
        self.hostname = hostname
        self.ip_address = ip_address
        self.port = port
        self.platform = platform
        self.credentials = credentials

        self.results: List[Result] = []

    def execute_change(self, instruction: Instruction) -> None:
        """Method to execute change."""

        # Connect to device
        with pygnmi.client.gNMIclient(
            target=(self.ip_address, self.port),
            username=self.credentials.username,
            password=self.credentials.password,
            skip_verify=True,
            timeout=5,
        ) as gconn:
            # Get state before change
            before = gconn.get(path=instruction.command, datatype="config")
            before_stringified = self.dict_to_xpath(before)

            # Apply change
            config_result = gconn.set(update=instruction.config, encoding="json_ietf")
            print(f"{config_result=}")

            # Get state after change
            after = gconn.get(path=instruction.command, datatype="config")
            after_stringified = self.dict_to_xpath(after)

            # Diff
            diff = "\n".join(
                difflib.context_diff(
                    before_stringified,
                    after_stringified,
                    lineterm="",
                )
            )

            self.results.append(
                Result(
                    instruction=instruction,
                    diff=diff,
                    timestamp=datetime.datetime.now(),
                )
            )

    def dict_to_xpath(self, data: dict) -> list:
        """Method to convert dict to xpath."""
        result = []

        if isinstance(data, str):
            return data

        for key, value in data.items():
            if isinstance(value, list):
                for ind, item in enumerate(value):
                    tr = self.dict_to_xpath(item)
                    result.extend([f"{key}/{ind}/{_}" for _ in tr])

            elif isinstance(value, dict):
                tr = self.dict_to_xpath(value)
                result.extend([f"{key}/{_}" for _ in tr])

            else:
                result.append(f"{key} = {value}")

        return result


# Functions
def read_args() -> argparse.Namespace:
    """Helper function to read CLI arguments."""
    parser = argparse.ArgumentParser(description="User input.")
    parser.add_argument("-i", "--inventory", type=str, help="Path to inventory file.")
    return parser.parse_args()


def load_inventory(filename: str, credentials: Credentials) -> List[Device]:
    """Function to load inventory data."""
    # Open file
    try:
        with open(filename, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

    except FileNotFoundError as e:
        print(e)
        sys.exit(1)

    # Populate list of devices
    result = []
    for device in data:
        result.append(Device(credentials=credentials, **device))

    return result


def get_credentials() -> Credentials:
    """Function to get credentials."""
    username = os.getenv("AUTOMATION_USER")
    password = os.getenv("AUTOMATION_PASS")
    return Credentials(username, password)


# Main code
if __name__ == "__main__":
    # Read CLI arguments
    args = read_args()

    # Get credentials
    credentials = get_credentials()

    # Load inventory
    devices = load_inventory(args.inventory, credentials=credentials)

    # Config
    instruction = Instruction(
        command=["/openconfig-interfaces:interfaces"],
        config=[
            (
                "/openconfig-interfaces:interfaces",
                {
                    "interface": [
                        {
                            "name": "Loopback 23",
                            "config": {
                                "name": "Loopback 23",
                                "description": "Test-gnmi-python-2",
                            }
                        },
                    ],
                },
            ),
        ],
    )

    # Execute command
    for device in devices:
        device.execute_change(instruction)

    # Print results
    for device in devices:
        print(f"Device: {device.hostname}")
        for result in device.results:
            print(f"Config: {result.instruction.config}", f"Impact: {result.diff}", f"Timestamp: {result.timestamp}", sep="\n")

It is strongly recommended to read previous blogs as concepts explained before aren’t repeated.

Let’s break what’s new and how GNMI is used:

  1. Import “pygnmi.client” to get access to GNMI client.
  2. Data class “Instruction” is amended so that its field “command” became list of strings instead of dictionary and the field “config” became list of tuples instead of dictionary. These changes are made to map the pygnmi data structures.
  3. Differences are made within method “execute_change()” from “Deivce” class:
    • Instance of class “pygnmi.client.gNMIclient” is created, which handles gNMI connection with the device
    • Using “get()” method of this class the operational data is retrieved. As an argument you pass here list of GNMI Path to fetch data from in a format of simple strings.
    • Using “set()” method of this class the configuration is pushed to network device. There are three options how you can do it:
      • update” will create new one or modify existing one by overriding provided key/value pairs.
      • replace” will completely replace the content of the existing key/container and all its further nested elements.
      • delete” will delete the key/container including all nested elements.
    • Using “dict_to_xpath()” the nested dictionaries are converted to list of key/value pairs, which is then compared.
  4. Method “dict_to_xpath()” created in the previous blog to convert the nested keys in XPath was proven to be extremely useful and we continue using it here as well to convert nested GNMI data to XPath.
  5. The input object “instruction” is modified to match changes in “Instruction” class as outlined above.

As you can see, we used the absolutely identical logic to change and code structure for SSH, NETCONF and now for GNMI. What slightly changes is an input data. In real production application you would typically have some normalization layer, which will handle these differences.

The result of execution is the following:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
$ python main.py -i ../data/inventory.yaml
ssl_target_name_override is applied, should be used for testing only!
config_result={'timestamp': 1743270193131674380, 'prefix': None, 'response': [{'path': 'interfaces', 'op': 'UPDATE'}]}
Device: go-blog-arista
Config: [('/openconfig-interfaces:interfaces', {'interface': [{'name': 'Loopback 23', 'config': {'name': 'Loopback 23', 'description': 'Test-gnmi-python-2'}}]})]
Impact: ***
---
***************
*** 97,110 ****
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-fcs-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-octets = 49384793
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-unicast-pkts = 649097
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-broadcast-pkts = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-discards = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-octets = 18386266
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-unicast-pkts = 63350
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/openconfig-platform-port:hardware-port = Port97
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/ifindex = 999001
--- 97,110 ----
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-fcs-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-octets = 49390958
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/in-unicast-pkts = 649163
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-broadcast-pkts = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-discards = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-octets = 18434988
! notification/0/update/0/val/openconfig-interfaces:interface/0/state/counters/out-unicast-pkts = 63463
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/openconfig-platform-port:hardware-port = Port97
  notification/0/update/0/val/openconfig-interfaces:interface/0/state/ifindex = 999001
***************
*** 742,748 ****
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/openconfig-if-ip:ipv6/state/mtu = 1500
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/state/index = 0
! notification/0/update/0/val/openconfig-interfaces:interface/3/config/description = Test-gnmi-python
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/arista-intf-augments:load-interval = 300
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/loopback-mode = True
--- 742,748 ----
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/openconfig-if-ip:ipv6/state/mtu = 1500
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/2/subinterfaces/subinterface/0/state/index = 0
! notification/0/update/0/val/openconfig-interfaces:interface/3/config/description = Test-gnmi-python-2
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/arista-intf-augments:load-interval = 300
  notification/0/update/0/val/openconfig-interfaces:interface/3/config/loopback-mode = True
***************
*** 757,763 ****
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/name = Loopback23
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/openconfig-vlan:tpid = openconfig-vlan-types:TPID_0X8100
! notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/description = Test-gnmi-python
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/index = 0
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/index = 0
--- 757,763 ----
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/name = Loopback23
  notification/0/update/0/val/openconfig-interfaces:interface/3/state/openconfig-vlan:tpid = openconfig-vlan-types:TPID_0X8100
! notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/description = Test-gnmi-python-2
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/config/index = 0
  notification/0/update/0/val/openconfig-interfaces:interface/3/subinterfaces/subinterface/0/index = 0
Timestamp: 2025-03-29 17:43:15.708137

The content of line, which are different starts with the exclamation mark. So you see that name of interface is changed following execution of our change.

Now onto Golang.

Go (Golang)

For Golang we use the following external packages:


1
2
3
4
$ go get github.com/google/go-cmp/cmp
$ go get gopkg.in/yaml.v3
$ go get github.com/openconfig/gnmic/api
$ go get google.golang.org/protobuf/encoding/prototext

Finally I had an opportunity to work with gnmic library created by Nokia’s Roman Dodin and Karim Radhouani.

Using gnmic, the following Go (Golang) application was developed to manage network devices using GNMI:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/* From Python to Go: Go: 018 - GNMI. */

package main

// Imports
import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/google/go-cmp/cmp"
    "github.com/openconfig/gnmic/pkg/api"
    "google.golang.org/protobuf/encoding/prototext"
    "gopkg.in/yaml.v3"
)

// Types and Receivers
type Arguments struct {
    /* Class to starte CLI arguments */
    Inventory string
}

type Crendetials struct {
    /* Struct to store credentials. */
    Username string
    Password string
}

type Instruction struct {
    Command string
    Config  struct {
        Path  string
        Value any
    }
}

type Result struct {
    /* Struct to store command execution result. */
    Instruction Instruction
    Diff        string
    Timestamp   time.Time
}

type Device struct {
    /* Struct to interact with netowrk device. */
    Hostname    string `yaml:"hostname"`
    IpAddress   string `yaml:"ip_address"`
    Port        uint   `yaml:"port"`
    Platform    string `yaml:"platform"`
    Crendetials Crendetials
    Result      []Result
}

func (d *Device) executeChange(i Instruction) {
    /* Method to execute command */

    // Create GNMI Target
    gnmiTarget, err := api.NewTarget(
        api.Name(d.Hostname),
        api.Address(fmt.Sprintf("%s:%d", d.IpAddress, d.Port)),
        api.Username(d.Crendetials.Username),
        api.Password(d.Crendetials.Password),
        api.SkipVerify(true),
    )
    if err != nil {
        log.Fatal("Cannot create GNMI Target: ", err)
    }

    // Create context
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Create GNMI client
    err = gnmiTarget.CreateGNMIClient(ctx)
    if err != nil {
        log.Fatal("Cannot create GNMI Client: ", err)
    }
    defer gnmiTarget.Close()

    // Get state before change
    getReq, err := api.NewGetRequest(
        api.Path(i.Command),
        api.DataType("config"),
        api.Encoding("json_ietf"),
    )
    if err != nil {
        log.Fatal("Cannot create Get request: ", err)
    }
    beforeGetResponse, err := gnmiTarget.Get(ctx, getReq)
    if err != nil {
        log.Fatal("Cannot make a Get request: ", err)
    }
    beforeStruct := OpenConfigInterfaces{}
    err = json.Unmarshal(beforeGetResponse.Notification[0].Update[0].Val.GetJsonIetfVal(), &beforeStruct)
    if err != nil {
        log.Fatal("Cannot unmarshall JSON: ", err)
    }

    // Make change
    setReq, err := api.NewSetRequest(
        api.Update(
            api.Path(i.Config.Path),
            api.Value(i.Config.Value, "json_ietf"),
        ),
    )
    if err != nil {
        log.Fatal("Cannot create Set request: ", err)
    }
    setResp, err := gnmiTarget.Set(ctx, setReq)
    if err != nil {
        log.Fatal("Cannot make a Set request: ", err)
    }
    log.Println(prototext.Format(setResp))

    // Get state after change
    afterGetResponse, err := gnmiTarget.Get(ctx, getReq)
    if err != nil {
        log.Fatal("Cannot make a Get request: ", err)
    }
    afterStruct := OpenConfigInterfaces{}
    err = json.Unmarshal(afterGetResponse.Notification[0].Update[0].Val.GetJsonIetfVal(), &afterStruct)
    if err != nil {
        log.Fatal("Cannot unmarshall JSON: ", err)
    }
    // Diff
    diff := cmp.Diff(beforeStruct, afterStruct)

    // Update the result
    (*d).Result = append((*d).Result, Result{
        Instruction: i,
        Diff:        diff,
        Timestamp:   time.Now(),
    })
}

// Functions
func readArgs() Arguments {
    /* Helper function to read CLI arguments */
    result := Arguments{}

    flag.StringVar(&result.Inventory, "i", "", "Path to the inventory file")

    flag.Parse()

    return result
}

func loadInventory(p string) *[]Device {
    /* Function to load inventory data. */

    // Open file
    bs, err := os.ReadFile(p)
    if err != nil {
        fmt.Println("Get error ", err)
        os.Exit(1)
    }

    // Load inventory
    result := &[]Device{}

    err = yaml.Unmarshal(bs, result)
    if err != nil {
        fmt.Println("Get error ", err)
        os.Exit(1)
    }

    // Return result
    return result
}

func getCredentials() Crendetials {
    /* Function to get credentials. */
    return Crendetials{
        Username: os.Getenv("AUTOMATION_USER"),
        Password: os.Getenv("AUTOMATION_PASS"),
    }
}

// Main
func main() {
    /* Core logic */
    // Read CLI arguments
    cliArgs := readArgs()

    // Get credentials
    sshCreds := getCredentials()

    // Load inventory
    inventory := loadInventory(cliArgs.Inventory)

    // Config
    instruction := Instruction{
        Command: "/openconfig-interfaces:interfaces",
        Config: struct {
            Path  string
            Value any
        }{
            Path: "/openconfig-interfaces:interfaces",
            Value: map[string]any{
                "interface": []map[string]any{
                    {
                        "name": "Loopback 23",
                        "config": map[string]any{
                            "name":        "Loopback 23",
                            "description": "Test-gnmi-golang-3",
                        },
                    },
                },
            },
        },
    }

    // Execute commands
    for i := 0; i < len(*inventory); i++ {
        (*inventory)[i].Crendetials = sshCreds
        (*inventory)[i].executeChange(instruction)
    }

    // Print results
    for i := 0; i < len(*inventory); i++ {
        for j := 0; j < len((*inventory)[i].Result); j++ {
            fmt.Printf(
                "Config: %v\nImpact: %v\nTimestamp: %v\n",
                (*inventory)[i].Result[j].Instruction.Config,
                (*inventory)[i].Result[j].Diff,
                (*inventory)[i].Result[j].Timestamp,
            )
        }
    }
}

And structs for OpenConfig, which we re-use from previous blog with some minor modifications:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* From Python to Go: Go: 018 - GNMI: modules */

package main

// Import

// Data types
type OpenConfigInterface struct {
    Name   string `xml:"name,omitempty"`
    Config struct {
        Name        string `xml:"name,omitempty" json:"name,omitempty"`
        Description string `xml:"description,omitempty" json:"description,omitempty"`
        Enabled     bool   `xml:"enabled,omitempty" json:"enabled,omitempty"`
    } `xml:"config" json:"config"`
}
type OpenConfigInterfaces struct {
    Interface []OpenConfigInterface `xml:"openconfig-interfaces:interface,omitempty" json:"openconfig-interfaces:interface,omitempty"`
}

It is strongly recommended to read previous blogs as concepts explained before aren’t repeated.

Here what happens:

  1. In structs we add mapping to JSON keys. The reason we do this is because many vendors choose to send serialized JSON as a string within Protobuf message. Using JSON allows us to parse it and to deal with structured data.
  2. Fields “Command” and “Config” of type “Instruction” are modified to be list of strings and list of structs respectively, matching the pattern we had with pygnmi before.
  3. Also like in Python part, the major changes happened within receiver function “executeChange()“:
    • Using function “NewTarget()” from gnmic library the gnmi target is created. Target here is an abstract definition of the device, where the connectivity will be established to.
    • Using receiver function “CreateGNMIClient()” of Target struct, connectivity is established.
    • Using function “NewGetRequest()” and “NewSetRequest()” the Protobus messages are generated.
    • Using receiver functions “Get()” and “Set()” of Target these messages are sent are responses receveid.
    • Responses are parsed from JSON strings embedded in Protobuf messages into structs using parsing methodology from JSON to struct we covered before.
  4. The struct “instruction” created out ouf “Instruction” type is populated with details needed to do configuration.

Now let’s execute this Go (Golang) code to perform change on network device:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ go run . -i ../data/inventory.yaml
2025/03/29 20:48:28 response:  {
  path:  {
    elem:  {
      name:  "openconfig-interfaces:interfaces"
    }
  }
  op:  UPDATE
}
timestamp:  1743281306005644545

Config: {/openconfig-interfaces:interfaces map[interface:[map[config:map[description:Test-gnmi-golang-3 name:Loopback 23] name:Loopback 23]]]}
Impact:   main.OpenConfigInterfaces{
        Interface: []main.OpenConfigInterface{
                {Name: "Management1", Config: {Name: "Management1", Enabled: true}},
                {Name: "Ethernet2", Config: {Name: "Ethernet2", Enabled: true}},
                {Name: "Ethernet1", Config: {Name: "Ethernet1", Enabled: true}},
                {
                        Name: "Loopback23",
                        Config: struct{ Name string "xml:"name,omitempty" json:"name,omitempty""; Description string "xml:"description,omitempty" json:"description,omitempty""; Enabled bool "xml:"enabled,omitempty" json:"enabled,omitempty"" }{
                                Name:        "Loopback23",
-                               Description: "Test-gnmi-golang-2",
+                               Description: "Test-gnmi-golang-3",
                                Enabled:     true,
                        },
                },
                {Name: "Loopback0", Config: {Name: "Loopback0", Enabled: true}},
                {Name: "Loopback51", Config: {Name: "Loopback51", Description: "pytest-update-test-33"}},
        },
  }

Timestamp: 2025-03-29 20:48:28.7119283 +0000 GMT m=+0.725116820

You can see the similar logic as we had in the previous blog, where comparing of two structs using third-party libraries provides very preciese difference and we can assess if our change reached the goal.

Lessons in GitHub

You can find the final working versions of the files from this blog at out GitHub page.

Conclusion

GNMI is no doubts a new and powerful protocol. However, there are still talks though in industry, where it will have wide production usage outside of Google network. Time will answer this question. From our perspective, though, it is important to know how you can use it both in Python and Go (Golang). Take care and good bye.

Support us






P.S.

If you have further questions or you need help with your networks, I’m happy to assist you, just send me message. Also don’t forget to share the article on your social media, if you like it.

BR,

Anton Karneliuk 

Exit mobile version