Site icon Karneliuk

From Python to Go 017. Interaction With Network Devices Using NETCONF.

Hello my friend,

We continue exploring programmable network management using Python and Go (Golang) as programming languages. In today’s blog post we’ll cover how to interact with network devices using NETCONF.

How To Chose Which API To Use?

There are many APIs (Application Programmable Interfaces) out there. We already covered SSH and now covering NETCONF. And there are a few more existing, which we are going to cover. Cannot we just stick to a single API for all use cases. The truth is that each API has its own advantages and disadvantages, as well as design patterns and areas, where it shall be used. As such, each of them is important and valuable.

And in our training programs we do deep-dive in all these APIs. Enrol 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?

NETCONF is a truly programmable way to manage network devices. NETCONF is a standard, covered in a number of RFCs; check 6241 and 6242 to start with. Being standard, which exists already for a while, it is wide spreaded with majority of vendors supporting it. I personally like it a lot and use it a lot across various projects. In today’s blog we are going to show you, how you can use NETCONF, namely:

  1. How to prepare XML messages and parse incoming ones?
  2. How to get configuration from network devices using NETCONF?
  3. How to configure network devices using NETCONF?

Explanation

NETCONF is a great protocol to manage network devices in a programmable way. It uses SSH transport, as we discussed previously. However, it uses XML serialisation, what makes it simpler to deal with from code perspective compared to CLI commands we sent the last time. It is simpler, because XML serialises structured data, which with minimal efforts can be converted in maps/dictionaries or data classes/structs. The model of structured data is defined using YANG modules.

Join our Zero-To-Hero Network Automation Training to master YANG.

Here is an example of NETCONF message:


1
2
3
4
5
6
7
8
9
10
11
12
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.0</capability>
    <capability>urn:ietf:params:netconf:base:1.1</capability>
    <capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
    <capability>urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring</capability>
    <capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
    <capability>urn:ietf:params:netconf:capability:url:1.0?scheme=file,flash,ftp,http</capability>
    <capability>http://openconfig.net/yang/network-instance-l3?module=openconfig-network-instance-l3&revision=2018-11-21</capability>
  </capabilities>
</hello>

This can fit in the following set of Python data classes:


1
2
3
4
5
6
7
8
9
10
from dataclasses import dataclass
from typing import List

@dataclass
class Capability:
    capability: List[str]

@dataclass
class Capabilities:
    capabilities: Capability

Or into the following Go (Golang) struct:


1
2
3
4
5
6
import "encoding/xml"

type Capabilities struct {
   XMLName     xml.Name   `xml:"capabilities"`
   Capability  []string   `xml:"capability"`
}

One question that you may have in your mind now, how do we convert XML data to structured data? Luckily, we already discussed that in our blog series: there is a great third party Python library, which does conversion into Python dictionary for you. Afterwards, you populate data class from dictionary.

In case of Go (Golang), we use built-in package “encoding/xml”, which does for us marshalling (conversion of structs to XML bytes) and unmarshalling (conversion of XML bytes to structs).

To create messages, apart from YANG modules, we also need to know basic NETCONF operations (officially they are called RPCs – remote procedure calls). We’ll call here a few, some of which we are to use in our example:

RPCDescription
getRetrieve operational states of network device. Think of it as “show xxx” CLI commands
get-configRetrieve configuration of network device. You can specify, if you want to fetch running, candidate or startup configuration
edit-configPush the configuration to network device to specified data store.
commitMerge configuration for candidate to running

Enrol into Zero-to-Hero Network Automation Training to learn more about NETCONF operations and how to use them.

“Talk is cheap. Show me the code”, as Linus Torvalds is used to day. Let’s take a look at the examples.

Example

We’ll re-use the scenario from the previous blog post, with replacing SSH/CLI by NETCONF/YANG:

As we are dealing with NETCONF, though, we will need to ensure we do conversion between dictionaries or structs to XML strings and vice versa.

For YANG modules, we will OpenConfig, which is a multi-vendor YANG model. It is supported by Arista EOS, Cisco IOS XR, Cisco NX-OS and many others.

