Skip to main content

Command Palette

Search for a command to run...

Introduction to Network Automation: Build Your First Backup Script with Python for a multi-vendor environment - Part 2

Updated
10 min read
Introduction to Network Automation: Build Your First Backup Script with Python for a multi-vendor environment - Part 2

Welcome back to Part 2 of our network automation series. In Part 1, we explored the fundamentals of Netmiko, including connecting to a device and executing commands. Now, we will apply those skills.

In this post, we will develop a practical configuration backup script to automate a common task for network engineers: backing up device configurations across multiple network devices. Instead of manually SSH-ing into each router or switch, you will have a Python script that performs this task automatically and saves timestamped backups.

What you'll learn:

  • Efficiently managing multiple device connections

  • Implementing error handling for practical scenarios

  • Organizing and timestamping backup files

What you'll need:

  • A basic understanding of Netmiko (as covered in Part 1)

  • Python 3.x installed

  • Access to network devices (whether physical, virtual, or via Containerlab)

  • A text editor or IDE

By the end of this tutorial, you'll have a working automation script that you can adapt for your own network environment. Let's get started

Building Your First Backup Script

Version 2: Multi-Vendor Support

Now, let's extend this to support Nokia, Cisco, and Juniper devices.

from netmiko import ConnectHandler
from datetime import datetime
import os
import sys

# Device inventory - add your devices here
devices = [
    {
        'device_type': 'alcatel_sros',
        'host': '172.20.20.13',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'nokia'
    },
    {
        'device_type': 'cisco_xr',  # Use 'cisco_xe' for IOS-XE, 'cisco_xr' for IOS-XR
        'host': '172.20.20.15',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'cisco'
    },
    {
        'device_type': 'juniper_junos',
        'host': '172.20.20.16',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'juniper'
    }
]

# Vendor-specific commands
VENDOR_COMMANDS = {
    'nokia': {
        'pager': 'environment no more',
        'config': 'admin display-config',
        'prompt': r'#'
    },
    'cisco': {
        'pager': 'terminal length 0',
        'config': 'show running-config',
        'prompt': r'#'
    },
    'juniper': {
        'pager': 'set cli screen-length 0',
        'config': 'show configuration',
        'prompt': r'[>#]'
    }
}

# Create backup directory if it doesn't exist
backup_dir = 'backups'
if not os.path.exists(backup_dir):
    os.makedirs(backup_dir)
    print(f"Created backup directory: {backup_dir}\n")

def validate_config(config_text, vendor):
    """Validate that configuration was retrieved successfully"""
    if not config_text or len(config_text) < 100:
        return False, "Configuration appears empty or too short"

    # Vendor-specific validation
    validation_keywords = {
        'nokia': ['configure', 'system'],
        'cisco': ['version', 'interface'],
        'juniper': ['system', 'interfaces']
    }

    keywords = validation_keywords.get(vendor, [])
    config_lower = config_text.lower()

    if not any(keyword in config_lower for keyword in keywords):
        return False, f"Configuration doesn't appear to be valid {vendor.upper()} config"

    return True, "Configuration validated"

def backup_device(device_info):
    """Backup a single device and return status"""
    vendor = device_info.get('vendor', 'unknown')
    host = device_info['host']

    print(f"\n{'='*70}")
    print(f"Processing {vendor.upper()} device: {host}")
    print(f"{'='*70}")

    try:
        # Get vendor-specific commands
        commands = VENDOR_COMMANDS.get(vendor)
        if not commands:
            return False, f"Unknown vendor: {vendor}"

        # Create connection parameters (exclude 'vendor' key)
        connection_params = {k: v for k, v in device_info.items() if k != 'vendor'}

        # Connect to device
        print(f"Connecting to {host}...")
#        connection = ConnectHandler(**device_info)
        connection = ConnectHandler(**connection_params)
        print("✓ Connected successfully")

        # Get hostname for filename
