Home > Networking > Automation > Paramiko > Automating Cisco Device Configuration Backups with Python + Paramiko

Automating Cisco Device Configuration Backups with Python + Paramiko

One of the first things any network engineer learns (usually the hard way) is: always have recent backups of your running configurations.

Manual copy running-config tftp://... or show run → paste into Notepad works… until you manage 20+ devices.
After that you either forget, make mistakes, or spend hours doing repetitive work.

I recently put together a small Python script using Paramiko to automate full running-config backups from Cisco devices (switches + routers like ISR4451, Catalyst 1300, etc.).
It turned out useful enough that I thought I’d share the core version + some lessons learned.

What the script does

  • Reads a list of IP addresses from a text file (one per line)
  • Connects via SSH using Paramiko
  • Enters enable mode (if needed)
  • Disables paging (terminal length 0)
  • Captures show running-config
  • Removes unwanted sections (in my case: a specific ACL named test_acl)
  • Strips ANSI escape codes and extra blank lines
  • Saves clean timestamped .txt files in a local folder
  • Supports multi-threading so you can backup many devices in parallel

Quick sample below:

Python
import paramiko
import re
import os
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

# ── Credentials & Paths ──
USERNAME = "your-username"
PASSWORD = "your-password"
IPS_FILE  = r"D:\python\devices.txt"
BACKUP_DIR = r"D:\python\backups"
os.makedirs(BACKUP_DIR, exist_ok=True)

MAX_WORKERS = 4

def remove_test_acl_block(config_text):
    lines = config_text.splitlines()
    result = []
    in_target_acl = False
    for line in lines:
        stripped = line.strip()
        if re.match(r'^ip access-list (extended|standard) test_acl\b', stripped):
            in_target_acl = True
            continue
        if in_target_acl:
            if stripped and not line.startswith(' '):
                in_target_acl = False
                result.append(line.rstrip())
            continue
        result.append(line.rstrip())
    cleaned = '\n'.join(result)
    cleaned = re.sub(r'\n\s*\n+', '\n\n', cleaned).strip()
    return cleaned

def backup_device(ip):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    ts = datetime.now().strftime("%Y%m%d_%H%M")
    filename = f"{ip.replace('.', '_')}_config_{ts}.txt"
    path = os.path.join(BACKUP_DIR, filename)
    
    try:
        client.connect(ip, username=USERNAME, password=PASSWORD, timeout=12)
        shell = client.invoke_shell()
        time.sleep(1.5)
        
        # Disable paging
        shell.send("terminal length 0\n")
        time.sleep(1)
        while shell.recv_ready():
            shell.recv(8192)
        
        # Get config
        shell.send("show running-config\n")
        time.sleep(2)
        output = ""
        start = time.time()
        while time.time() - start < 60:
            if shell.recv_ready():
                chunk = shell.recv(16384).decode(errors="ignore")
                output += chunk
                if re.search(r'[>#]\s*$', chunk):
                    break
            time.sleep(0.2)
        
        # Clean up
        clean = re.sub(r'\x1B\[[0-?]*[ -/]*[@-~]', '', output)
        filtered = remove_test_acl_block(clean)
        
        # Save
        with open(path, "w", encoding="utf-8") as f:
            f.write(f"Backup — {ip}{ts}\n{'='*50}\n\n{filtered}\n")
        
        print(f"OK  {ip}{filename}")
        return path
    
    except Exception as e:
        print(f"FAIL {ip}: {e}")
        return None
    
    finally:
        client.close()

# ── Main ──
if __name__ == "__main__":
    if not os.path.exists(IPS_FILE):
        print("IP file missing")
        exit(1)
    
    with open(IPS_FILE) as f:
        ips = [l.strip() for l in f if l.strip()]
    
    print(f"Backing up {len(ips)} devices...\n")
    
    successes = 0
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as ex:
        futures = {ex.submit(backup_device, ip): ip for ip in ips}
        for future in as_completed(futures):
            if future.result():
                successes += 1
    
    print(f"\nDone: {successes}/{len(ips)} successful")

Tips:

  1. Enable mode If your devices require enable, add enable\n + password right after connect. (I left it commented in the version above since my lab devices use privilege 15 on login.)
  2. Timeouts Large configs (>30k lines) can take 30–60 seconds. I set generous timeouts.
  3. ACL removal The remove_test_acl_block() function is very simple — it stops at the next non-indented line. Works for most cases, but if you have nested objects inside the ACL it might cut too early. (Very rare in standard ACLs.)
  4. Security Never commit this script with real passwords. Use getpass.getpass() or environment variables / keyring in production.

Leave a Comment