darrenqu.net

Automation

netrasp: Netmiko-Style Network Automation in Go

1092 words 6 min read

go

A Go library for driving network devices over SSH. The API, a working read and write example, and the error-handling behaviour that will bite you if you assume Netmiko semantics.

on this page

Network automation is Python by default. Netmiko, NAPALM, Nornir and Ansible cover most of what anyone needs, and there is rarely a reason to leave.

The reason I keep coming back to Go is deployment. A Go program compiles to a single static binary with no interpreter, no virtualenv, and no dependency resolution on the target host. For a tool that has to run on a jump box you do not control, or ship to colleagues who should not have to manage a Python environment, that difference is the whole argument.

netrasp is the Netmiko equivalent in that world: connect to a device over SSH, send commands, get output back.

Supported drivers#

netrasp.WithDriver("ios")    // Cisco IOS
netrasp.WithDriver("nxos")   // Cisco NX-OS
netrasp.WithDriver("asa")    // Cisco ASA
netrasp.WithDriver("sros")   // Nokia SR OS

A much shorter list than Netmiko’s, and the first thing to check against your own estate. There is no Huawei or H3C driver, which for a lot of us is decisive — the honest position is that netrasp is a good fit for Cisco-heavy environments and not yet a general replacement.

An invalid driver name fails at netrasp.New() rather than at connect time, which is the right place for it.

Reading from a device#

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/networklore/netrasp/pkg/netrasp"
)

func main() {
	device, err := netrasp.New("192.0.2.10",
		netrasp.WithUsernamePassword("admin", "password"),
		netrasp.WithDriver("ios"),
		netrasp.WithDialTimeout(5*time.Second),
		netrasp.WithSSHPort(22),
		netrasp.WithInsecureIgnoreHostKey(),
	)
	if err != nil {
		log.Fatalf("unable to initialize device: %v", err)
	}

	if err := device.Dial(context.Background()); err != nil {
		log.Fatalf("unable to connect: %v", err)
	}
	defer device.Close(context.Background())

	output, err := device.Run(context.Background(), "show running-config")
	if err != nil {
		log.Fatalf("unable to run command: %v", err)
	}
	fmt.Println(output)
}

The shape of it:

  • netrasp.New() builds and validates the connection parameters. It does not connect. Options are functional — you pass as many With... as you need and omit the rest.
  • device.Dial() opens the SSH session.
  • defer device.Close() is the equivalent of Python’s with: it runs when the function returns, whichever path it takes.
  • device.Run() sends one command and returns its output.
  • if err != nil after every call is Go’s whole error model. Verbose, and it means every failure is handled at the point it happens rather than unwinding to some distant handler.

Two options worth knowing about for real networks:

netrasp.WithSSHCipher("aes128-cbc")   // reach older gear
netrasp.WithInsecureIgnoreHostKey()   // skip host key verification

WithSSHCipher adds an algorithm that modern SSH clients have dropped, which is how you reach a switch that has not had a firmware update in eight years.

WithInsecureIgnoreHostKey does what it says. It is the pragmatic choice in a lab and against equipment whose keys change every time it is replaced. It is named Insecure on purpose — it disables the protection against a man-in-the-middle on the management network, and that is a real decision, not a formality. Use it knowingly.

Configuring a device#

config := []string{
	"ip access-list extended tcp_connect",
	" permit tcp any any eq 23",
	" permit tcp any any eq 80",
	" do write memory",
}

output, err := device.Configure(context.Background(), config)
if err != nil {
	log.Fatalf("unable to configure device: %v", err)
}
fmt.Println(output)

Configure() takes a slice of lines, enters configuration mode, sends them in order, and returns each command paired with the device’s response:

{[{ip access-list extended tcp_connect }
  { permit tcp any any eq 23 }
  { permit tcp any any eq 80 }
  { do write memory Building configuration...
Compressed configuration from 3398 bytes to 1971 bytes[OK]
}]}

Note do write memory rather than write memory. Configure() leaves you inside configuration mode, so do is needed to run an exec command from there — an easy detail to lose an hour to.

The behaviour that will bite you#

Configure() does not stop on a device error.

If a line is rejected — bad syntax, unsupported command, insufficient privilege — netrasp does not return an error. It sends the remaining lines and returns normally. err is nil. Your program reports success.

This is not Netmiko’s behaviour and it is not what most people assume. The consequence is that a partially applied configuration looks exactly like a successful one, and on an ACL or a routing policy, partially applied is often worse than not applied at all.

So the returned output is not a log to print and forget. It is the only place the failure is recorded, and you have to inspect it:

output, err := device.Configure(context.Background(), config)
if err != nil {
	log.Fatalf("unable to configure device: %v", err)
}

for _, line := range output.ConfigCommands {
	if strings.Contains(line.Output, "Invalid input") ||
		strings.Contains(line.Output, "%") {
		log.Printf("device rejected %q: %s", line.Command, line.Output)
	}
}

String matching on error text is crude, and it is what the library leaves you with. Match against the error markers your platform actually emits — % Invalid input detected, % Incomplete command, % Authorization failed on IOS — and treat any of them as a failure of the run, not of one line.

Verify independently afterwards. Push the configuration, then read it back with Run() and confirm the device is in the state you intended. Trusting the push is how a script reports success on fifty devices and leaves three of them broken.

netrasp and Gornir#

For running the same job across many devices, Gornir is the Go counterpart to Nornir: an inventory, concurrent task execution, and result aggregation. netrasp is the connection layer inside it, the way Netmiko sits inside Nornir.

Concurrency is where Go’s argument gets stronger. Goroutines make fanning out across hundreds of devices straightforward, without the thread-pool and async bookkeeping the Python equivalent needs.

Would I use it over Netmiko#

For a Cisco-heavy estate where the tool has to be handed to other people: yes. A single binary that runs anywhere with no environment to prepare is worth real friction elsewhere.

For a mixed-vendor environment: not yet. The driver list is the limit, and no amount of language preference compensates for the platform you actually own not being supported.

For learning: worth an afternoon regardless. Most network engineers write Python and a few write Go, and the ones who do are noticeably more comfortable with concurrency, static typing, and shipping tools that other people can run. That gap is worth closing before you need it.


References

  • netrasp
  • Gornir — concurrent task execution across an inventory
  • Netmiko — the Python equivalent, for comparison

API surface reflects the netrasp version I used; check the repository for current options and behaviour. The Configure() error semantics described above are the behaviour I observed — verify against your own version before relying on it.

← more in Automation