Python · Netmiko · 9 min read

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

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

SymptomCauseFix
Empty or truncated configPaging still enabledNetmiko disables it, but check terminal length 0 on odd platforms
Auth works, commands failAccount lacks privilegeAdd secret or use privilege 15
Timeouts on a few devicesSlow link or ACLRaise read_timeout, check management path
Secrets in gitPasswords in inventoryKeys + 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.

Get early access