Site icon Karneliuk

From Python to Go 019. Interaction With Applications Via REST API.

Hello my friend,

So far we’ve covered all means to interact with network devices, which are meaningful in our opinion: SSH, NETCONF/YANG, and GNMI/YANG. There is one more protocol, which exists for managing network devices, which is called RESTCONF, which is application of REST API to network devices. From our experience, its support across network vendors is very limited; therefore, we don’t cover it. However, REST API itself is immensely important, as it is still the most widely used protocol for applications to talk to each other. And this is the focus for today’s blog.

I See Everywhere Stop Learning Code, Why Do You Teach It?

Generative AI, Agentic AI, all other kinds of AI is absolutely useful things. The advancements there are very quick and we ourselves using them in our projects. At the same time, if you don’t know how to code, how to solve algorithmic tasks, how can you reason if the solution provided by AI is correct? If that optimal? And moreover, when it breaks, because every software breaks sooner or later, how can you fix it? That’s why we believe it is absolutely important to learn software development, tools and algorithms. Perhaps, more than ever as various software are used more and more around the world

And our trainings are designed to teach you foundation of software development of network and IT infrastructure management. 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?

REST API is de-facto standard for application talking to each other since beginning of 2010s, when it was introduced and started replacing SOAP/XML. A lot of existing applications nowadays have REST API to allow users programmatically to interact with them, including tools in IT and network infrastructure management. That’s why we are talking about the REST API today, focusing on:

  1. How REST API operates?
  2. Which popular tools in IT management has REST API?
  3. How to interact with external applications via REST API in your applications?

Explanation

REST API is the most popular way for application to talk to each other. It walked long a way from its first inception back in 2000.

We covered REST API in depth earlier in our blog: Part 1, Part 2, and Part 3.

Let’s do a quick refresher, on how REST API works.

First of all, REST is a short of REpresentational State Transfer API. In real world it is based on HTTP transport, and utilizes various HTTP methods to perform actions tailored to data lifecycle (CRUD):

Logical operationHTTP MethodMeaning
CreatePOSTCreating new data entry
ReadGETRetrieve existing data entry/entries
UpdatePOST, PUTModify existing data entry/entries
DeleteDELETEDelete existing data entry

Authentication in REST API is done using HTTP headers; in fact, HTTP headers for much more: they are used to signal various metadata, such as provided/expected content types, encoding, etc.

Typically REST API uses JSON as data serialization, although I saw some applications using XML. Usage of JSON, which is easily human-readable, and clean stateless API structure made REST API easy to implement and, therefore, very popular nowadays.

It is not ideal, though. There are a number of pitfalls, which some other API techniques tried to address:

Despite these drawbacks, REST API is still very much used. I saw an interesting insight in LinkedIn, that some companies even going back from GRPC to REST API for some services.

Practical Applications

NetBox

One of the best tools in the world in my opinion for network and IT network infrastructure management is NetBox.

If you want to master NetBox, join our Zero-to-Hero Network Automation Training.

It has a great a UI and REST API allowing you to manage all data points about the IT/network infrastructure.

We have quite a few blogs about NetBox, which we encourage you to read if you aren’t familiar with NetBox.

Other Applications

Service Now, JIRA, Infoblox, Slack, Microsoft Team, Ansible Tower (Ansible Automation Controller), even Google Sheets – all these applications have REST API, and this is just a tip of the iceberg. Therefore, if you are in software development, you absolutely should know how to programmatically interact with REST API and how to create REST API services yourself.

Example

We’ll continue our logic of build on top of what we’ve build so far in previous blogs. To show you how to use REST API, we will:

  1. Replace local static inventory YAML file with integration to public demo NetBox website.
  2. Use this inventory to connect to network device using GNMI.

Python

To interact with REST API services, we need libraries, which are able to perform HTTP requests. There are some libraries, such as urllib, which are included in standard Python distribution. However, they are outdated. The most advanced, which is also gaining more popularity, is httpx. You need to install, in addition to pygnmi, which is used to interact with network devices.


1
$ pip install pygnmi httpx

And now the code of application in Python, which gets via REST API from NetBox inventory and then connects to those 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
"""From Python to Go: Python: 017 - NETCONF."""

# Modules
import argparse
import datetime
import os
import sys
from dataclasses import dataclass
from typing import List, Tuple
import difflib
import pygnmi.client
import yaml
import httpx


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


@dataclass
class InventoryCredentials:
    """Class to store credentials."""
    url: str
    token: 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 load_inventory(inventory: InventoryCredentials, credentials: Credentials) -> List[Device]:
    """Function to load inventory data."""
    # Create HTTP client and set headers
    hclient = httpx.Client(
        base_url=inventory.url.rstrip("/"),
        headers={"Authorization": f"Token {inventory.token}"},
    )
    # Retrieve data from REST API
    try:
        response = hclient.get(
            "/api/dcim/devices/",
            params={
                "site": "kblog",
            }
        )
        response.raise_for_status()
        data = response.json()

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

    # Populate list of devices
    result = []
    for device in data["results"]:
        result.append(
            Device(
                hostname=device["name"],
                ip_address=device["primary_ip"]["address"].split("/")[0],
                port=device["custom_fields"].get("gnmi_port", 50051),
                platform=device["platform"]["slug"],
                credentials=credentials,
            )
        )

    return result


