You do not need a framework. Most network automation is one of five things: call a REST API, run CLI commands over SSH, speak NETCONF, render configuration, or poll SNMP. Each has a library that is boring and reliable.

The short list

Library Use when Notes
requests Device or controller has a REST API Everything else is HTTP
netmiko Device only has SSH CLI Handles prompts, paging, timeouts
napalm You need multi-vendor getters Abstraction over CLI/NETCONF
ncclient Device speaks NETCONF/YANG Structured, transactional
jinja2 You need to render config Templates, not string concatenation
pysnmp You need counters Async, v3 support

Start with requests. A controller API is the most stable interface a district has, and it does not break when firmware changes a prompt string.

Five libraries, five jobs: pick by the interface the device offers.

REST: the UniFi example

import requests

s = requests.Session()
s.post("https://10.0.0.46/api/auth/login",
       json={"username": USER, "password": PASS}, verify=False)
s.headers["X-CSRF-Token"] = s.cookies.get("csrf_token", "")

r = s.get("https://10.0.0.46/proxy/network/api/s/default/rest/networkconf", verify=False)
r.raise_for_status()
networks = r.json()["data"]

Three habits: a Session for cookies, raise_for_status() so failures are loud, and verify=False only for a self-signed controller on a trusted network.

SSH: netmiko

from netmiko import ConnectHandler

with ConnectHandler(device_type="cisco_ios", host=host,
                    username=user, password=pw) as conn:
    conn.enable()
    print(conn.send_command("show interfaces status"))

send_command handles paging and prompts. It does not make the output structured — you still parse text, which is why this is the fallback, not the first choice.

NETCONF: ncclient

from ncclient import manager

with manager.connect(host=host, username=user, password=pw,
                     hostkey_verify=False) as m:
    cfg = m.get_config(source="running")
    print(cfg.xml)

ncclient returns XML you can parse with a real parser, not a regex.

Templates: jinja2

{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}

Templates make configuration reviewable. The reviewable artifact is the template plus the data, not the rendered output.

Never build configuration with string concatenation

"vlan " + str(id) is how a missing newline becomes a merged command and an outage. Templates handle whitespace, escaping, and loops; concatenation handles none of them.

Worked example: nightly config snapshot and diff

import difflib, pathlib, datetime

today = pathlib.Path(f"snap/{datetime.date.today()}.cfg")
yesterday = sorted(pathlib.Path("snap").glob("*.cfg"))[-2]

diff = difflib.unified_diff(
    yesterday.read_text().splitlines(),
    today.read_text().splitlines(),
    lineterm="",
)
print("\n".join(diff) or "no change")

Thirty lines, no dependencies, and it answers "what changed on the network last night" every morning.

What breaks in production

  • Parsing with regex when a structured API exists.
  • No timeouts, so one unreachable device hangs the run.
  • No retries, so a transient 502 fails a nightly job silently.
  • Credentials in source, which is a rotation problem forever.
  • Rate limits ignored, so a loop of 200 API calls gets throttled.
  • Silent failures, because raise_for_status() was omitted.

Next

Automation that runs is not automation that is safe. Continue with Validating Configuration.

  • Python
  • requests
  • netmiko
  • napalm
  • ncclient
  • Automation