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

現場で役立つPython:ネットワーク機器のポート情報自動収集とレポート

Tags: Python, ネットワーク自動化, Netmiko, ポート情報, レポート生成

はじめに

ネットワーク機器のポート情報は、日々の運用において非常に重要です。新しい機器の導入時、設定変更時、トラブルシューティング時、あるいは定期的なインベントリ更新など、様々な場面で現在のポートの状態(リンクアップ/ダウン、エラーカウンタ、VLAN設定、Descriptionなど)を確認する必要があります。

これらの情報を手作業で、一台ずつCLIコマンドを実行して収集・記録するのは、時間と手間がかかる上に、ヒューマンエラーの原因にもなり得ます。特に管理対象の機器が多い環境では、この作業は大きな負担となります。

そこで本記事では、Pythonを活用してネットワーク機器のポート情報を自動的に収集し、人が確認しやすい形式でレポートとして出力する手法をご紹介します。Pythonのライブラリを利用することで、複数の機器から効率的に情報を取得し、整形・集計することが可能になります。

Pythonによる開発やクラウドインフラ自動化の経験が豊富な読者の皆様にとって、この記事がネットワーク機器の物理・論理的な状態を把握し、管理業務を効率化するための一助となれば幸いです。

ポート情報収集の目的とPythonによる自動化のメリット

ネットワーク機器のポート情報を自動収集することには、以下のような目的とメリットがあります。

Pythonは、豊富なネットワーク関連ライブラリや強力なデータ処理能力を持っているため、このような自動化に適しています。

必要なライブラリの準備

ポート情報収集スクリプトを実装するために、主に以下のPythonライブラリを使用します。

これらのライブラリはpipを使ってインストールできます。

pip install netmiko pandas jinja2

ネットワーク機器からのポート情報収集

まず、Netmikoを使ってネットワーク機器に接続し、ポート情報を取得するCLIコマンドを実行します。取得する情報としては、インターフェース名、物理的なリンク状態(up/down)、管理上の状態(up/down/admin down)、Description、VLAN ID(アクセスポートの場合)、ポートモード(アクセス/トランク)などが一般的です。

取得したい情報に応じて、実行するコマンドはベンダーやOSによって異なります。代表的な例としては以下のようなコマンドがあります。

ここでは、例としてCisco IOS機器からshow interface statusコマンドで主要なポート情報を取得し、show interfaces descriptionコマンドでDescriptionを取得するシナリオを想定します。

以下は、単一の機器から情報を取得する基本的なコード例です。

import os
from netmiko import ConnectHandler

# 環境変数から認証情報を取得(現場ではより安全な方法を推奨)
# export NET_DEVICE_HOSTNAME=your_device_ip
# export NET_DEVICE_USERNAME=your_username
# export NET_DEVICE_PASSWORD=your_password
# export NET_DEVICE_TYPE=cisco_ios

device_info = {
    'device_type': os.environ.get('NET_DEVICE_TYPE', 'cisco_ios'),
    'host': os.environ.get('NET_DEVICE_HOSTNAME'),
    'username': os.environ.get('NET_DEVICE_USERNAME'),
    'password': os.environ.get('NET_DEVICE_PASSWORD'),
    'port': 22, # デフォルトSSHポート
    'secret': '', # enableパスワードが必要な場合
}

# 接続情報のバリデーション
if not all([device_info['host'], device_info['username'], device_info['password']]):
    print("エラー: ネットワーク機器の接続情報が環境変数に設定されていません。")
    exit(1)

port_status_output = ""
port_description_output = ""

try:
    # ネットワーク機器への接続
    print(f"機器 {device_info['host']} に接続しています...")
    with ConnectHandler(**device_info) as net_connect:
        print("接続に成功しました。")

        # コマンド実行
        print("ポートステータス情報を取得中...")
        port_status_output = net_connect.send_command("show interface status")

        print("ポートDescription情報を取得中...")
        port_description_output = net_connect.send_command("show interfaces description")

        print("コマンド実行完了。")

except Exception as e:
    print(f"接続またはコマンド実行中にエラーが発生しました: {e}")

# 取得したCLI出力を表示 (デバッグ用)
# print("\n--- show interface status ---")
# print(port_status_output)
# print("\n--- show interfaces description ---")
# print(port_description_output)

print("\nポート情報の収集が完了しました。")

# この後、取得した `port_status_output` と `port_description_output` をパース・整形します。
# このコード例は単一機器からの取得のみを示しています。

