Cisco SD-WANのルータのログを一括取得したい

# はじめに
2024年CiscoSystems合同会社社員有志によるAdventCalendarの記事の一つです。
https://qiita.com/advent-calendar/2024/cisco

# ご説明
以下のスクリプトを適当なvManageのホームディレクトリにおいてください。
#vi multi-hosts-commands.py
```python
import paramiko
import argparse
import time
import ipaddress
import concurrent.futures

def is_valid_ip(ip_address):
    try:
        ipaddress.IPv4Address(ip_address)
        return True
    except ipaddress.AddressValueError:
        return False

def connect_and_execute(router_ip, username, password, commands_file, output_filename):
    # Create an SSH client
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        # Connect to the router using the NETCONF port (port 830)
        ssh.connect(router_ip, port=830, username=username, password=password)

        # Start an interactive shell
        shell = ssh.invoke_shell()

        # Send a command to invoke the shell (e.g., 'shell' command)
        shell.send("shell\n")

        # Wait for the command to execute and receive the output
        time.sleep(1)  # Add a small delay to ensure the password prompt is received
        output = ""
        while shell.recv_ready():
            output += shell.recv(1024).decode()

        # Check if the password prompt is present
        if "password:" in output.lower():
            # Send the password again to authenticate
            shell.send(f"{password}\n")

        # Set terminal length to 0 to disable pagination
        shell.send("terminal length 0\n")

        # Wait for the command to execute and receive the output
        time.sleep(2)  # Add a small delay to ensure the command output is received
        output = ""
        while shell.recv_ready():
            output += shell.recv(1024).decode()

        # Read commands from the file and send them one by one
        with open(commands_file, "r") as file:
            for line in file:
                command = line.strip()
                shell.send(f"{command}\n")
                time.sleep(1)  # Add a small delay between commands

                # Receive and store the command output in the output_file
                command_output = ""
                while shell.recv_ready():
                    command_output += shell.recv(1024).decode()

                with open(output_filename, "a") as output_file:
                    output_file.write(command_output)

                time.sleep(1)  # Add a small delay to allow the router to respond with the final output

        # Close the SSH connection
        ssh.close()

    except paramiko.AuthenticationException:
        print(f"Authentication failed for router {router_ip}. Please check your credentials.")
    except paramiko.SSHException as ssh_ex:
        print(f"Error while connecting to router {router_ip}: {ssh_ex}")

if __name__ == "__main__":
    # Create argument parser
    parser = argparse.ArgumentParser(description="Connect to Cisco SD-WAN routers and execute commands.")
    parser.add_argument("hosts_file", help="The file containing the list of hosts (IP, username, password)")
    parser.add_argument("commands_file", help="The file containing the list of commands")
    args = parser.parse_args()

    # Read hosts file and execute commands for each valid router using parallel processing
    with open(args.hosts_file, "r") as hosts_file:
        host_lines = hosts_file.readlines()

    with concurrent.futures.ThreadPoolExecutor() as executor:
        futures = []
        for line in host_lines:
            router_ip, username, password = line.strip().split(",")

            # Check if the IP address is valid
            if not is_valid_ip(router_ip.strip()):
                print(f"Invalid IP address: {router_ip.strip()}. Skipping this host.")
                continue

            output_filename = f"output_{router_ip.strip()}.txt"
            future = executor.submit(connect_and_execute, router_ip.strip(), username.strip(), password.strip(), args.commands_file, output_filename)
            futures.append(future)

        # Wait for all futures to complete
        for future in concurrent.futures.as_completed(futures):
            future.result()

```


hosts.txtはログを取りたいホストの一覧で
Sytem-IPとユーザID、パスワードの順で記載したもの。
commands.txtは実行するコマンドです。

```bash
vmanage:~/python$ more hosts.txt
8.1.1.1,admin,admin
8.1.1.2,admin,admin
12.1.1.1,admin,admin
#8.1.1.3,admin,admin
#8.1.1.4,admin,admin
vmanage:~/python$ more commands.txt
show version | inc Version
show ip int bri
show ip route
show sdwan control connections
vmanage:~/python$ 
```
実行例
```bash
vmanage:~/python$ python3 multi-hosts-commands.py hosts.txt commands.txt
Invalid IP address: #8.1.1.3. Skipping this host.
Invalid IP address: #8.1.1.4. Skipping this host.
Authentication failed for router 12.1.1.1. Please check your credentials.
vmanage:~/python$ ls -ltr
total 60
-rw------- 1 admin admin    88 Aug  1  2023 commands.txt
-rw------- 1 admin admin  3986 Aug  1  2023 multi-hosts-commands.py
-rw------- 1 admin admin   103 Feb  8 15:31 hosts.txt
-rw------- 1 admin admin 21106 Feb  8 15:37 output_8.1.1.1.txt
-rw------- 1 admin admin 21106 Feb  8 15:37 output_8.1.1.2.txt
vmanage:~/python$ more output_8.1.1.1.txt
show version | inc Version
Cisco IOS XE Software, Version 17.11.01a
Cisco IOS Software [Dublin], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.11.1a, RELEASE SOFTWARE (fc3)
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
GPL code under the terms of GPL Version 2.0.  For more details, see the
c8kv-001#show ip int bri
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       unassigned      YES unset  up                    up
GigabitEthernet2       unassigned      YES unset  up                    up
GigabitEthernet2.100   unassigned      YES other  up                    up
GigabitEthernet2.200   unassigned      YES other  up                    up
GigabitEthernet3       192.168.1.2     YES other  up                    up
GigabitEthernet4       10.71.1xx.xx    YES DHCP   up                    up
Sdwan-system-intf      8.1.1.1         YES unset  up                    up
vmanage_system         unassigned      YES unset  up                    up  
```

<a id="#参考文献"></a>
# 参考文献
- [リリースノート](https://www.cisco.com/c/en/us/support/routers/sd-wan/products-release-notes-list.html)
- [マニュアルリンク](https://www.cisco.com/c/en/us/td/docs/routers/sdwan/config/ios-xe-sdwan17.html)