← Interview Prep

Automation Frameworks — Ansible+NAPALM vs Nornir vs Annet+gnetcli

A head-to-head of three network-automation stacks: Ansible+NAPALM (declarative YAML), Nornir (pure Python), and Annet+gnetcli (generators + vendor-aware diff, SSH/Telnet). Clarifies that NAPALM is a library and gnetcli a transport (not frameworks), with a side-by-side table, when-to-choose guidance, and how they compose.

"Which network automation tool should we use?" has no single answer — it depends on your team, your vendors, and how much logic you need. This compares three common stacks head-to-head: Ansible + NAPALM, Nornir, and Annet + gnetcli — their philosophies, where they overlap, and when to pick each.

First, untangle the layers: Ansible, Nornir, and Annet are frameworks. NAPALM is a multi-vendor library (getters + config diff/commit) and gnetcli is a transport (SSH/Telnet as a service). NAPALM and gnetcli plug into frameworks — they aren't alternatives to them.

The three approaches in one line

Side by side

DimensionAnsible + NAPALMNornirAnnet + gnetcli
ParadigmDeclarative (YAML)Imperative (Python)Generators → structured diff
You writePlaybooks + JinjaPython functionsPython generators (per-vendor)
Config modelTemplates / resource modulesWhatever you codeGenerators + ACL-scoped partial config
Diff engineNAPALM (vendor-dependent)NAPALM, or your ownNative multi-vendor diff (a core strength)
Transportnetwork_cli/netconf/httpapi; NAPALM driversnetmiko / NAPALM / scraplignetcli (SSH and Telnet, via a Go daemon)
IdempotencyYes (resource modules); NAPALM replaceYou build it (or via NAPALM)Native — deploy the computed delta
Inventory / SoTStatic or dynamic (NetBox)Inventory plugins (NetBox)NetBox or file
Multi-vendorBroad (collections + NAPALM)Via NAPALM/netmiko/scrapliStrong (breeds + per-vendor generators)
ParallelismForks (processes)Threaded runnergnetcli server handles sessions
Testingmolecule, ansible-lintpytest (plain Python)pytest + offline fake-device
Who / learning curveLow — ops teamsMedium — Python devsHigher — platform/NetDevOps teams
Ecosystem / maturityVery largeSolid, growingNiche (Yandex-origin), focused

Where NAPALM and gnetcli fit

When to choose which

If you…Lean toward
Want low-code, a big module library, mixed server+network opsAnsible + NAPALM
Have complex logic, want unit tests, a Python team, real scaleNornir
Run a large multi-vendor network and want a trustworthy diff + partial config, or need TelnetAnnet + gnetcli
Just need cross-vendor facts / a safe config replaceNAPALM (inside any of them)

The same task in each tool

One Cisco IOS device, four steps: (1) collect show version, (2) generate an interface's IP + MTU, (3) see the diff, (4) apply. Note the asymmetry on step 1:

StepAnsible + NAPALMNornirAnnet + gnetcli
show versionios_command / ios_factsnetmiko_send_commandcan't — Annet doesn't collect facts
generate IP+MTUtemplate / ios_config / resource modulebuild lines in Pythona generator (from the SoT)
diff--check --diffNAPALM dry_run=Trueannet diff
applyrun without --checkNAPALM dry_run=Falseannet deploy

Ansible + NAPALM

# 1) show version (ad-hoc)
ansible ios -m cisco.ios.ios_command -a "commands='show version'"

# 2-4) interface IP+MTU — playbook (iface.yml)
- hosts: ios
  gather_facts: false
  tasks:
    - name: Interface IP + MTU
      cisco.ios.ios_config:
        parents: interface GigabitEthernet0/1
        lines:
          - ip address 10.0.0.1 255.255.255.0
          - mtu 9000
        backup: true

# 3) diff (nothing changes):   ansible-playbook iface.yml --check --diff
# 4) apply:                    ansible-playbook iface.yml

Nornir

from nornir_netmiko import netmiko_send_command
from nornir_napalm.plugins.tasks import napalm_configure
from nornir_utils.plugins.functions import print_result

# 1) show version
print_result(nr.filter(platform="cisco_ios").run(
    task=netmiko_send_command, command_string="show version"))

# 2) generate the config (plain Python)
cfg = "interface GigabitEthernet0/1\n ip address 10.0.0.1 255.255.255.0\n mtu 9000\n"

# 3) diff — NAPALM dry-run prints the delta, changes nothing
napalm = nr.filter(platform="ios")     # NAPALM uses 'ios' (netmiko uses 'cisco_ios')
print_result(napalm.run(task=napalm_configure, configuration=cfg, dry_run=True))

# 4) apply
napalm.run(task=napalm_configure, configuration=cfg, dry_run=False)

Annet + gnetcli

Step 1 is not possible the way it is above: Annet has no facts-collection step. It reads intent from the SoT (NetBox/file), fetches only the device's running-config to diff against, and deploys the delta. For arbitrary show output you'd use gnetcli / netmiko / NAPALM alongside it.

# 2) generate — a generator; the ip/mtu come from the SoT, NOT the device
class IfaceIP(PartialGenerator):
    def acl_cisco(self, _):
        return "interface\n    ip address\n    mtu"

    def run_cisco(self, device):
        for iface in device.interfaces:
            if iface.ip and iface.mtu:
                with self.block(f"interface {iface.name}"):
                    yield f"ip address {iface.ip} {iface.mask}"
                    yield f"mtu {iface.mtu}"

# 3) diff — fetch running-config over gnetcli, structured diff vs generated
annet diff   cisco-sw1.example.net
#   interface GigabitEthernet0/1
# +  ip address 10.0.0.1 255.255.255.0
# +  mtu 9000

# 4) apply
annet deploy cisco-sw1.example.net

So the honest summary: Ansible and Nornir can pull operational state (show version); Annet cannot — it is intent/SoT-driven and only ever fetches running-config to compute a diff. See Annet, Nornir — Practice, and Ansible — Cases.

They compose — it's not either/or

These mix freely: Nornir + NAPALM is a classic pairing; Ansible can shell out to Python; a shop might use Annet for the render-and-diff engine with gnetcli transport while driving surrounding workflow from Nornir. The frameworks are the orchestration; NAPALM/netmiko/scrapli/gnetcli are interchangeable transports and libraries underneath.

Likely interview questions

Related: Ansible · Nornir · Annet · Python Automation.