実践ネット自動化スクリプト集

現場で役立つPythonネットワーク自動化:異なるベンダー機器のコマンド差分吸収術

Tags: Python, ネットワーク自動化, Netmiko, TextFSM, NTC-Templates, CLI

はじめに

近年、システムインフラの自動化は標準的なプラクティスとなり、ネットワーク領域においても自動化の重要性が高まっています。しかし、ネットワーク機器はベンダーごとにコマンド体系や出力形式が大きく異なるため、自動化スクリプトを作成する上で大きな課題となります。特に、複数のベンダー機器を混在して利用している環境では、この課題が顕著になります。

Pythonを使ったネットワーク自動化に関心はあるものの、ベンダーごとの差異に対応する知識や経験が限定的であると感じている方もいらっしゃるかもしれません。本記事では、このような課題に対し、Pythonを活用して異なるベンダー機器のコマンドや出力形式の差分を吸収し、汎用的な自動化スクリプトを作成する方法をご紹介します。具体的には、Netmikoによる機器接続とコマンド実行、そしてCLI出力を構造化データに変換するTextFSM/NTC-Templatesの活用に焦点を当てて解説します。

ネットワーク自動化におけるベンダー差分の課題

ネットワーク機器の操作は、主にCLI(コマンドラインインターフェース)を通じて行われます。しかし、例えば「インターフェースの状態を表示する」という単純な操作であっても、ベンダーAでは show ip interface brief、ベンダーBでは show interfaces terse といったように、実行すべきコマンドが異なります。さらに、そのコマンドの出力形式もベンダーごとに全く異なります。

自動化スクリプトを、ベンダーごとに異なるコマンド文字列や、複雑な正規表現を駆使して出力パース処理を記述する方法で作成すると、以下の問題が発生します。

これらの課題を克服し、より効率的でメンテナンスしやすい自動化を実現するためには、ベンダー差分を吸収する仕組みが必要です。

PythonとNetmikoによる機器接続・コマンド実行の共通化

Pythonでネットワーク機器と通信するための強力なライブラリの一つに Netmiko があります。Netmikoは、Paramiko(SSHv2)やTelnetLibなどのライブラリをラップし、Cisco、Juniper、Arista、FortiGate、Palo Alto Networksなど、多くのベンダー機器に対するSSHやTelnet接続、コマンド実行を共通のインターフェースで提供します。

Netmikoを使用することで、ベンダーごとの接続方法や基本的なコマンド実行ロジックの違いを吸収できます。

from netmiko import ConnectHandler
import yaml # 設定情報を外部ファイルから読み込む例

# 接続先デバイスの設定(例: devices.yaml)
"""
devices:
  - name: router1
    device_type: cisco_ios
    host: 192.168.1.1
    username: admin
    password: password123
  - name: switch1
    device_type: juniper_junos
    host: 192.168.1.2
    username: admin
    password: password123
"""

# devices.yaml ファイルから設定を読み込む
try:
    with open("devices.yaml", 'r') as f:
        config = yaml.safe_load(f)
    devices = config['devices']
except FileNotFoundError:
    print("Error: devices.yaml not found.")
    exit()
except KeyError:
    print("Error: 'devices' key not found in devices.yaml.")
    exit()
except Exception as e:
    print(f"Error loading devices.yaml: {e}")
    exit()


for device in devices:
    print(f"--- Connecting to {device['name']} ({device['host']}) ---")
    try:
        # ConnectHandlerがデバイスタイプに応じて適切なドライバーを選択
        with ConnectHandler(**device) as net_connect:
            # 各デバイスタイプに対応したコマンドを実行(ここではまだベンダー固有コマンド)
            if device['device_type'] == 'cisco_ios':
                command = "show ip interface brief"
            elif device['device_type'] == 'juniper_junos':
                command = "show interfaces terse"
            else:
                print(f"Unsupported device type: {device['device_type']}")
                continue

            print(f"Executing command: {command}")
            output = net_connect.send_command(command)

            print("--- Output ---")
            print(output)
            print("-" * 20)

    except Exception as e:
        print(f"Connection or command execution failed for {device['name']}: {e}")