#        hostname = connection.find_prompt().strip('#> ')
#        print(f"✓ Device hostname: {hostname}")

        # Get hostname for filename
        raw_hostname = connection.find_prompt().strip('#> ')

        # Clean hostname - remove invalid characters for filenames
        # For Cisco XR: RP/0/RP0/CPU0:hostname becomes hostname
        if ':' in raw_hostname:
            hostname = raw_hostname.split(':')[-1]  # Get part after last colon
        else:
            hostname = raw_hostname

        # Remove any remaining invalid filename characters
        hostname = hostname.replace('/', '_').replace('\\', '_').replace(':', '_')


        # Set terminal parameters
        print("Setting terminal parameters...")
        connection.send_command(commands['pager'], expect_string=commands['prompt'])

        # Retrieve configuration
        print(f"Retrieving configuration from {hostname}...")
        config = connection.send_command(
            commands['config'],
            expect_string=commands['prompt'],
            delay_factor=2
        )

        # Validate configuration
        is_valid, message = validate_config(config, vendor)
        print(f"✓ {message}")

        if not is_valid:
            print(f"⚠ Warning: {message}")
            connection.disconnect()
            return False, message

        # Create timestamp for filename
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

        # Create vendor subdirectory
        vendor_dir = os.path.join(backup_dir, vendor)
        if not os.path.exists(vendor_dir):
            os.makedirs(vendor_dir)

        # Create filename
        filename = f"{vendor_dir}/{hostname}_{timestamp}.txt"

        # Save configuration to file
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(f"# Backup created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write(f"# Device: {hostname} ({host})\n")
            f.write(f"# Vendor: {vendor.upper()}\n")
            f.write(f"# Device Type: {device_info['device_type']}\n")
            f.write(f"# {'='*60}\n\n")
            f.write(config)

        # Verify file was created and has content
        file_size = os.path.getsize(filename)
        print(f"✓ Configuration saved to {filename}")
        print(f"✓ File size: {file_size:,} bytes")

        # Disconnect
        connection.disconnect()
        print("✓ Disconnected successfully")

        return True, filename

    except ConnectionError as e:
        error_msg = f"Connection error: {str(e)}"
        print(f"✗ {error_msg}")
        return False, error_msg

    except Exception as e:
        error_msg = f"Error: {str(e)} (Type: {type(e).__name__})"
        print(f"✗ {error_msg}")
        return False, error_msg

# Main execution
def main():
    """Backup all devices in inventory"""
    print("\n" + "="*70)
    print("MULTI-VENDOR NETWORK DEVICE BACKUP")
    print("="*70)
    print(f"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Total devices: {len(devices)}\n")

    results = {
        'success': [],
        'failed': []
    }

    # Process each device
    for device in devices:
        success, info = backup_device(device)

        if success:
            results['success'].append({
                'host': device['host'],
                'vendor': device.get('vendor', 'unknown'),
                'file': info
            })
        else:
            results['failed'].append({
                'host': device['host'],
                'vendor': device.get('vendor', 'unknown'),
                'error': info
            })

    # Print summary
    print("\n" + "="*70)
    print("BACKUP SUMMARY")
    print("="*70)
    print(f"Successful: {len(results['success'])}/{len(devices)}")
    print(f"Failed: {len(results['failed'])}/{len(devices)}")

    if results['success']:
        print("\n✓ Successful backups:")
        for item in results['success']:
            print(f"  - {item['vendor'].upper()}: {item['host']}")

    if results['failed']:
        print("\n✗ Failed backups:")
        for item in results['failed']:
            print(f"  - {item['vendor'].upper()}: {item['host']}")
            print(f"    Reason: {item['error']}")

    print(f"\nEnd time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("="*70 + "\n")

    # Exit with appropriate code
    sys.exit(0 if not results['failed'] else 1)

if __name__ == "__main__":
    main()

Expected Output

python3 multivendorbackup.py

======================================================================
MULTI-VENDOR NETWORK DEVICE BACKUP
======================================================================
Start time: 2026-01-22 21:14:29
Total devices: 3


======================================================================
Processing NOKIA device: 172.20.20.13
======================================================================
Connecting to 172.20.20.13...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from PE1...
✓ Configuration validated
✓ Configuration saved to backups/nokia/PE1_20260122_211432.txt
✓ File size: 4,263 bytes
✓ Disconnected successfully

======================================================================
Processing CISCO device: 172.20.20.15
======================================================================
Connecting to 172.20.20.15...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from cisco_xrv9000...
✓ Configuration validated
✓ Configuration saved to backups/cisco/cisco_xrv9000_20260122_211434.txt
✓ File size: 2,529 bytes
✓ Disconnected successfully

======================================================================
Processing JUNIPER device: 172.20.20.16
======================================================================
Connecting to 172.20.20.16...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from admin@juniper...
✓ Configuration validated
✓ Configuration saved to backups/juniper/admin@juniper_20260122_211435.txt
✓ File size: 1,114 bytes
✓ Disconnected successfully

======================================================================
BACKUP SUMMARY
======================================================================
Successful: 3/3
Failed: 0/3

✓ Successful backups:
  - NOKIA: 172.20.20.13
  - CISCO: 172.20.20.15
  - JUNIPER: 172.20.20.16

End time: 2026-01-22 21:14:35
======================================================================

Lets analyze the script for a clearer understanding

  1. from netmiko import ConnectHandler Firstly we import the ConnectHandler from the netmiko library used to connect into network devices

  2. from datetime import datetime We then import datetime from the datetime library , to be able to work with time and dates

  3. import os which is an operating system library used for working with files and folders

  4. import sys used for system-level operations

  5. Device Inventory: This is where we define all the devices we want to back up. The devices variable holds our list of all devices. Inside the list, each device is represented by a dictionary

  6. vendor_commands A dictionary of dictionaries (nested dictionaries). Each vendor has its own set of commands

  7. Create a backup directory if it doesn’t exist. backup_dir = 'backups' a variable is created named backup_dir . This is the name of the folder where we will store the backups. os.path.exists(backup_dir)

    Checks if the folder exists and it returns True if it exists, False if it doesn't

  8. The Validation Function def validate_config(config_text, vendor) checks if config_text is empty or None and returns True if it is empty, and False if it has content. The or operator means if either condition is true, the entire expression is true. len(config_text) < 100 Checks if config has fewer than 100 characters. return returns two values to the function false = validation failed and "Configuration appears..." = Error Message .

  9. Vendor-Specific Validation involves creating a dictionary of keywords we expect to find. Each vendor's configuration should include certain words. If these words are missing, there could be a problem..

  10. def backup_device(device_info): This function takes one input, device_info, which is a dictionary containing the device details. vendor = device_info.get('vendor', 'unknown') uses .get() to safely retrieve the vendor. If the 'vendor' key doesn't exist, it uses 'unknown' as the default. host = device_info['host'] gets the IP address from device_info.

  11. Create connection parameters using connection_params = {k: v for k, v in device_info.items() if k != 'vendor'}. The device_info.items() retrieves pairs of (key, value), and for k, v in ... loops through each pair. If k != 'vendor', it doesn't include the vendor, and {k: v ...} creates a new dictionary. Connect to the device.

  12. Retrieve the hostname for the filename and sanitize it by removing any characters that are not valid for filenames.

  13. connection.send_command(commands['pager'], expect_string=commands['prompt']) used to turn off pagination

  14. The commands['config'] is used to retrieve the configuration, and expect_string=commands['prompt'] specifies the prompt to wait for before continuing.

  15. is_valid, message = validate_config(config, vendor) calls the validation function. If the validation fails, it prints a warning.

  16. Create a timestamp and a vendor directory. Use os.path.join() to combine folder paths.

  17. Create the filename using filename = f"{vendor_dir}/{hostname}_{timestamp}.txt" and save the configuration.

  18. Verify file creation by checking the file size in bytes with os.path.getsize(filename).

  19. Error Handling except ConnectionError as e : Catches only connection-related errors and except Exception as e : Catches any other error

  20. The main function main() orchestrates the entire backup process. It calls backup_device() for each device and collects and displays the results.

How It All Works Together

The Complete Flow

1. Script starts
├─> Import libraries
├─> Define device list
├─> Define vendor commands
└─> Create backup directory

2. main() function runs
├─> Print header
├─> Create results dictionary
└─> For each device:
├─> Call backup_device()
│ ├─> Connect to device
│ ├─> Get hostname
│ ├─> Disable paging
│ ├─> Get configuration
│ ├─> Validate configuration
│ ├─> Save to file
│ ├─> Disconnect
│ └─> Return success/failure
│
└─> Add to results (success or failed list)

3. Print summary
├─> Show success count
├─> Show failure count
├─> List successful backups
├─> List failed backups
└─> Exit with status code

Best Practices and Tips

Security Considerations

Avoid hardcoding passwords in scripts. Consider using one of the following approaches:

  • Environmental variables

  • Encrypted vaults (Ansible Vault, HashiCorp Vault)

  • Keyring libraries

  • Prompts for passwords at runtime

Example with environment variables:

devices = 
    {
        'device_type': 'alcatel_sros',
        'host': '172.20.20.13',
        'username': os.environ.get('NETWORK_USERNAME'),
        'password': os.environ.get('NETWORK_PASSWORD'),
  
    }

Error Handling

Always implement proper error handling:

  • Connection timeouts

  • Authentication failures

  • Device unreachable

  • Insufficient privileges

Logging

Comprehensive logging helps troubleshoot issues:

  • Connection attempts

  • Successful operations

  • Errors with full stack traces

  • Execution duration

Backup Retention

Implement a retention policy to manage disk space

Troubleshooting Common Issues

Issue 1: "Authentication failed"

Solution : Verify credentials, check if AAA is configured correctly

Issue 2: "Connection timeout"

Solution : Verify network connectivity, check firewall rules, ensure SSH is enabled

Issue 3: "Command not recognized"

Solution : Verify the backup command for your device type, some platforms use different commands

Issue 4: "Permission denied"

Solution : Ensure the user has proper privilege levels (Cisco: privilege 15, Juniper: superuser class)

Next Steps

Now that you have a functional backup script, consider implementing the following enhancements:

  1. Schedule automated backups using cron (Linux) or Task Scheduler (Windows)

  2. Add Git integration to track configuration changes over time

  3. Implement differential backups to store only changes

  4. Add email notifications for backup failures

  5. Extend to more device types (Palo Alto, Fortinet etc.)

Conclusion

Congratulations on building your first network automation tool. This script has the potential to save you significant time and serves as a foundation for more advanced automation tasks.

In next week's post, we will explore Network APIs and discuss how to manage network devices using modern API-first approaches on Juniper platforms.

Download the code

All the code from this post is available on GitLab. : network-automation-week-1-part2

Questions or Feedback?

Connect with me on LinkedIn. I'd love to hear about your automation journey!

Introduction to Network Automation: Build Your First Backup Script with Python for a multi-vendor environment - Part 2