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.
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.