上記のコードは、指定した機器にSSH接続し、2つのCLIコマンドを実行してその結果を取得する基本的な流れを示しています。認証情報には環境変数を使用していますが、これはあくまで例であり、現場ではよりセキュアなCredential Managerなどの利用を検討すべきです。

CLI出力のパースとデータの整形

Netmikoのsend_commandで取得できるのは単なる文字列データです。この非構造化データを、後続の処理やレポート生成に使いやすいように構造化する必要があります。CLI出力のパース方法としては、以下の選択肢があります。

  1. 正規表現や文字列分割: シンプルなCLI出力であれば、Pythonの標準機能だけでパース可能です。
  2. TextFSM/TTP: 定義ファイル(テンプレート)に基づいてCLI出力を構造化されたデータ(リストや辞書)に変換するツールです。NetmikoはTextFSMの機能を取り込んでいます (send_command(..., use_textfsm=True))。
  3. genieparser: Ciscoが開発した、より高度で正確なパースライブラリです。対応コマンドは多いですが、特定のベンダーに依存する場合があります。

今回は、より汎用性が高く、Netmikoとも連携しやすいTextFSM、またはPythonとPandasを使った基本的な文字列処理でパースを行う方法に焦点を当てます。特にPandasを使うと、表形式での整形や結合が容易になります。

show interface statusの出力例(Cisco IOS)は以下のようになります。

Port      Name               Status    Vlan      Duplex  Speed Type
Fa0/1     Server Port        connected 10        a-full  a-100 10/100BaseTX
Fa0/2                        notconnect  1       auto    auto  10/100BaseTX
Gi0/1     Uplink             connected 100       a-full  a-1000 10/100/1000BaseTX
...

show interfaces descriptionの出力例(Cisco IOS)は以下のようになります。

Interface                      Status         Protocol Description
Fa0/1                          up             up       Server Port
Fa0/2                          down           down     --
Gi0/1                          up             up       Uplink
...

これらの情報を組み合わせて、インターフェース名、Status、VLAN、Descriptionなどをまとめた表を作成します。

import pandas as pd
import io # ファイルライクオブジェクトとして扱うために使用

# 前のステップで取得したCLI出力を使用
# port_status_output = "..."
# port_description_output = "..."

# TextFSMを利用する場合 (Netmikoの機能)
# NetmikoがTextFSMテンプレートを内部に持っている場合、use_textfsm=Trueでパースできます。
# ただし、全てのコマンドに対応しているわけではありません。
# status_data = net_connect.send_command("show interface status", use_textfsm=True)
# description_data = net_connect.send_command("show interfaces description", use_textfsm=True)
# status_df = pd.DataFrame(status_data)
# description_df = pd.DataFrame(description_data)

# Python/Pandasで基本的な文字列処理を行う場合
# (TextFSMで対応していないコマンドや、よりカスタマイズしたい場合に有効)