上記のコード例では、Netmikoを使用してCisco IOSとJuniper Junosの機器に接続し、それぞれのベンダー固有のコマンドを実行しています。Netmikoは接続部分を抽象化してくれますが、実行するコマンド自体と、その出力形式はまだベンダーに依存しています。次のステップでは、この出力形式の差分を吸収する方法について解説します。

CLI出力の構造化:TextFSMとNTC-Templatesの活用

ベンダーごとに異なるCLI出力形式の差分を吸収し、プログラムで扱いやすい共通の構造化データ(リストや辞書)に変換するために、TextFSMというGoogleが開発したオープンソースライブラリが非常に有効です。

TextFSMは、CLI出力のパースルールを定義した「テンプレートファイル」に基づいて、非構造化なテキストデータ(CLI出力)を構造化データに変換します。そして、そのTextFSMテンプレートを多くのベンダー/OS/コマンドに対して網羅的に作成し、コミュニティで共有されているのが NTC-Templates です。NetmikoはこのNTC-Templatesと連携する機能を持っています。

Netmikoの send_command メソッドに use_textfsm=True オプションを付与すると、Netmikoは実行したコマンドに対応するNTC-Templatesのテンプレートを自動的に探し出し、TextFSMを使って出力をパースし、構造化されたPythonオブジェクト(通常は辞書のリスト)として返してくれます。これにより、ベンダーごとの出力形式の違いを意識することなく、共通のデータ構造として扱うことが可能になります。

必要なライブラリをインストールします。

pip install netmiko textfsm ntc-templates

次に、TextFSM/NTC-Templatesを活用したコード例を示します。

from netmiko import ConnectHandler
import yaml
# NTC-Templatesのパスを設定するために必要なライブラリ
import os
from ntc_templates.parse import parse_output

# devices.yaml ファイルから設定を読み込む(上記の例と同じ)
try:
    with open("devices.yaml", 'r') as f:
        config = yaml.safe_load(f)
    devices = config['devices']
except FileNotFoundError:
    print("Error: devices.yaml not found.")
    exit()
except KeyError:
    print("Error: 'devices' key not found in devices.yaml.")
    exit()
except Exception as e:
    print(f"Error loading devices.yaml: {e}")
    exit()

# TextFSMテンプレートディレクトリのパスを設定 (netmiko/ntc-templates >= 2.4.0 で自動的にパスが設定されるようになったため、基本的には不要ですが、明示的に設定する場合は以下のコメントアウトを外します)
# os.environ['NET_TEXTFSM'] = '/path/to/ntc-templates/templates' # NTC-Templatesのリポジトリをcloneしたパスを指定

for device in devices:
    print(f"--- Connecting to {device['name']} ({device['host']}) ---")
    try:
        with ConnectHandler(**device) as net_connect:
            # ベンダー固有のコマンドを実行
            if device['device_type'] == 'cisco_ios':
                command = "show ip interface brief"
            elif device['device_type'] == 'juniper_junos':
                command = "show interfaces terse"
            else:
                print(f"Unsupported device type: {device['device_type']}")
                continue

            print(f"Executing command: {command}")

            # Netmikoのsend_commandでTextFSMパースを有効化
            # device_typeとcommandに基づいて、NTC-Templatesから適切なテンプレートが自動的に使用される
            output_structured = net_connect.send_command(command, use_textfsm=True)

            print("--- Structured Output ---")
            # 構造化されたデータは通常リスト形式で返される
            if output_structured:
                for interface in output_structured:
                    print(f"  Interface: {interface.get('intf', 'N/A')}, "
                          f"IP Address: {interface.get('ipaddr', 'N/A')}, "
                          f"Status: {interface.get('status', 'N/A')}")
            else:
                 # use_textfsm=Trueでもパースできなかった場合は元の文字列が返されることがある
                 # または、対応するテンプレートが存在しない、あるいは出力がテンプレートと一致しない場合
                print("Could not parse output using TextFSM templates.")
                # デバッグ用に生の出力も表示
                # print("--- Raw Output ---")
                # print(net_connect.send_command(command, use_textfsm=False))

            print("-" * 20)

    except Exception as e:
        print(f"Connection or command execution failed for {device['name']}: {e}")

