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
from netmiko import ConnectHandlerFirstly we import theConnectHandlerfrom thenetmikolibrary used to connect into network devicesfrom datetime import datetimeWe then importdatetimefrom thedatetimelibrary , to be able to work with time and datesimport oswhich is an operating system library used for working with files and foldersimport sysused for system-level operationsDevice Inventory: This is where we define all the devices we want to back up. The
devicesvariable holds our list of all devices. Inside the list, each device is represented by a dictionaryvendor_commandsA dictionary of dictionaries (nested dictionaries). Each vendor has its own set of commandsCreate a backup directory if it doesn’t exist.
backup_dir = 'backups'a variable is created namedbackup_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
Trueif it exists,Falseif it doesn'tThe Validation Function
def validate_config(config_text, vendor)checks ifconfig_textis empty or None and returns True if it is empty, and False if it has content. Theoroperator means if either condition is true, the entire expression is true.len(config_text) < 100Checks if config has fewer than 100 characters.returnreturns two values to the functionfalse= validation failed and"Configuration appears..."= Error Message .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..
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 fromdevice_info.Create connection parameters using
connection_params = {k: v for k, v in device_info.items() if k != 'vendor'}. Thedevice_info.items()retrieves pairs of (key, value), andfor k, v in ...loops through each pair. Ifk != 'vendor', it doesn't include the vendor, and{k: v ...}creates a new dictionary. Connect to the device.Retrieve the hostname for the filename and sanitize it by removing any characters that are not valid for filenames.
connection.send_command(commands['pager'], expect_string=commands['prompt'])used to turn off paginationThe
commands['config']is used to retrieve the configuration, andexpect_string=commands['prompt']specifies the prompt to wait for before continuing.is_valid, message = validate_config(config, vendor)calls the validation function. If the validation fails, it prints a warning.Create a timestamp and a vendor directory. Use
os.path.join()to combine folder paths.Create the filename using
filename = f"{vendor_dir}/{hostname}_{timestamp}.txt"and save the configuration.Verify file creation by checking the file size in bytes with
os.path.getsize(filename).Error Handling except ConnectionError as e : Catches only connection-related errors and except Exception as e : Catches any other error
The main function
main()orchestrates the entire backup process. It callsbackup_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:
Schedule automated backups using cron (Linux) or Task Scheduler (Windows)
Add Git integration to track configuration changes over time
Implement differential backups to store only changes
Add email notifications for backup failures
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!




