Why gnetcli: talking to network devices from code without losing your mind
Here's a problem that sounds trivial until you've actually done it: run a command on a network device from code and get the output back.
On a server it's one line. On a router or a firewall it's a small nightmare. There's no clean API for most of what network engineers do day to day - there's a CLI designed for a human sitting at a terminal. So for twenty years the "automation" answer has been the same: pretend to be that human. Screen-scraping with expect. Matching prompts with regexes. Sending a space every time a pager stops the output. Guessing when a command finished. Hoping the vendor didn't change the banner.
It works right up until it doesn't - at 2 a.m., on the one device that's running a slightly different OS version.
This is the problem gnetcli exists to kill. I use it every day as the engine underneath my config-delivery pipeline, and I've contributed a handful of patches back to it. This post is why I think it's the right foundation - and why the boring-sounding problem it solves is actually the hard part of network automation.
The real problem isn't the command. It's everything around it.
When a human runs "show interfaces" on a switch, their brain silently does a dozen things: it recognises the prompt and knows the device is ready for input; it sees the pager prompt and hits space to keep scrolling; it notices "% Invalid input" and understands the command failed; it ignores the command echoed back before the output; it strips the terminal control codes that colour and position the text; and it knows when the output has ended and the prompt has returned.
Every expect-style script re-implements all of that by hand, per vendor, with regexes. That's the fragility. You're not automating a command - you're reverse-engineering a terminal session and encoding a hundred tiny assumptions that each vendor gets to break on a whim.
What gnetcli does differently
gnetcli reframes the whole thing. Instead of "drive a terminal," the model is: a command goes in; structured output, an error, and a status come back.
It handles the messy middle for you - pagers, interactive yes/no prompts, command-validation errors, echo removal, terminal control-code evaluation - so the thing your code receives is the clean output a human would have read, plus a clear signal of whether the command actually succeeded. No more "did it work? let me grep the output for the word 'error' and pray."
It's written in Go, speaks SSH (with tunnelling), supports NETCONF, and ships with device support for the vendors you actually meet in a mixed estate - Huawei, Juniper, Cisco, FortiGate, RouterOS. Each vendor's prompt, pager and error quirks live in one place in the framework, not scattered across every script in your organisation.
Three ways to use it, and why the gRPC one matters
gnetcli gives you the same core through three interfaces. As a Go library, you import it and drive devices directly from your Go services. As a CLI tool, it's a standalone binary for one-off commands and quick scripting. And as a gRPC server, you run it as a service - and now any language or system can talk to network devices through it.
That third one is the quiet power move. The moment device access is a gRPC service, your automation stops being a pile of per-engineer scripts and becomes infrastructure. A config pipeline, a web tool, a Python job, an orchestrator written in a different language - they all hit the same well-tested endpoint instead of each re-inventing the terminal-wrangling. This is exactly why gnetcli sits at the core of annet, the config-generation toolchain a lot of us build on, and why it's the layer my own GitOps-for-networks pipeline reaches for when it's time to actually touch a device.
What I learned contributing to it
Using something in production is one thing; sending patches upstream is where you really learn its shape. My contributions were unglamorous, and that's the point. I added Cisco ASA support, because a firewall's CLI has its own personality - contexts, prompts, error behaviour - that the generic Cisco handling didn't fully cover. I added a telnet transport, because the real world still has reachable-only-over-telnet gear, and "we only support SSH" is a luxury a mixed estate can't always afford. And I fixed proxy-jump, because getting to a device through a bastion is the normal case, not the exception, and it has to just work.
None of these are exciting features. All of them are the difference between "works in the demo" and "works on your network." That's the tax of network automation: the last 10% of device reality is 90% of the pain, and a good framework is one where that last 10% is a small, local patch instead of a rewrite.
Why this is open source, and why that matters
Here's the thing about the vendor-CLI mess: it's universal. Every network team on earth has fought the same pager behaviour, the same echoed command, the same "is this the login prompt or the enable prompt" ambiguity. Solving it privately, a hundred times over, inside a hundred companies, is a staggering waste.
An open, shared framework turns that duplicated pain into a single, well-tested dependency. You get to spend your time on the part that's actually specific to your network - the intent, the policy, the pipeline - instead of re-deriving how to read a Juniper pager for the thousandth time. And when you do hit a gap - a new vendor, a weird transport - you fix it once, upstream, and everyone downstream of you stops hitting it too.
If you're starting out
If you're writing your first network automation and you're reaching for expect or paramiko plus a wall of regexes: don't. Start from the model of command in, structured result out, and let a framework own the terminal. Point gnetcli at a lab device, run a show command, and watch how much nonsense you don't have to write.
The interesting problems in network automation - source of truth, config generation, safe rollout, drift detection - all sit on top of "reliably talk to the device." gnetcli is the layer that makes that bottom turtle solid, so you can go build the rest.