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

現場で役立つPython:ネットワーク経路情報の自動収集と可視化スクリプト

Tags: Python, ネットワーク自動化, ルーティング, Netmiko, Genie Parser, 可視化

はじめに

システム運用やインフラ管理において、ネットワーク経路情報は非常に重要な要素です。サービスの疎通確認、パフォーマンス問題の調査、セキュリティポリシーの確認など、様々な場面でネットワーク機器のルーティングテーブルや実際のパケットが通過する経路(traceroute結果など)を参照する必要があります。

これらの情報の収集は、通常ネットワーク機器にログインしてCLIコマンドを実行することで行われます。しかし、対象機器が多数にわたる場合や、頻繁に状態を確認する必要がある場合、手動での作業は非効率であり、ミスが発生するリスクも高まります。

Pythonを活用することで、これらのネットワーク経路情報の収集と解析、さらにはその後の可視化に向けたデータ準備を自動化できます。本記事では、Pythonを用いてネットワーク機器から経路情報を自動収集し、扱いやすい構造化データとして解析する基本的な手法と、簡単な活用例としての可視化への連携について、具体的なスクリプト例を交えて解説します。

なぜネットワーク経路情報の自動化が必要か

システム開発やクラウドインフラの自動化に慣れているエンジニアにとって、ネットワーク機器への操作は馴染みが薄いかもしれません。しかし、アプリケーションやサービスが稼働する上で、ネットワークの挙動は常に考慮すべき課題です。

これらの作業を自動化することで、作業時間の短縮、ヒューマンエラーの削減、そしてより迅速かつ正確な情報に基づいた判断が可能になります。

Pythonによるネットワーク経路情報の収集手法

Pythonでネットワーク機器から経路情報を収集するには、いくつかの方法があります。代表的なのはCLIベースのアプローチとAPIベースのアプローチです。対象読者のスキルセットと現場での利用シーンを考慮し、本記事ではCLIベースのアプローチに焦点を当てます。多くの現場で既存のネットワーク機器がCLI操作に対応しており、Pythonistaにとって最も始めやすい方法の一つであるためです。

1. CLI経由でのコマンド実行

これは最も一般的な方法です。SSHを使用してネットワーク機器に接続し、show ip route, show route, show ip bgp summary, traceroute といったCLIコマンドを実行してその出力結果を取得します。

2. CLI出力のパース(構造化データへの変換)

CLIコマンドの出力は人間が読むには適していますが、Pythonスクリプトで機械的に処理するには不向きな非構造化テキストです。これを解析(パース)し、扱いやすいJSONやPythonの辞書のような構造化データに変換する必要があります。

対象読者はPythonスキルが高いことから、CLI出力のパース部分に焦点を当てることで、既存のPythonスキルを活かせるイメージを持ってもらいやすくなります。ここではnetmikoでコマンドを実行し、genie.libs.parserでパースする例を中心に紹介します。Genie Parserは特定のコマンドに対して非常に高品質なパース結果を提供するため、お勧めです。

実践スクリプト例:ルーティングテーブルの収集と解析

ここでは、netmikoを使用してネットワーク機器のshow ip routeコマンドを実行し、genie.libs.parser(PyATSの一部)でその出力をパース、特定の情報を抽出するスクリプト例を示します。

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

pip install netmiko pyats.libs

次に、スクリプトコードです。

import json
from netmiko import ConnectHandler
from genie.libs.parser.iosxe.show_routing import ShowIpRoute

# 機器接続情報(実際の環境に合わせて修正してください)
device = {
    'device_type': 'cisco_ios', # 使用する機器のベンダータイプ
    'host': 'your_device_ip',
    'username': 'your_username',
    'password': 'your_password',
    # 'secret': 'your_enable_password', # 特権EXECモードに移行する場合
    'port': 22,
}