# show interface status のパース (簡易版)
# 実際の出力に合わせて調整が必要です
status_lines = port_status_output.strip().splitlines()
status_data_list = []
if len(status_lines) > 1: # ヘッダー行があるか確認
    header = status_lines[0].split() # スペース区切りでヘッダーを取得 (簡易)
    # 実際のパースロジックは、固定幅や特定の区切り文字、正規表現を使うなど、出力形式に依存します。
    # ここでは非常に単純なスペース区切りを想定していますが、厳密なパースには工夫が必要です。
    # 例:
    # status_regex = re.compile(r"(\S+)\s+(\S*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)")
    # for line in status_lines[1:]:
    #    match = status_regex.match(line)
    #    if match:
    #        status_data_list.append(match.groups())

    # TextFSM形式で出力されていると仮定してPandasで読み込む方が頑丈な場合があります
    # TextFSMの出力は通常JSONやリストの形式ですが、ここではCLI出力をTextFSMが処理しやすい形式と見なす
    # もしくは、TabularDataを想定してPandasで読み込む (Cisco等のshowコマンドはTabularDataに近いものがある)
    try:
        # StringIOを使って文字列をファイルライクオブジェクトとして扱い、read_fwfで固定幅として読み込む試み
        # 固定幅カラムの位置は実際の出力を見て調整が必要です
        status_df_raw = pd.read_fwf(io.StringIO(port_status_output), skiprows=1) # ヘッダーをスキップ
        # 必要に応じてカラム名をリネームしたり、不要なカラムを削除
        # 例: status_df = status_df_raw[['Port', 'Name', 'Status', 'Vlan', 'Duplex', 'Speed', 'Type']]
        # ただし、固定幅での読み込みは出力形式が少しでもずれると失敗しやすいです。
        # 最も確実なのはTextFSMテンプレートを使うか、genieparserのような専用パーサーです。

        # ここではTextFSMが最も一般的でPandasと連携しやすいので、TextFSMでパースされたデータ構造を想定した処理例を示します。
        # 実際には、TextFSMテンプレートを適用して得られたリスト of 辞書 を pandas.DataFrame で読み込みます。
        # TextFSMでのパース結果例: [{'port': 'Fa0/1', 'name': 'Server Port', 'status': 'connected', 'vlan': '10', ...}, {...}]
        # status_data_from_textfsm = [...] # ここにTextFSMでパースした結果が入ると仮定
        # status_df = pd.DataFrame(status_data_from_textfsm)
        print("CLI出力を整形しました。")

        # 仮のDataFrameを作成(実際にはパース結果で置き換える)
        status_data = [
            {'port': 'Fa0/1', 'name': 'Server Port', 'status': 'connected', 'vlan': '10'},
            {'port': 'Fa0/2', 'name': '', 'status': 'notconnect', 'vlan': '1'},
            {'port': 'Gi0/1', 'name': 'Uplink', 'status': 'connected', 'vlan': '100'},
        ]
        description_data = [
            {'interface': 'Fa0/1', 'description': 'Server Port'},
            {'interface': 'Fa0/2', 'description': '--'},
            {'interface': 'Gi0/1', 'description': 'Uplink'},
        ]
        status_df = pd.DataFrame(status_data)
        description_df = pd.DataFrame(description_data).rename(columns={'interface': 'port'}) # 結合キーを合わせる

        # データフレームを結合
        # インターフェース名 ('port'/'interface') をキーとして結合します
        # 結合方法やキー名は、実際のパース結果に合わせてください
        merged_df = pd.merge(status_df, description_df, on='port', how='left')

        # 必要に応じてカラムの並べ替えやリネーム
        # merged_df = merged_df[['port', 'description', 'status', 'vlan', ...]]

        print("\n整形後のデータ:")
        print(merged_df.to_string()) # コンソール表示用

    except Exception as e:
         print(f"CLI出力のパースまたはデータ処理中にエラーが発生しました: {e}")
         merged_df = pd.DataFrame() # エラー時は空のDataFrameを作成

else:
    print("CLI出力が予期された形式ではありませんでした。")
    merged_df = pd.DataFrame() # エラー時は空のDataFrameを作成


# 後続のレポート生成処理に merged_df を使用します。

上記の例では、TextFSMでパースした結果(リスト of 辞書)をPandas DataFrameに変換することを想定した処理の概念を示しています。TextFSMのテンプレートファイルは、GitHubのnetworktocode/ntc-templatesリポジトリなどで公開されているものを使用するか、自作することも可能です。

もしTextFSMテンプレートが利用できない、あるいはPandasでより柔軟に処理したい場合は、正規表現や文字列分割、Pandasの強力な文字列処理関数 (.strアクセサ) を組み合わせてパース・整形を行います。

レポートの生成

整形したデータを基に、目的や用途に応じたレポートを生成します。簡単なCSV形式、あるいはMarkdownやHTML形式で出力することが考えられます。Pandasはこれらの出力形式をサポートしています。

ここでは、収集したポート情報をMarkdown形式の表として出力する例を示します。Jinja2テンプレートを使うと、より柔軟なレイアウトでレポートを作成できます。

# 前のステップで作成した merged_df を使用

