pythonを使ってCisco IOS機器のバックアップを取得してみた

# はじめに
お勉強のためにpythonの以下ライブラリを使ってCisco IOS機器を操作してみました。
機器情報の取得やコンフィグ投入等、多様な操作ができることがわかりましたが、今回はそれぞれのライブラリを使ってバックアップを取得する（`show run`の結果を取得してファイルに保存する）スクリプトを作成したので参考までに共有します。


# 使用するライブラリ
- [Paramiko](https://www.paramiko.org/)
- [Netmiko](https://github.com/ktbyers/netmiko)
- [Napalm](https://napalm.readthedocs.io/en/latest/base.html)


# 前提
- 実行環境（Cisco IOSに接続するクライアント）：`ubuntu 20.04`
- pythonバージョン：`3.8.10`
- 実行環境上で各種ライブラリをインストールします。

```
pip install paramiko netmiko napalm
```

- 以下図の`Ubuntu`からIOUルータ３台のバックアップを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/43e43cd2-ce0a-bfad-8c68-a691b976b0c7.png)

# paramiko
paramikoの汎用モジュール（`paramiko_mod.py`）と、
バックアップを実行するメインファイル(`paramiko_ios_backup_executer.py`)の２部構成です。

### ①paramikoの汎用モジュール

```python:paramiko_mod.py
import paramiko
import time

from paramiko.client import AutoAddPolicy

def connect(server_ip: str, server_port: str, user: str, password: str): 
    '''SSHサーバに接続'''
    ssh_client = paramiko.SSHClient()
    ssh_client.set_missing_host_key_policy(AutoAddPolicy())
    print(f'Connecting to {server_ip}')
    ssh_client.connect(
        hostname=server_ip, 
        port=server_port,
        username=user,
        password=password,
        look_for_keys=False,
        allow_agent=False
    )
    return ssh_client

def get_shell(ssh_client):
    '''対話型シェルを返す'''
    shell = ssh_client.invoke_shell()
    return shell

def send_command(shell, command: str, timeout=1):
    '''コマンドを実行'''
    print(f'Sending command: {command}')
    shell.send(command + '\n')
    time.sleep(timeout)

def show(shell, n=10000):
    '''結果をデコードして返す'''
    # 結果をバイト列で取得
    output = shell.recv(n)
    return output.decode()

def close(ssh_client):
    '''SSH接続を閉じる'''
    if ssh_client.get_transport().is_active() == True:
        print('Closing the connection')
        ssh_client.close()
```
- SSH認証方式はユーザ認証としています。公開鍵認証、SSHエージェントも利用できるようです。
- `send_command(shell, command: str, timeout=1):`
    - コマンド結果が指定秒以内に返って来ない場合があるため、`timeout`値は呼び出し時に変更できるようにしています。

### ②バックアップを実行するメインファイル(paramiko)
```python:paramiko_ios_backup_executer.py
import os
import datetime
import threading
import paramiko_mod
from datetime import datetime

def backup(router):
    # sshクライアントを生成
    client = paramiko_mod.connect(**router)

    # 対話型シェルを呼び出す
    shell = paramiko_mod.get_shell(client)
    
    # ssh接続先で実行するコマンドをシェルに渡す
    paramiko_mod.send_command(shell, 'enable')
    paramiko_mod.send_command(shell, '######') # enableパスワード
    paramiko_mod.send_command(shell, 'terminal length 0')
    
    # sh runの結果が取得できない場合はtimeout値を調整
    paramiko_mod.send_command(shell, 'sh run', timeout=3)     

    # 結果を取得&リストに整形
    output = paramiko_mod.show(shell)
    
    # [show run]コマンド 実行結果の不要な箇所を削除するため、一度リストに変換する
    output_list = output.splitlines()
    output_list = output_list[11:-1]

    # リストを文字列に戻す 
    output = '\n'.join(output_list)
    print(output)

    # バックアップ先のディレクトリがなければ作成
    backup_dir = f'backup/{router["server_ip"]}'
    if not os.path.exists(backup_dir):
        os.makedirs(backup_dir)

    # 結果をバックアップファイルに書き出す
    bk_file_name = f'{router["server_ip"]}_{now.year}{now.month}{now.day}'
    with open(f'{backup_dir}/{bk_file_name}', 'w') as f:
        f.write(output)

    paramiko_mod.close(client)

router1 = {
    'server_ip': '192.168.15.40',
    'server_port': '22',
    'user': 'XXXXXXX',
    'password': 'XXXXXXX'
}
router2 = {
    'server_ip': '192.168.15.41',
    'server_port': '22',
    'user': 'XXXXXXX',
    'password': 'XXXXXXX'
}
router3 = {
    'server_ip': '192.168.15.42',
    'server_port': '22',
    'user': 'XXXXXXX',
    'password': 'XXXXXXX'
}

routers = [router1, router2, router3]

if __name__ == '__main__':

    now = datetime.now()
    
    # バックアップ関数をスレッドリストに詰める
    threads = [threading.Thread(target=backup, args=(router,)) for router in routers]

    # マルチスレッドでバックアップ開始
    for th in threads:
        th.start()

    # バックアップがすべて終わるまで待機
    for th in threads:
        th.join()

```
- 本スクリプトでは、3台のCiscoルータのバックアップ処理をマルチスレッドで実行しています。
- バックアップファイルはカレントディレクトの`backup/{router["server_ip"]}`配下に保存される仕様です。
- パスワードやユーザ情報はハードコーディングしていますが、環境変数や別の設定ファイル(ini, yml等)に分ける等の実装がよいと思います。（対象機器の台数が多くなった場合の設定管理の意味も含めて）

# netmiko
機器の設定情報を記述したjsonファイル（`target_device_info.json`）と、
バックアップを実行するメインファイル(`netmiko_ios_backup_executer.py`)の２部構成です。

### ①機器の設定情報を記述したjsonファイル
```json:target_device_info.json
[
    {
        "host": "192.168.15.40",
        "port": "22", 
        "username": "XXXXX", 
        "password": "XXXXX", 
        "device_type": "cisco_ios",
        "secret": "XXXXX",
        "verbose": "True"
    },
    {
        "host": "192.168.15.41",
        "port": "22", 
        "username": "XXXXX", 
        "password": "XXXXX", 
        "device_type": "cisco_ios",
        "secret": "XXXXX",
        "verbose": "True"
    },
    {
        "host": "192.168.15.42",
        "port": "22", 
        "username": "XXXXX", 
        "password": "XXXXX", 
        "device_type": "cisco_ios",
        "secret": "XXXXX",
        "verbose": "True"
    }
]
```

### ②バックアップを実行するメインファイル(netmiko)
```python:paramiko_ios_backup_executer.py
from datetime import datetime
import os
import netmiko
import json
import threading

def backup(device):
    print(f'Connect {device["host"]}')
    connection = netmiko.ConnectHandler(**device)

    connection.enable()

    output = connection.send_command('show run')

    # [show run]コマンド 実行結果の不要な箇所を削除するため、一度リストに変換する
    output_list = output.splitlines()
    output_list = output_list[11:-1]
    # リストを文字列に戻す
    output = '\n'.join(output_list)

    # バックアップ先のディレクトリがなければ作成
    backup_dir = f'backup/{device["host"]}'
    if not os.path.exists(backup_dir):
        os.makedirs(backup_dir)

    # 結果をバックアップファイルに書き出す
    now = datetime.now()
    bk_file_name = f'{device["host"]}_{now.year}{now.month}{now.day}'
    with open(f'{backup_dir}/{bk_file_name}', 'w') as f:
        f.write(output)

    print(f'Disconnect {device["host"]}')
    connection.disconnect() 

if __name__ == "__main__":

    # 接続機器の設定情報が記述されたjsonを読み込む
    with open(r"target_device_info.json", "r") as f:
        device_info_list = json.load(f)

    threads = list()
    for info in device_info_list:

        # バックアップ関数をスレッドリストに詰める
        thread = threading.Thread(target=backup, args=(info,))
        threads.append(thread)

    # マルチスレッドでバックアップを実行
    for th in threads:
      th.start()

    # すべてのバックアップ処理が完了するまで待機
    for th in threads:
      th.join()
```
- バックアップ対象機器の情報を設定ファイルから読み込む仕様です。
- paramikoのバックアップスクリプトと同様に処理をマルチスレッドで実行しています。
- バックアップファイルはカレントディレクトの`backup/{device["host"]}`配下に保存される仕様です。


# napalm
netmikoと同じく、機器の設定情報を記述したjsonファイル（`target_device_info.json`）と、
バックアップを実行するメインファイル(`napalm_ios_backup_executer.py`)の２部構成です。

### ①機器の設定情報を記述したjsonファイル
```json:target_device_info.json
[
    {
        "host": "192.168.15.40",
        "username": "admin1", 
        "password": "cisco1", 
        "device_type": "ios",
        "secret": "cisco1"
    },
    {
        "host": "192.168.15.41",
        "username": "admin1", 
        "password": "cisco1", 
        "device_type": "ios",
        "secret": "cisco1"
    },
    {
        "host": "192.168.15.42",
        "username": "admin1", 
        "password": "cisco1", 
        "device_type": "ios",
        "secret": "cisco1"
    }
]
```

### ②バックアップを実行するメインファイル(napalm)
```python:napalm_ios_backup_executer.py
from datetime import datetime
import os
from napalm import get_network_driver
import json
import threading

def backup(device):
    print(f'Connect {device["host"]}')

    # 接続設定を読み込む
    driver = get_network_driver(device["device_type"])
    option_args = {'secret': device["secret"]}
    ios = driver(
        device["host"], device["username"], device["password"], optional_args=option_args)

    # 接続    
    ios.open()

    # sh run の結果を取得    
    output = ios.get_config()['running']
    print(output)

    # バックアップ先のディレクトリがなければ作成
    backup_dir = f'backup/{device["host"]}'
    if not os.path.exists(backup_dir):
        os.makedirs(backup_dir)

    # 結果をバックアップファイルに書き出す
    now = datetime.now()
    bk_file_name = f'{device["host"]}_{now.year}{now.month}{now.day}'
    with open(f'{backup_dir}/{bk_file_name}', 'w') as f:
        f.write(output)

    print(f'Disconnect {device["host"]}')

    ios.close()

if __name__ == "__main__":

    # 接続機器の設定情報が記述されたjsonを読み込む
    with open(r"target_device_info.json", "r") as f:
        device_info_list = json.load(f)

    threads = list()
    for info in device_info_list:

        # バックアップ関数をスレッドリストに詰める
        thread = threading.Thread(target=backup, args=(info,))
        threads.append(thread)

    # マルチスレッドでバックアップを実行
    for th in threads:
        th.start()

    # すべてのバックアップ処理が完了するまで待機
    for th in threads:
        th.join()

```