def get_and_parse_route_info(device_params: dict, target_prefix: str = None):
    """
    ネットワーク機器からルーティングテーブルを取得し、解析する関数

    Args:
        device_params (dict): Netmiko接続パラメータ
        target_prefix (str, optional): 抽出したい特定のネットワークプレフィックス. Defaults to None.

    Returns:
        dict or list or None: 解析されたルーティング情報。target_prefix指定時はリスト、指定なし時は全情報。
                               取得/解析に失敗した場合はNone。
    """
    try:
        # NetmikoでSSH接続
        print(f"Connecting to {device_params['host']}...")
        with ConnectHandler(**device_params) as net_connect:
            # 特権EXECモードに移行(必要に応じて)
            # net_connect.enable()

            # 'show ip route' コマンド実行
            print("Executing 'show ip route'...")
            cli_output = net_connect.send_command('show ip route')
            print("Command executed successfully.")

        # Genie ParserでCLI出力をパース
        # Parserクラスのインスタンスを作成し、parse()メソッドを呼び出す
        # 引数としてCLI出力を渡す
        parser = ShowIpRoute(device_params['device_type']) # device_typeはparser初期化に必要
        parsed_output = parser.parse(cli_output)

        print("CLI output parsed successfully.")

        # 解析結果はネストされた辞書構造になる。
        # 例えば、IPv4のRIB情報は parsed_output['vrf']['default']['address_family']['ipv4']['routes'] に格納されている。

        # 特定のプレフィックスを抽出、または全情報を返す
        if target_prefix:
            print(f"Extracting information for prefix: {target_prefix}")
            extracted_routes = []
            # Genie Parserの出力構造は複雑なため、実際の出力を見てパスを確認する必要があります。
            # ここでは一般的なパスを例として使用しますが、機器/OSバージョンにより異なる場合があります。
            routes = parsed_output.get('vrf', {}).get('default', {}).get('address_family', {}).get('ipv4', {}).get('routes', {})
            for prefix, route_info in routes.items():
                 if prefix == target_prefix:
                     extracted_routes.append({prefix: route_info}) # 見つかったルート情報をリストに追加
            print(f"Found {len(extracted_routes)} routes for {target_prefix}.")
            return extracted_routes if extracted_routes else None
        else:
            # 全ルーティング情報を返す
            return parsed_output.get('vrf', {}).get('default', {}).get('address_family', {}).get('ipv4', {}).get('routes', {})

    except Exception as e:
        print(f"An error occurred: {e}")
        return None

# スクリプト実行部分
if __name__ == "__main__":
    # 特定のプレフィックスを検索する場合
    # target_prefix = '192.168.1.0/24'
    # route_info = get_and_parse_route_info(device, target_prefix=target_prefix)

    # 全ルーティング情報を取得する場合
    route_info = get_and_parse_route_info(device)


    if route_info:
        print("\n--- Parsed Route Information ---")
        # 取得した情報をJSON形式で整形して表示
        print(json.dumps(route_info, indent=4))

        # 特定の情報を抽出する例(全情報取得時)
        if not target_prefix and isinstance(route_info, dict):
            print("\n--- Extracting sample info (e.g., next-hops for direct routes) ---")
            for prefix, info in route_info.items():
                if 'directly_connected' in info and info['directly_connected']:
                     print(f"Prefix: {prefix}, Directly Connected Via: {info.get('next_hop', {}).get('outgoing_interface')}")
                elif 'next_hop' in info:
                     # ネクストホップが複数ある場合もあるためリストとして扱う
                     next_hops = info['next_hop']
                     if isinstance(next_hops, list):
                          for nh in next_hops:
                                print(f"Prefix: {prefix}, Next Hop: {nh.get('next_hop_list', [{}])[0].get('next_hop')}, Interface: {nh.get('next_hop_list', [{}])[0].get('outgoing_interface')}")
                     elif isinstance(next_hops, dict):
                         # Single next-hop case might be dictionary depending on parser output structure
                         print(f"Prefix: {prefix}, Next Hop: {next_hops.get('next_hop_list', [{}])[0].get('next_hop')}, Interface: {next_hops.get('next_hop_list', [{}])[0].get('outgoing_interface')}")


    else:
        print("\nFailed to retrieve or parse route information.")

コードの解説:

  1. インポート: netmikoで機器接続、jsonで結果表示整形、genie.libs.parser.iosxe.show_routing.ShowIpRouteでCisco IOS XEのshow ip routeパーサーをインポートしています。
  2. 機器接続情報: device辞書に接続に必要な情報を定義します。device_typeはNetmikoだけでなくGenie Parserでも機器のOSタイプを識別するために重要です。実際の環境に合わせてIPアドレス、認証情報などを修正してください。
  3. get_and_parse_route_info関数:
    • ConnectHandler(**device_params)でNetmikoによるSSH接続を確立します。with構文を使うことで、接続が自動的に閉じられるようにします。
    • net_connect.send_command('show ip route')でコマンドを実行し、標準出力を文字列として取得します。
    • ShowIpRoute(device_params['device_type'])でパーサーオブジェクトを作成します。
    • parser.parse(cli_output)で取得したCLI出力をパースします。Genie Parserは複雑なCLI出力を非常に詳細なPython辞書に変換してくれます。
    • パース結果の辞書構造は、コマンドや機器のOSバージョンによって異なります。Genie Parserの公式ドキュメントや、実際にコマンド出力をパースして結果を確認することで、正確な構造を把握する必要があります。例として示したパス (parsed_output.get('vrf', {}).get('default', {}).get('address_family', {}).get('ipv4', {}).get('routes', {})) は一般的な構造ですが、環境によっては異なる可能性があります。.get({}, {})のようにデフォルト値として空の辞書を指定することで、キーが存在しない場合でもエラーにならないようにしています。
    • target_prefixが指定されていれば、パース結果の中から該当するプレフィックスの情報だけを抽出してリストとして返します。指定されていなければ、全ルーティング情報(IPv4のデフォルトVRF)を返します。
    • エラーハンドリングとして、try...exceptブロックで例外をキャッチし、エラーメッセージを表示してNoneを返します。
  4. スクリプト実行部分 (if __name__ == "__main__":)
    • get_and_parse_route_info関数を呼び出します。
    • 返されたroute_infoNoneでなければ、json.dumpsを使って整形されたJSON形式でコンソールに表示します。これは、取得した構造化データの全体像を確認するのに便利です。
    • 全情報取得時 (target_prefixを指定しない場合) のみ、パース結果から特定の情報(例: Directly Connectedな経路のインターフェース、ネクストホップを持つ経路のネクストホップIPとインターフェース)を抽出して表示する例を示しています。この部分も、Genie Parserの出力構造に合わせて適切に修正する必要があります。