このコードを実行すると、Cisco IOSとJuniper Junosから取得したインターフェース情報が、TextFSM/NTC-Templatesによってパースされ、共通の辞書のリスト形式で output_structured 変数に格納されます。これにより、以降のデータ処理ロジックは、ベンダーごとの出力形式の違いを意識することなく、共通のキー(例: intf, ipaddr, status)を参照して記述できます。

例えば、特定のIPアドレスを持つインターフェースを探す、すべてのインターフェースの状態をチェックするといった処理は、取得した output_structured リストに対して行うだけで済みます。

設定投入におけるコマンド差分への対応

情報取得だけでなく、設定投入においてもベンダーごとにコマンドは異なります。Netmikoには send_config_set メソッドがあり、設定コマンドのリストを一括で投入できますが、投入するコマンドリスト自体はベンダーごとに作成する必要があります。

設定投入時のベンダー差分を吸収するアプローチとしては、以下のようなものが考えられます。

  1. 設定テンプレートの活用:
    • Jinja2などのテンプレートエンジンを使用し、共通の設定データ(変数)とベンダー固有のコマンドテンプレートを組み合わせる方法です。例えば、NTPサーバーのIPアドレスを共通変数とし、Cisco用のテンプレートでは ntp server {{ ntp_server_ip }}、Juniper用では set system ntp server {{ ntp_server_ip }} といったテンプレートを用意します。実行時に共通データとベンダー固有テンプレートを組み合わせて、機器に投入する最終的な設定コマンドリストを生成します。
  2. 抽象化ライブラリ/フレームワークの利用:
    • NAPALMやNornirといった、さらに高レベルの抽象化を提供するライブラリ/フレームワークを利用する方法です。これらのツールは、特定の操作(例: VLANの作成、BGPピアの設定)を共通のデータモデルで定義し、そのデータモデルを基に、裏側で各ベンダー機器に適切なコマンドやAPIコールを実行します。これにより、ユーザーはベンダー固有のコマンドを意識することなく、ネットワーク機器の状態を定義できます。

簡単な設定であれば、Pythonの条件分岐でコマンドリストを切り替えることも可能ですが、複雑な設定や多数の機器に対応する場合は、テンプレートや高レベル抽象化ツールの導入を検討することが推奨されます。

実践的な考慮事項

現場で実際にこれらのスクリプトを運用する際には、いくつかの実践的な考慮事項があります。

インフラ自動化パイプラインへの組み込み

Pythonによるネットワーク自動化スクリプトは、独立して実行するだけでなく、より広範なインフラ自動化パイプラインの一部として組み込むことで、その価値を最大限に発揮します。

このように、Pythonで記述されたネットワーク自動化スクリプトは、既存の自動化ワークフローやツール群と柔軟に連携できるため、IaCやCI/CDの推進において非常に強力な要素となります。

まとめ

本記事では、Pythonを使用して異なるベンダーのネットワーク機器におけるコマンドや出力形式の差分を吸収し、ネットワーク自動化スクリプトをより効率的に作成する方法について解説しました。

CLIからの自動化は、APIが提供されていない機器や、特定の情報に素早くアクセスしたい場合に非常に有効な手段です。本記事で紹介した手法が、皆様の現場におけるネットワーク自動化の推進の一助となれば幸いです。

将来的には、多くのネットワーク機器でAPI(RESTConf, NETConfなど)の利用が広まることが期待されますが、現状ではCLIからの自動化も依然として重要なスキルセットです。ぜひ、ここで紹介したPythonライブラリや概念を活用して、日々の運用業務の効率化に取り組んでみてください。