Introduction:
In many enterprise environments, network engineers often find themselves logging into multiple Cisco devices to perform repetitive tasks such as collecting configurations, checking logs, or verifying status. Doing this manually for each switch is time consuming.
To streamline this process, I’ve developed a Python script that automates SSH connections to multiple Cisco switches and executes specific CLI commands per switch. The output is saved into organized log files for documentation and further analysis.
Purpose:
Suppose we want to run a unique set of commands on multiple switches. This script allows us to do exactly that define separate commands per switch, connect via SSH, execute them, and save the results automatically.
Prerequisites:
Before you run the script, ensure the following are in place:
- Python 3.x installed on your system.
- You need to install the Python package
paramiko
, which is used to establish SSH connections.
To install it, run:
pip install paramiko
- Ensure SSH access is enabled on all Cisco switches.
- All switches should use the same local user credentials, or authentication via TACACS+/RADIUS should be enabled.
- A file named
commands.json
should exist in the same directory as the script, containing the list of switch IPs and their respective commands.
Switch IP with Predefibed Commnands(commands.json)
The script reads from a JSON file named commands.json
, which acts as a map of switch IPs to the commands that need to be executed on them. The structure looks like this:
{ "192.168.0.245": [ "terminal length 0", "enable", "show tech", "show version", "show running-config" ], "192.168.0.246": [ "terminal length 0", "enable", "show tech", "show interface status", "show ip int brief" ], "192.168.0.244": [ "terminal length 0", "enable", "show tech", "show logging", "show spanning-tree" ], "192.168.0.247": [ "terminal length 0", "enable", "show tech", "show logging", "show spanning-tree" ] }
Explanation:
- Each IP address is a key.
- The value is a list of Cisco CLI commands specific to that switch.
- This allows for flexibility, as different switches can have different sets of commands.
- Commands are executed in the order they appear in the list.
How the Script Works:
- Checks Directory: Validates whether the specified directory for output logs exists. If it doesn’t, you’ll be prompted to create it(Output will be save in this directory).
- Checks Reachability: Pings each switch to confirm it’s reachable before attempting a connection.
- Prompts for Credentials: Asks the user for SSH login credentials (username and password).
- Establishes SSH Connection: Logs into each switch using
paramiko
and invokes a shell session. - Runs Commands: The list of commands specified for each switch is executed sequentially.
- Saves Output: The combined output of all commands is saved to a timestamped
.txt
file named after the switch IP. - Displays Summary: At the end, a summary shows how many switches were processed successfully or failed.
Full Python Script:
import paramiko import time import getpass import datetime import os import json import subprocess # For improved ping handling def check_switch(ip): # Execute the ping command and capture the full output ping_result = subprocess.run( ["ping", "-n", "4", ip], capture_output=True, text=True ) output = ping_result.stdout print(output) # Show full ping output to console # Check for failure keywords in output failure_signs = ["unreachable", "timed out", "100% loss"] if any(sign.lower() in output.lower() for sign in failure_signs): return False return True def ensure_directory_exists(directory): if not os.path.exists(directory): user_input = input(f"Directory '{directory}' does not exist. Do you want to create it? (y/n): ").strip().lower() if user_input == 'y': os.makedirs(directory) print(f"Directory '{directory}' created successfully.") else: print(f"Directory '{directory}' was not created.") else: print(f"Directory '{directory}' already exists.") def load_commands(file_path): with open(file_path, "r") as file: return json.load(file) def run_commands_on_switch(ip_address, commands, username, password, output_dir): try: ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh_client.connect(hostname=ip_address, username=username, password=password) print(f"Successful connection to {ip_address}") remote_connection = ssh_client.invoke_shell() time.sleep(1) # Accumulate output for all commands output = [] for command in commands: remote_connection.send(command + "\n") time.sleep(2) # Adjust if needed, depending on the expected command execution time raw_output = remote_connection.recv(655350).decode("utf-8", errors="ignore") output.append(raw_output) # Save all accumulated output after all commands have been executed now = datetime.datetime.now() date_str = now.strftime("%d-%m-%Y") time_str = now.strftime("%H%M%S") fname = os.path.join(output_dir, f"{ip_address}_{date_str}_{time_str}.txt") with open(fname, "w") as saveoutput: saveoutput.write("".join(output)) # Write all accumulated output to file print(f"Output saved for {ip_address}: {fname}") ssh_client.close() return True except Exception as e: print(f"An error occurred while connecting to {ip_address}: {e}") return False def main(): my_dir = "C:\\Python\\Backup\\Test2024" ensure_directory_exists(my_dir) command_file = "commands.json" command_mapping = load_commands(command_file) username = input("Enter Username: ") password = getpass.getpass("Enter Password: ") total_processed = 0 successful = 0 failed = 0 for ip_address, commands in command_mapping.items(): if not check_switch(ip_address): print(f"{ip_address} is not reachable. Skipping...\n") failed += 1 continue print(f"Processing {ip_address} with commands: {commands}") total_processed += 1 if run_commands_on_switch(ip_address, commands, username, password, my_dir): successful += 1 else: failed += 1 print("\n--- Summary ---") print(f"Total switches processed: {total_processed}") print(f"Successful connections: {successful}") print(f"Failed connections: {failed}") if __name__ == "__main__": main()
Result:
D:\Python3>python.exe ciscotest.py Directory 'C:\Python\Backup\Test2024' already exists. Enter Username: admin Enter Password: Pinging 192.168.0.245 with 32 bytes of data: Reply from 192.168.0.245: bytes=32 time=1ms TTL=255 Reply from 192.168.0.245: bytes=32 time=1ms TTL=255 Reply from 192.168.0.245: bytes=32 time=1ms TTL=255 Reply from 192.168.0.245: bytes=32 time=1ms TTL=255 Ping statistics for 192.168.0.245: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 1ms, Maximum = 1ms, Average = 1ms Processing 192.168.0.245 with commands: ['terminal length 0', 'enable', 'show tech', 'show version', 'show running-config'] Successful connection to 192.168.0.245 Output saved for 192.168.0.245: C:\Python\Backup\Test2024\192.168.0.245_11-05-2025_201100.txt Pinging 192.168.0.246 with 32 bytes of data: Reply from 192.168.0.246: bytes=32 time=2ms TTL=255 Reply from 192.168.0.246: bytes=32 time=1ms TTL=255 Reply from 192.168.0.246: bytes=32 time=1ms TTL=255 Reply from 192.168.0.246: bytes=32 time=4ms TTL=255 Ping statistics for 192.168.0.246: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 1ms, Maximum = 4ms, Average = 2ms Processing 192.168.0.246 with commands: ['terminal length 0', 'enable', 'show tech', 'show interface status', 'show ip int brief'] Successful connection to 192.168.0.246 Output saved for 192.168.0.246: C:\Python\Backup\Test2024\192.168.0.246_11-05-2025_201114.txt Pinging 192.168.0.244 with 32 bytes of data: Reply from 192.168.0.62: Destination host unreachable. Reply from 192.168.0.62: Destination host unreachable. Reply from 192.168.0.62: Destination host unreachable. Reply from 192.168.0.62: Destination host unreachable. Ping statistics for 192.168.0.244: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), 192.168.0.244 is not reachable. Skipping... Pinging 192.168.0.247 with 32 bytes of data: Reply from 192.168.0.247: bytes=32 time=2ms TTL=255 Reply from 192.168.0.247: bytes=32 time<1ms TTL=255 Reply from 192.168.0.247: bytes=32 time<1ms TTL=255 Reply from 192.168.0.247: bytes=32 time<1ms TTL=255 Ping statistics for 192.168.0.247: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 0ms, Maximum = 2ms, Average = 0ms Processing 192.168.0.247 with commands: ['terminal length 0', 'enable', 'show tech', 'show logging', 'show spanning-tree'] Successful connection to 192.168.0.247 Output saved for 192.168.0.247: C:\Python\Backup\Test2024\192.168.0.247_11-05-2025_201139.txt --- Summary --- Total switches processed: 3 Successful connections: 3 Failed connections: 1
Output Logs:
Conclusion:
This Python script offers a simple and efficient way to manage multiple Cisco switches by automating command execution and saving outputs for future reference. One of its key advantages is the ability to run different sets of commands on different switches, based on what is defined in the commands.json
file.