注意点:

収集したデータの活用と可視化への連携

Pythonで構造化された経路情報を取得できれば、様々な応用が可能になります。

例えば、取得したルーティング情報から「どのネットワークがどのネクストホップを経由しているか」という情報を抽出し、GraphvizのDOT言語形式で出力するスクリプトを作成すれば、簡単な経路マップを自動生成することも可能です。

# 前述のget_and_parse_route_info関数で取得したparsed_routes(全情報)を想定
parsed_routes = get_and_parse_route_info(device) # この関数呼び出しは別途実行済みとする

if parsed_routes:
    print("\n--- Generating Graphviz DOT format ---")
    dot_output = "digraph Routes {\n"
    dot_output += '  rankdir="LR";\n' # 左から右への描画方向

    for prefix, info in parsed_routes.items():
        # Directly Connectedな経路は除外するか、特別なノードとして扱う
        if 'directly_connected' in info and info['directly_connected']:
             continue # この例では直接接続経路はスキップ

        if 'next_hop' in info:
             next_hops = info['next_hop']
             # ネクストホップがリスト形式の場合
             if isinstance(next_hops, list):
                  for nh_group in next_hops: # ECMPなどでnext_hop_listが複数グループある場合
                      # 各next_hop_list内のエントリを処理
                      if 'next_hop_list' in nh_group and isinstance(nh_group['next_hop_list'], list):
                          for nh_entry in nh_group['next_hop_list']:
                              next_hop_ip = nh_entry.get('next_hop')
                              outgoing_interface = nh_entry.get('outgoing_interface')
                              if next_hop_ip:
                                   # ノードとエッジをDOT形式で記述
                                   # プレフィックスとネクストホップをノードとする
                                   dot_output += f'  "{prefix}" -> "{next_hop_ip}";\n'
                                   # インターフェース情報をエッジのラベルとして追加
                                   if outgoing_interface:
                                       dot_output += f'  "{prefix}" -> "{next_hop_ip}" [ label="{outgoing_interface}" ];\n'


             # ネクストホップが辞書形式(単一ネクストホップ)の場合
             elif isinstance(next_hops, dict):
                 if 'next_hop_list' in next_hops and isinstance(next_hops['next_hop_list'], list):
                     for nh_entry in next_hops['next_hop_list']:
                          next_hop_ip = nh_entry.get('next_hop')
                          outgoing_interface = nh_entry.get('outgoing_interface')
                          if next_hop_ip:
                               dot_output += f'  "{prefix}" -> "{next_hop_ip}";\n'
                               if outgoing_interface:
                                   dot_output += f'  "{prefix}" -> "{next_hop_ip}" [ label="{outgoing_interface}" ];\n'


    dot_output += "}\n"

    print(dot_output)
    print("\n--- End of DOT format ---")
    print("You can use graphviz tools (e.g., dot command) to render this output.")

# Graphvizのインストールが必要です(例: sudo apt-get install graphviz)
# 生成されたDOTファイルを保存し、コマンドラインで実行例: dot -Tpng output.dot -o route_graph.png

このGraphviz出力スクリプトは、非常に単純化された例であり、ECMP(等コストマルチパス)など複雑なルーティング構造には対応していません。しかし、収集した構造化データを様々なツールで活用できる可能性を示しています。

実践的な考慮事項

現場で自動化スクリプトを運用する際には、以下の点を考慮することが重要です。

まとめ

本記事では、Pythonとライブラリ(Netmiko, Genie Parserなど)を用いて、ネットワーク機器からルーティングテーブルなどの経路情報を自動収集し、構造化データとして解析する基本的な手法を解説しました。手動作業の非効率性やヒューマンエラーのリスクを回避し、迅速かつ正確なネットワーク状態の把握を自動化できることを示しました。

収集した構造化データは、単に情報を参照するだけでなく、レポート生成、設定の自動検証、CMDB/監視ツールとの連携、そして可視化ツールの入力データ生成など、様々な形で活用可能です。

ネットワーク機器に直接的な操作経験が限定的であっても、Pythonのスキルを活用することで、ネットワーク領域の自動化に大きく貢献できます。今回紹介した手法を基礎として、ぜひご自身の現場で役立つネットワーク自動化スクリプト開発に取り組んでみてください。今後は、traceroute結果の自動解析や、BGP/OSPFといった特定のルーティングプロトコル情報の詳細な収集・分析についても掘り下げていく予定です。


関連情報・参考文献

これらのリソースを参照することで、さらに深くネットワーク自動化の技術を学ぶことができます。