if not merged_df.empty:
    # Pandasのto_markdown()メソッドでMarkdown形式の表を生成
    # index=False で行番号を出力しないようにします
    markdown_table = merged_df.to_markdown(index=False)

    # レポートファイル名の決定
    report_filename = f"{device_info['host']}_port_report.md"

    # レポートファイルの作成
    try:
        with open(report_filename, "w") as f:
            f.write(f"# ネットワーク機器ポート情報レポート - {device_info['host']}\n\n")
            f.write("## 収集日時\n\n")
            import datetime
            f.write(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
            f.write("## ポート一覧\n\n")
            f.write(markdown_table)
            f.write("\n") # 末尾に改行を追加

        print(f"レポートを {report_filename} として出力しました。")

    except Exception as e:
        print(f"レポート出力中にエラーが発生しました: {e}")

else:
    print("処理対象のデータがありませんでした。レポートは生成されません。")

このコードは、Pandas DataFrameをMarkdown形式の文字列に変換し、簡単なヘッダーと組み合わせてファイルに書き出す例です。DataFrameの内容やカラム名に合わせて、to_markdown()の引数を調整してください。

Jinja2を使う場合は、以下のような流れになります。

  1. レポートのMarkdown/HTMLテンプレートファイルを作成する。 例: report_template.md ```markdown # ネットワーク機器ポート情報レポート - {{ hostname }}

    収集日時

    {{ collection_time }}

    ポート一覧

    | {{ columns | join(" | ") }} | | {{ columns | map("length") | map("multiply", "-") | join(" | ") }} | {% for row in data %} | {{ row | map("string") | join(" | ") }} | {% endfor %} 2. Pythonスクリプト内でJinja2環境をセットアップし、テンプレートを読み込む。 3. テンプレートにデータを渡してレンダリングする。python from jinja2 import Environment, FileSystemLoader

    Jinja2環境のセットアップ (テンプレートファイルがあるディレクトリを指定)

    env = Environment(loader=FileSystemLoader('.')) # スクリプトと同じディレクトリにある場合 template = env.get_template('report_template.md')

    テンプレートに渡すデータの準備

    DataFrameをリスト of リスト または リスト of 辞書 に変換

    report_data = merged_df.values.tolist() report_columns = merged_df.columns.tolist()

    テンプレートをレンダリング

    rendered_report = template.render( hostname=device_info['host'], collection_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), columns=report_columns, data=report_data )

    レポートファイルとして保存

    report_filename_jinja = f"{device_info['host']}_port_report_jinja.md" with open(report_filename_jinja, "w") as f: f.write(rendered_report)

    print(f"Jinja2を使用してレポートを {report_filename_jinja} として出力しました。") ``` Jinja2を使うことで、より柔軟でデザイン性の高いレポートを生成できます。

複数の機器への対応と実践的な考慮点

単一の機器だけでなく、複数の機器から情報を収集するには、いくつかの拡張が必要です。

  1. インベントリ管理: 接続先の機器情報をリストやファイル(CSV, YAMLなど)で管理します。Nornirのような自動化フレームワークは、強力なインベントリ機能を備えています。
  2. 並列処理: 複数の機器に順番に接続すると時間がかかるため、threadingやasyncioなどを使って並列に処理を実行することで、全体の実行時間を大幅に短縮できます。Nornirは並列実行をサポートしています。
  3. エラーハンドリング: 機器への接続失敗、認証失敗、コマンド実行失敗など、発生しうるエラーに対する適切なハンドリングが必要です。機器ごとに結果を記録し、失敗した機器についてはエラーメッセージを含めるなどの工夫が有効です。
  4. 認証情報の安全な管理: スクリプト内にパスワードを直書きするのは絶対に避けるべきです。環境変数、専用のCredential Manager(HashiCorp Vault, CyberArkなど)、あるいはOSの機能を利用するなど、安全な方法で認証情報を管理してください。Netmikoはいくつかの認証情報取得方法に対応しています。
  5. 定期実行: ポート情報は常に変化しうるため、収集スクリプトを定期的に実行することが推奨されます。LinuxのcronやWindowsのタスクスケジューラなどを利用できます。
  6. 取得データの活用: 収集したデータは、単にレポートとして出力するだけでなく、構成管理データベース(CMDB)の更新、ネットワーク監視ツールのインポートデータ、あるいはIaCツール(Ansibleなど)のファクトデータとして活用することも可能です。

まとめ

本記事では、PythonとNetmiko、Pandas、Jinja2といったライブラリを活用し、ネットワーク機器の物理・論理ポート情報を自動収集してレポートを生成する実践的な手法をご紹介しました。手作業での情報収集にかかる時間と労力を大幅に削減し、情報の正確性と鮮度を高めることが可能になります。

CLI出力のパースには、TextFSMやPandasによる文字列処理などいくつかの方法があり、収集したデータはPandasを使って容易に整形できます。レポート生成にはMarkdownやCSV、あるいはJinja2を使った柔軟な出力形式が利用できます。

ご紹介したスクリプトは基本的なものですが、これを基盤として、複数の機器への対応、エラーハンドリングの強化、認証情報の安全な管理、定期実行の設定などを加えることで、現場で十分に活用できる自動化スクリプトを構築できます。

ネットワークの状態把握は、安定したインフラ運用に不可欠です。Pythonを使ったポート情報自動収集は、その最初の一歩として非常に有効です。ぜひ、ご自身の環境に合わせてスクリプトをカスタマイズし、日々の業務に役立ててください。