def get_credentials() -> Tuple[Credentials, InventoryCredentials]:
    """Function to get credentials."""
    return (
        Credentials(
            os.getenv("AUTOMATION_USER"),
            os.getenv("AUTOMATION_PASS"),
        ),
        InventoryCredentials(
            os.getenv("AUTOMATION_INVENTORY_URL"),
            os.getenv("AUTOMATION_INVENTORY_TOKEN"),
        ),
    )


# Main code
if __name__ == "__main__":
    # Get credentials
    credentials, inventory_credentials = get_credentials()

    # Load inventory
    devices = load_inventory(inventory_credentials, 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")

Read previous blogs to get better understanding of this code.

What’s changed since the previous version:

  1. New data class InventoryCredentials is introduced to store URL and token of NetBox inventory.
  2. Function get_credentials() now returns two credentials: in addition to devices’ credentials it returns also inventory for NetBox, which it reads from environment variable.
  3. Function load_inventory() underwent the major rework:
    • Using class Client from httpx library the object is instantiated to talk to NetBox. As arguments to this class both the NetBox base URL as well “Authorization” header with Token are passed.
    • Using get() method, inventory data is retrieved from NetBox via REST API. Arguments this method is specific URL, which contains all the devices, and the arguments, which allows to filter specific subset of data.
    • Retrieved data is converted from a string in JSON format to a dictionary using json() method of the received response.
    • Then the result is populated , which uses unaltered Device data class, which we use for all past examples.

That’s all the changes. As you can see, the modularity we created in code, allows us to replace content of one of the functions with other actions so long we maintains the result structure, which is used as an input to other functions.

Let’s execute the code:


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
ssl_target_name_override is applied, should be used for testing only!
config_result={'timestamp': 1744970788637880096, 'prefix': None, 'response': [{'path': 'interfaces', 'op': 'UPDATE'}]}
Device: ka-blog-dev-001
Config: [('/openconfig-interfaces:interfaces', {'interface': [{'name': 'Loopback 23', 'config': {'name': 'Loopback 23', 'description': 'Test-gnmi-python-2'}}]})]
Impact: ***
---
***************
*** 687,700 ****
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-fcs-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-octets = 43751
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-unicast-pkts = 463
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-broadcast-pkts = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-discards = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-octets = 20010
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-unicast-pkts = 144
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/openconfig-platform-port:hardware-port = Port97
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/ifindex = 999001
--- 687,700 ----
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-fcs-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-octets = 47298
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/in-unicast-pkts = 493
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-broadcast-pkts = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-discards = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-errors = 0
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-multicast-pkts = 0
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-octets = 50683
! notification/0/update/0/val/openconfig-interfaces:interface/2/state/counters/out-unicast-pkts = 176
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/openconfig-platform-port:hardware-port = Port97
  notification/0/update/0/val/openconfig-interfaces:interface/2/state/ifindex = 999001
***************
*** 818,824 ****
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/openconfig-if-ip:ipv6/state/mtu = 1500
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/state/index = 0
! notification/0/update/0/val/openconfig-interfaces:interface/5/config/description = Go_Test_2
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/arista-intf-augments:load-interval = 300
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/loopback-mode = True
--- 818,824 ----
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/openconfig-if-ip:ipv6/state/mtu = 1500
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/4/subinterfaces/subinterface/0/state/index = 0
! notification/0/update/0/val/openconfig-interfaces:interface/5/config/description = Test-gnmi-python-2
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/arista-intf-augments:load-interval = 300
  notification/0/update/0/val/openconfig-interfaces:interface/5/config/loopback-mode = True
***************
*** 833,839 ****
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/name = Loopback23
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/openconfig-vlan:tpid = openconfig-vlan-types:TPID_0X8100
! notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/description = Go_Test_2
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/index = 0
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/index = 0
--- 833,839 ----
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/name = Loopback23
  notification/0/update/0/val/openconfig-interfaces:interface/5/state/openconfig-vlan:tpid = openconfig-vlan-types:TPID_0X8100
! notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/description = Test-gnmi-python-2
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/enabled = True
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/config/index = 0
  notification/0/update/0/val/openconfig-interfaces:interface/5/subinterfaces/subinterface/0/index = 0
Timestamp: 2025-04-18 11:06:30.556257

Go (Golang)

In contrast to Python, Golang has a first-class great library for HTTP requests in net/http package. As such, we need only to install libraries for GNMI and to compare objects:


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

Next contrast to Python, as Golang is strict typed language (we omit reflection use cases), we need to create struct for NetBox response so that we can use it later:


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
/* From Python to Go: Go: 019 - REST API and 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"`
}

type NetboxDcimDevices struct {
    /* Struct to store data from NetBox */
    Count   uint64 `json:"count"`
    Results []struct {
        Name       string `json:"name"`
        PrimaryIp4 struct {
            Address string `json:"address"`
        } `json:"primary_ip4"`
        Platform struct {
            Slug string `json:"slug"`
        } `json:"platform"`
        CustomFields struct {
            GnmiPort uint64 `json:"gnmi_port"`
        } `json:"custom_fields"`
    } `json:"results"`
}

