Automate Cisco config backups with Python and Netmiko
If your backup process is "log in and copy the config", it will fail exactly when you need it. Here's a script that does it for every device, every night, and tells you what changed.
What you need
- Python 3.9+ and
pip install netmiko pyyaml - SSH reachability to the devices (a management VLAN or jump host)
- A service account with privilege 15, or an account that can
enable - Key-based auth, ideally — passwords in YAML files end up in git
The inventory
Keep the device list in a separate file so the script stays generic. Credentials come from environment variables, not from the file:
# inventory.yml
- device_type: cisco_ios
host: 192.0.2.11
name: core-sw1
username: netauto
use_keys: true
key_file: ~/.ssh/id_ed25519_netauto
- device_type: cisco_ios
host: 192.0.2.12
name: core-sw2
username: netauto
use_keys: true
key_file: ~/.ssh/id_ed25519_netauto
The script
#!/usr/bin/env python3
"""Back up running-config from every device, with a diff when it changes."""
import difflib
from datetime import datetime
from pathlib import Path
import yaml
from netmiko import ConnectHandler
ROOT = Path("backups")
def main() -> None:
devices = yaml.safe_load(Path("inventory.yml").read_text())
stamp = datetime.now().strftime("%Y-%m-%d")
outdir = ROOT / stamp
outdir.mkdir(parents=True, exist_ok=True)
for dev in devices:
name = dev["name"]
config = dict(dev)
try:
with ConnectHandler(**config) as conn:
conn.enable()
running = conn.send_command("show running-config", read_timeout=60)
except Exception as exc:
print(f"{name:12} FAILED {exc}")
continue
target = outdir / f"{name}.cfg"
if target.exists():
old = target.read_text().splitlines()
diff = list(difflib.unified_diff(
old, running.splitlines(),
fromfile=f"{name} (previous)", tofile=f"{name} (now)",
lineterm=""))
if diff:
(outdir / f"{name}.diff").write_text("\n".join(diff) + "\n")
print(f"{name:12} CHANGED {len(diff)} diff lines")
target.write_text(running + "\n")
print(f"{name:12} ok {len(running.splitlines())} lines")
if __name__ == "__main__":
main()
Why each piece matters
conn.enable() — show running-config often needs privileged mode. Without this you get a partial config or an error, and the script "succeeds" with useless data.
read_timeout — large configs over slow links take longer than the default. A timeout leaves a truncated file that looks valid.
Writing the diff before the new file — order matters. If you overwrite first, you've destroyed the only copy of the old config and the diff is empty.
Failing loudly per device — one unreachable switch shouldn't abort the run. The try/except keeps going and the summary shows what failed.
Scheduling it
# crontab -e (as the automation user)
15 2 * * * cd /opt/netauto && /opt/netauto/.venv/bin/python backup.py >> logs/backup.log 2>&1
Keep at least 30 days of history. Disk is cheap; a config from three weeks ago is often the only way to prove what changed.
Common ways this breaks
| Symptom | Cause | Fix |
|---|---|---|
| Empty or truncated config | Paging still enabled | Netmiko disables it, but check terminal length 0 on odd platforms |
| Auth works, commands fail | Account lacks privilege | Add secret or use privilege 15 |
| Timeouts on a few devices | Slow link or ACL | Raise read_timeout, check management path |
| Secrets in git | Passwords in inventory | Keys + environment variables, and a .gitignore |
Next step: make it parallel
Looping through 40 devices serially takes a while. Wrap the per-device function in a concurrent.futures.ThreadPoolExecutor with a modest worker count (8–16) and the whole network finishes in the time one device takes. Start serial, get it correct, then parallelise.
This is module 4 of Network Automation Lab — the same script, built line by line against real IOS devices, with drift detection and a safe rollout in module 8.