Start Zero-to-Hero Network Automation Training to get up to speed with different YANG modules, and where they shall be used.

Python

We traditionally start with Python code, so we do today. First of all, you need to install the following dependencies:


1
$ pip install pyyaml scrapli scrapli-netconf xmltodict ssh2-python

If you are not familiar how to convert to/from XML, we recommend to read this blog first.

Now on to coding:


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
"""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 yaml
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: dict
    config: dict


@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, platform: str, credentials: Credentials):
        self.hostname = hostname
        self.ip_address = ip_address
        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 NetconfDriver(
            host=self.ip_address,
            port=830,
            auth_username=self.credentials.username,
            auth_password=self.credentials.password,
            auth_strict_key=False,
            transport="ssh2",
        ) as conn:
            filter_ = xmltodict.unparse(instruction.command).splitlines()[1]
            # Get state before change
            before = conn.get_config(source="running", filter_=filter_, filter_type="subtree")
            before_stringified = self.dict_to_xpath(xmltodict.parse(before.result))

            # Apply change
            config_ = "\n".join(xmltodict.unparse(instruction.config).splitlines()[1:])
            change_result = conn.edit_config(target="candidate", config=config_)

            if change_result.failed:
                print(f"Error: {change_result.result}")
            else:
                commit_result = conn.commit()
                if commit_result.failed:
                    print(f"Error: {commit_result.result}")

            # Get state after change
            after = conn.get_config(source="running", filter_=filter_, filter_type="subtree")
            after_stringified = self.dict_to_xpath(xmltodict.parse(after.result))

            # 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 = []
        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={"interfaces": {"@xmlns": "http://openconfig.net/yang/interfaces"}},
        config={
            "config": {
                "interfaces": {
                    "@xmlns": "http://openconfig.net/yang/interfaces",
                    "interface": [
                        {
                            "name": "Loopback 23",
                            "config": {
                                "name": "Loopback 23",
                                "description": "Test-netconf-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")

We strongly recommend to read previous blogs, if you aren’t sure about some parts of the code, as we will focus our explanation only on what is relevant to NETCONF.

Assuming you have read previous blogs, here is what is NETCONF-specific happening in this snippet:

  1. Instruction data class now uses dictionaries as data types, as we will use them to store configuration and operational filter, so that we can convert it to XML using xmltodict library.
  2. The method execute_change() of Device class is re-written to use NETCONF protocol leveraging scrapli-netconf library:
    • Class NetconfDriver() is used to establish NETCONF connectivity. We use transport “ssh2“, which is available for us after installing the aforementioned third party libraries.
    • Using unparse() function from xmltodict library, the dictionary is converted to a multi-line string with XML syntax. The string starts with XML declaration, which needs to be removed before it can be sent to network device.
    • Running configuration is collected from the remote device using get_config() method, which performs under the hood NETCONF get-config RPC. The result is parsed to dict using parse() function of xmltodict library and then processed by the new method (see below point 3).
    • Then configuration is pushed to network device, with message being converted from dict to XML using same method as explained above.
    • If result isn’t failed, then configuration is commit using commit() method of scrapli-netconf.
    • If there are no errors, the running configuration is collected again and processed.
    • Finally, the pre- and post- configurations are compared and difference is identified
  3. In addition, there is a new method dict_to_xpath(), which converts nested dictionaries and lists in a flat key/value structure, where key combines all the nested sub-keys using recursion. This allows us to simplify visualisation of changes made on the device.

As you can see, the logic is the same as we have in the previous blog post with SSH/CLI; we’ve added a few extra helper functions.

Let’s execute this script:


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
$ python main.py -i ../data/inventory.yaml
Device: go-blog-nexus
Config: {'config': {'interfaces': {'@xmlns': 'http://openconfig.net/yang/interfaces', 'interface': [{'name': 'Loopback 23', 'config': {'name': 'Loopback 23', 'description': 'Test-netconf-python-2'}}]}}}
Impact: ***
---
***************
*** 1,6 ****
  rpc-reply/@xmlns = urn:ietf:params:xml:ns:netconf:base:1.0
! rpc-reply/@message-id = 101
! rpc-reply/data/@time-modified = 2025-03-22T16:53:24.80910925Z
  rpc-reply/data/interfaces/@xmlns = http://openconfig.net/yang/interfaces
  rpc-reply/data/interfaces/interface/0/name = Management1
  rpc-reply/data/interfaces/interface/0/config/enabled = true
--- 1,6 ----
  rpc-reply/@xmlns = urn:ietf:params:xml:ns:netconf:base:1.0
! rpc-reply/@message-id = 104
! rpc-reply/data/@time-modified = 2025-03-22T16:53:25.701759874Z
  rpc-reply/data/interfaces/@xmlns = http://openconfig.net/yang/interfaces
  rpc-reply/data/interfaces/interface/0/name = Management1
  rpc-reply/data/interfaces/interface/0/config/enabled = true
***************
*** 134,140 ****
  rpc-reply/data/interfaces/interface/2/subinterfaces/subinterface/ipv6/config/enabled = false
  rpc-reply/data/interfaces/interface/2/subinterfaces/subinterface/ipv6/config/mtu = 1500
  rpc-reply/data/interfaces/interface/3/name = Loopback23
! rpc-reply/data/interfaces/interface/3/config/description = Test-netconf-golang-2
  rpc-reply/data/interfaces/interface/3/config/enabled = true
  rpc-reply/data/interfaces/interface/3/config/load-interval/@xmlns = http://arista.com/yang/openconfig/interfaces/augments
  rpc-reply/data/interfaces/interface/3/config/load-interval/#text = 300
--- 134,140 ----
  rpc-reply/data/interfaces/interface/2/subinterfaces/subinterface/ipv6/config/enabled = false
  rpc-reply/data/interfaces/interface/2/subinterfaces/subinterface/ipv6/config/mtu = 1500
  rpc-reply/data/interfaces/interface/3/name = Loopback23
! rpc-reply/data/interfaces/interface/3/config/description = Test-netconf-python-2
  rpc-reply/data/interfaces/interface/3/config/enabled = true
  rpc-reply/data/interfaces/interface/3/config/load-interval/@xmlns = http://arista.com/yang/openconfig/interfaces/augments
  rpc-reply/data/interfaces/interface/3/config/load-interval/#text = 300
***************
*** 148,154 ****
  rpc-reply/data/interfaces/interface/3/hold-time/config/down = 0
  rpc-reply/data/interfaces/interface/3/hold-time/config/up = 0
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/index = 0
! rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/description = Test-netconf-golang-2
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/enabled = true
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/index = 0
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/ipv4/@xmlns = http://openconfig.net/yang/interfaces/ip
--- 148,154 ----
  rpc-reply/data/interfaces/interface/3/hold-time/config/down = 0
  rpc-reply/data/interfaces/interface/3/hold-time/config/up = 0
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/index = 0
! rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/description = Test-netconf-python-2
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/enabled = true
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/config/index = 0
  rpc-reply/data/interfaces/interface/3/subinterfaces/subinterface/ipv4/@xmlns = http://openconfig.net/yang/interfaces/ip
Timestamp: 2025-03-22 16:53:26.975625

If we haven’t introduced new function for making nested keys flat, it will be very difficult to compare two configurations in Python. With our improvement though it is fairly trivial and easy understandable by humans.

Go (Golang)

Now we’ll demonstrate same application in Go (Golang). Here are the external packages, which need to be installed:


1
2
3
$ go get github.com/google/go-cmp/cmp
$ go get gopkg.in/yaml.v3
$ go get github.com/scrapli/scrapligo

Those are all the same packages, which were used in the previous blog about SSH. In contrast to Python, Go (Golang) version of scrapli – scrapligo, includes NETCONF driver and it doesn’t have to be installed separately.

Before jumping to the code, we’ll highlight one important difference we are going to implement in Go (Golang) code: instead of using map any types, we will create a set of structs, which will be used to prepare configuration as well as to parse the received messages. Here are those structs:


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
$ cat modules.go
/* From Python to Go: Go: 017 - NETCONF : modules */

package main

// Import
import (
    "encoding/xml"
)

// Data types
type OpenConfigInterface struct {
    Name   string `xml:"name,omitempty"`
    Config struct {
        Name        string `xml:"name,omitempty"`
        Description string `xml:"description,omitempty"`
        Enabled     bool   `xml:"enabled,omitempty"`
    } `xml:"config,omitempty"`
}
type OpenConfigInterfaces struct {
    XMLName   xml.Name              `xml:"interfaces"`
    Interface []OpenConfigInterface `xml:"interface,omitempty"`
}
type NetconfConfig struct {
    XMLName    xml.Name `xml:"config,omitempty"`
    Interfaces OpenConfigInterfaces
}
type NetconfData struct {
    XMLName    xml.Name `xml:"data,omitempty"`
    Interfaces OpenConfigInterfaces
}

type RPCResponse struct {
    XMLName xml.Name `xml:"rpc-reply"`
    Data    NetconfData
}

Type OpenConfigInterfaces follows OpenConfig Interfaces YANG module. We don’t implement all the keys, but only the tiny structure we need to configure network device for our use case. On top of examples of XML conversation we did before, there are a few new concepts we’d like to highlight:

  1. You may see the instruction “omitempty” after mapping the field to XML key. This instruction means that if we don’t set explicitly any value in struct, there will be no key created in XML struct. This is very important as we want to change value of some keys within the struct, no the entire struct.
  2. You see field “XMLName” of xml.Name data type. This data type is used, when we need to attribute key to some namespace, as it is struct with a few fields and “Namespace” is one of them.

We put these data types to a separate module to show that you can split code in Go (Golang) across multiple files and so long they have the same “package” value, they all will be built together.

Now let’s take a look in the code of application:


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
/* From Python to Go: Go: 017 - NETCONF. */

package main

// Imports
import (
    "encoding/xml"
    "flag"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/google/go-cmp/cmp"
    "github.com/scrapli/scrapligo/driver/netconf"
    "github.com/scrapli/scrapligo/driver/options"
    "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 OpenConfigInterfaces
    Config  NetconfConfig
}

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"`
    Platform    string `yaml:"platform"`
    Crendetials Crendetials
    Result      []Result
}

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

    // Get netowrk driver
    dr, err := netconf.NewDriver(
        (*d).IpAddress,
        options.WithAuthNoStrictKey(),
        options.WithAuthUsername(d.Crendetials.Username),
        options.WithAuthPassword(d.Crendetials.Password),
    )
    if err != nil {
        log.Fatalln("failed to fetch network driver from the platform; error ", err)
    }

    // Open session
    err = dr.Open()
    if err != nil {
        log.Fatalln("failed to open driver; error: ", err)
    }
    defer dr.Close()

    // Get change before start
    before, err := dr.GetConfig("running")
    if err != nil {
        log.Fatalln("failed to send command; error: ", err)
    }
    beforeStruct := RPCResponse{}
    err = xml.Unmarshal((*before).RawResult, &beforeStruct)
    if err != nil {
        log.Panic("Cannot parse received response: ", err)
    }

    // Apply change
    configXmlBs, err := xml.Marshal(i.Config)
    if err != nil {
        log.Fatalln("Cannot convert config to XML: ", err)
    }
    configXmlStr := string(configXmlBs)

    changeResponse, err := dr.EditConfig("candidate", configXmlStr)
    if err != nil {
        log.Fatalln("failed to send config; error: ", err)
    } else if changeResponse.Failed != nil {
        log.Fatalln("Return error from device during config; error: ", err)
    }

    commitResponse, err := dr.Commit()
    if err != nil {
        log.Fatalln("failed to commit config; error: ", err)
    } else if commitResponse.Failed != nil {
        log.Fatalln("return error from device during commit; error: ", err)
    }

    // Get state after change
    after, err := dr.GetConfig("running")
    if err != nil {
        log.Fatalln("failed to send command; error: ", err)
    }
    afterStruct := RPCResponse{}
    err = xml.Unmarshal((*after).RawResult, &afterStruct)
    if err != nil {
        log.Panic("Cannot parse received response: ", 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: OpenConfigInterfaces{
            XMLName: xml.Name{
                Space: "http://openconfig.net/yang/interfaces",
                Local: "interfaces",
            },
        },
        Config: NetconfConfig{
            Interfaces: OpenConfigInterfaces{
                XMLName: xml.Name{
                    Space: "http://openconfig.net/yang/interfaces",
                    Local: "interfaces",
                },
                Interface: make([]OpenConfigInterface, 0),
            },
        },
    }
    instruction.Config.Interfaces.Interface = append(instruction.Config.Interfaces.Interface, OpenConfigInterface{
        Name: "Loopback 23",
        Config: struct {
            Name        string "xml:"name,omitempty""
            Description string "xml:"description,omitempty""
            Enabled     bool   "xml:"enabled,omitempty""
        }{
            Name:        "Loopback 23",
            Description: "Test-netconf-golang-2",
        },
    })

    // 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 strongly recommend to read previous blogs, if you aren’t sure about some parts of the code, as we will focus our explanation only on what is relevant to NETCONF.

Let’s analyse the NETCONF specifics:

  1. Instruction struct uses structs NetconfConfig and OpenConfigInterface as data type.
  2. When instruction is created, the corresponding structs are populated as well. Pay attention how we populate slice of interface
  3. The receiver function (aka method) executeChange() is modified to perform interaction with network devices using NETCONF:
    • Using function NewDriver() from Netconf Scrapli driver “github.com/scrapli/scrapligo/driver/netconf“, the struct is created, which is used to interact with network devices.
    • Using receiver function GetConfig() of NetconfDriver struct, the actual running configuration is collected.
    • This data is then converted to struct RPCResponse using Unmarshall() function from “encoding/xml” package.
    • Then the configuration for change from NetconfConfig struct is converted to XML string using Marshall() from “encoding/xml” followed by string() type casting, as scrapligo requires string as input and marhsalling gives you “[]byte” type.
    • Configuration is send to network device using EditConfig() receiver function and is applied using Commit().
    • Then configuration is collected and parsed again
  4. In contrast to previous blogpost, where compared two slices of strings, here we comparing two structs and, as you will see shortly in the execution snippet, it is really fantastic.

Let’s execute this 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
$ go run . -i ../data/inventory.yaml
Config: {{ } {{http://openconfig.net/yang/interfaces interfaces} [{Loopback 23 {Loopback 23 Test-netconf-golang-2 false}}]}}
Impact:   main.RPCResponse{
        XMLName: {Space: "urn:ietf:params:xml:ns:netconf:base:1.0", Local: "rpc-reply"},
        Data: main.NetconfData{
                XMLName: {Space: "urn:ietf:params:xml:ns:netconf:base:1.0", Local: "data"},
                Interfaces: main.OpenConfigInterfaces{
                        XMLName: {Space: "http://openconfig.net/yang/interfaces", Local: "interfaces"},
                        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""; Description string "xml:"description,omitempty""; Enabled bool "xml:"enabled,omitempty"" }{
                                                Name:        "Loopback23",
-                                               Description: "Test-netconf-python-2",
+                                               Description: "Test-netconf-golang-2",
                                                Enabled:     true,
                                        },
                                },
                                {Name: "Loopback0", Config: {Name: "Loopback0", Enabled: true}},
                                {Name: "Loopback51", Config: {Name: "Loopback51", Description: "pytest-update-test-33"}},
                        },
                },
        },
  }

Timestamp: 2025-03-22 20:14:17.95016661 +0000 GMT m=+3.170702314

You see the structured data following all the marshalling/unmarshalling we do in our code. Also, the comparison part shows you precisely which key has changed and which values were before and after.

Lessons in GitHub

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

Conclusion

NETCONF/YANG provides a solid programmable framework for network devices management. It is complementing templating and SSH. In fact, I’d use NETCONF/YANG if it is available and will use SSH/CLI only as last resort. In the next blog post we’ll cover one more API, the most modern one, to manage network devices. 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