The new type NetboxDcimDevices of struct is created, which parses a few fields in the response. We don’t parse the entire message but only what is really needed for our inventory use case.

More on JSON parsing in Golang.

And not the code of main application written in Go (Golang):


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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/* From Python to Go: Go: 019 - REST API and GNMI. */

package main

// Imports
import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

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

// 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 InventoryCredentials struct {
    /* Struct to store inventory credentails */
    Url   string
    Token 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 loadInventory(iC InventoryCredentials) *[]Device {
    /* Function to load inventory data. */

    // Create HTTP client
    hclient := &http.Client{}

    // Prepare request
    NetboxRequest, err := http.NewRequest("GET", iC.Url+"/api/dcim/devices/", nil)
    if err != nil {
        fmt.Println("Error during preparing HTTP Request ", err)
        os.Exit(1)
    }

    // Set headers
    NetboxRequest.Header.Add("Authorization", fmt.Sprintf("Token %s", iC.Token))

    // Set URL params
    q := NetboxRequest.URL.Query()
    q.Add("site", "kblog")
    NetboxRequest.URL.RawQuery = q.Encode()

    // Get data
    resp, err := hclient.Do(NetboxRequest)
    if err != nil {
        fmt.Println("Erorr during executing HTTP query ", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error during reading body of HTTP response ", err)
        os.Exit(1)
    }

    td := NetboxDcimDevices{}
    err = json.Unmarshal(body, &td)
    if err != nil {
        fmt.Println("Error during parsing JSON ", err)
        os.Exit(1)
    }

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

    for _, v := range td.Results {
        *result = append(*result, Device{
            Hostname:  v.Name,
            Platform:  v.Platform.Slug,
            IpAddress: strings.Split(v.PrimaryIp4.Address, "/")[0],
            Port:      uint(v.CustomFields.GnmiPort),
        })
    }

    // Return result
    return result
}

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

// Main
func main() {
    /* Core logic */
    // Get credentials
    sshCreds, invCreds := getCredentials()

    // Load inventory
    inventory := loadInventory(invCreds)

    // 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,
            )
        }
    }
}

We suggest to read previous blogs to get better understanding of this code, as we omit many details in this post.

Changes:

  1. New custom data type of struct type is created, which is named InventoryCredentials and is used to store URL and Token to connect to NetBox.
  2. In the same manner as we did in Python, we modify function getCredentials() to read from environment variables with NetBox connectivity data.
  3. Function loadInventory() is reworked to fetch inventory data from NetBox using REST API:
    • Pointer to struct Client from http package is created.
    • Using function NewRequest(), which takes URL and Method name, the REST API request is prepared.
    • To this request the authorization header is added using Add() Receiver function
    • Also the query parameters are added to URL.
    • Using Do receiver function of Client, the request is made.
    • Content of the body is read as a slice of bytes
    • Using Unmarshall function from encoding/json package, the content is parsed into struct of NetboxDcimDevices type.
    • Finally, the result, which is a pointer to slice of Device is populated.

Executing the code:


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
$ go run .
2025/04/18 13:57:59 response: {
  path: {
    elem: {
      name: "openconfig-interfaces:interfaces"
    }
  }
  op: UPDATE
}
timestamp: 1744981077614602933

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{
                ... // 3 identical elements
                {Name: "Loopback51", Config: {Name: "Loopback51", Description: "pytest-update-test-33"}},
                {Name: "Loopback0", Config: {Name: "Loopback0", 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-python-2",
+                               Description: "Test-gnmi-golang-3",
                                Enabled:     true,
                        },
                },
        },
  }

Timestamp: 2025-04-18 13:57:59.50417202 +0100 BST m=+1.361305621

Lessons in GitHub

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

Conclusion

As you see, the interaction with REST API in Python and Golang are slightly different due to Golang being strict typed programming language and, thefore, requires more work on data processing. However, it comes also with benefit of clean data with right types. By now we’ve covered all main APIs to interact with network devices and other applications, which is part of network and IT infrastructure automation journey: SSH, NETCONF/YANG, GNMI/YANG, and now REST. There is one more topic we are to cover for us to complete this guide. 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