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:
- Zero-to-Hero Network Automation Training
- High-scale automation with Nornir
- Ansible Automation Orchestration with Ansble Tower / AWX
- Expert-level training: Closed-loop Automation and Next-generation Monitoring
During these trainings you will learn the following topics:
- Success and failure strategies to build the automation tools.
- Principles of software developments and the most useful and convenient tools.
- Data encoding (free-text, XML, JSON, YAML, Protobuf).
- Model-driven network automation with YANG, NETCONF, RESTCONF, GNMI.
- Full configuration templating with Jinja2 based on the source of truth (NetBox).
- Best programming languages (Python, Bash) for developing automation
- The most rock-solid and functional tools for configuration management (Ansible) and Python-based automation frameworks (Nornir).
- Network automation infrastructure (Linux, Linux networking, KVM, Docker).
- Orchestration of automation workflows with AWX and its integration with NetBox, GitHub, as well as custom execution environments for better scalability.
- Collection network data via SNMP and streaming telemetry with Prometheus
- Building API gateways with Python leveraging Fast API
- Integration of alerting with Slack and your own APIs
- … and many more
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:
- How to prepare XML messages and parse incoming ones?
- How to get configuration from network devices using NETCONF?
- 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&amp;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:
RPC | Description |
---|---|
get | Retrieve operational states of network device. Think of it as “show xxx” CLI commands |
get-config | Retrieve configuration of network device. You can specify, if you want to fetch running, candidate or startup configuration |
edit-config | Push the configuration to network device to specified data store. |
commit | Merge 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:
- Get the config of interfaces before the change
- Apply new configuration
- Get the configuration of interfaces after the change
- Compare the configuration
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:
- 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.
- 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
- 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:
- 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.
- 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:
- Instruction struct uses structs NetconfConfig and OpenConfigInterface as data type.
- When instruction is created, the corresponding structs are populated as well. Pay attention how we populate slice of interface
- 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
- 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