Octet

Back up device configs with a Netmiko script

PythonCore

Tasks

  1. Make sure every device accepts SSH from SRV1 with a backup account that lands in privileged mode.
  2. Write inventory.yml with the three devices.
  3. Write backup.py that backs up all devices in parallel into backups/<date>/<name>.cfg.
  4. Handle authentication failures, unreachable devices and slow output per device without stopping the run.
  5. Exit with code 1 if any device failed.

Solution

  1. 1Device prerequisitesASW1
    ASW1 on Cisco IOS
    ip domain name example.com
    crypto key generate rsa modulus 2048
    ip ssh version 2
    username backup privilege 15 secret 0 Backup.2026
    line vty 0 15
     login local
     transport input ssh

    Repeat on ASW2 and CORE1. A privilege 15 local user keeps the lab simple. In production use a TACACS+ service account that is authorized only for the show commands the job needs.

  2. 2Inventory file
    YAML
    # inventory.yml
    devices:
      - name: CORE1
        host: 10.0.0.1
        device_type: cisco_xe
      - name: ASW1
        host: 10.0.0.11
        device_type: cisco_ios
      - name: ASW2
        host: 10.0.0.12
        device_type: cisco_ios
  3. 3The backup script
    Python
    #!/usr/bin/env python3
    """Back up the running config of every device in inventory.yml."""
    import os
    import sys
    from concurrent.futures import ThreadPoolExecutor
    from datetime import datetime
    from pathlib import Path
    
    import yaml
    from netmiko import ConnectHandler
    from netmiko.exceptions import (
        NetmikoAuthenticationException,
        NetmikoTimeoutException,
        ReadTimeout,
    )
    
    USERNAME = os.environ["NET_USER"]
    PASSWORD = os.environ["NET_PASS"]
    BACKUP_DIR = Path("backups") / datetime.now().strftime("%Y-%m-%d")
    
    
    def backup(dev):
        params = {
            "device_type": dev["device_type"],
            "host": dev["host"],
            "username": USERNAME,
            "password": PASSWORD,
        }
        try:
            with ConnectHandler(**params) as conn:
                config = conn.send_command("show running-config", read_timeout=120)
        except NetmikoAuthenticationException:
            return dev["name"], "FAIL auth"
        except NetmikoTimeoutException:
            return dev["name"], "FAIL unreachable"
        except ReadTimeout:
            return dev["name"], "FAIL read timeout"
        path = BACKUP_DIR / f"{dev['name']}.cfg"
        path.write_text(config + "\n")
        return dev["name"], f"OK {len(config.splitlines())} lines"
    
    
    def main():
        with open("inventory.yml") as f:
            devices = yaml.safe_load(f)["devices"]
        BACKUP_DIR.mkdir(parents=True, exist_ok=True)
        with ThreadPoolExecutor(max_workers=10) as pool:
            results = list(pool.map(backup, devices))
        failed = 0
        for name, status in sorted(results):
            print(f"{name:<8} {status}")
            if status.startswith("FAIL"):
                failed += 1
        sys.exit(1 if failed else 0)
    
    
    if __name__ == "__main__":
        main()

    Each worker catches its own exceptions and returns a status, so one bad device never stops the others. read_timeout=120 covers large configs; the Netmiko 4 default is 10 seconds.

  4. 4Run it
    Linux
    export NET_USER=backup
    read -s -p "Password: " NET_PASS; echo; export NET_PASS
    python3 backup.py

    read -s keeps the password out of shell history. For a cron job, load the variables from a file that only the service account can read, or from a secrets manager.

Verify

SRV1 output
SRV1# python3 backup.py; echo "exit code $?"
ASW1     OK 212 lines
ASW2     OK 209 lines
CORE1    OK 1184 lines
exit code 0

Every device reports OK and the script exits with 0.

SRV1 output
SRV1# ls backups/2026-11-10/ && head -5 backups/2026-11-10/ASW1.cfg
ASW1.cfg  ASW2.cfg  CORE1.cfg
Building configuration...

Current configuration : 6843 bytes
!
! Last configuration change at 09:12:44 UTC Mon Nov 9 2026 by netops

One file per device in today's folder, each starting with the device's own config header.

SRV1 output
SRV1# NET_PASS=wrong python3 backup.py; echo "exit code $?"
ASW1     FAIL auth
ASW2     FAIL auth
CORE1    FAIL auth
exit code 1

Failures are reported per device and the exit code is 1, so cron or CI flags the run.

Mistakes people make

  • Leaving read_timeout at the default and getting ReadTimeout on the switches with the biggest configs.
  • Letting one exception escape a worker. With pool.map, it is raised when you read the results and the whole run dies.
  • Putting the password in the script or the inventory file.
  • Storing backups in a world-readable folder. Configs hold password hashes, SNMP communities and keys.
  • Using cisco_ios for an NX-OS or Junos device. Pick the right device_type per platform.

On other platforms

The same idea with NAPALM, which can also strip secrets from the config it returns.

Python
from napalm import get_network_driver

driver = get_network_driver("ios")
with driver("10.0.0.11", USERNAME, PASSWORD) as dev:
    running = dev.get_config(retrieve="running", sanitized=True)["running"]

Say it in the interview

"The script reads devices from YAML, runs Netmiko in a thread pool, and each worker catches authentication, connection and read timeout errors and returns a status. Configs go to a dated folder, and the exit code is non-zero if anything failed so the scheduler alerts. Next steps would be committing the backups to a private Git repo for history and diffs."