現場で役立つ!Pythonによるネットワーク機器インベントリ自動収集スクリプト
はじめに
システムインフラの自動化を進める上で、対象となるネットワーク機器の情報を正確に把握し、一元管理することは非常に重要です。IPアドレス、ホスト名、OSバージョン、シリアル番号、設置場所といったインベントリ情報は、設定変更、監視、セキュリティ対策など、あらゆる自動化タスクの基盤となります。
手動でのインベントリ収集や更新は、機器数が増えるにつれて大きな負担となり、情報の陳腐化や入力ミスが発生しやすくなります。このような課題に対し、Pythonを活用したネットワーク機器のインベントリ自動収集は、効率的かつ正確な情報管理を実現する有効な手段です。
Pythonは、豊富なライブラリと高い汎用性により、ネットワーク機器との対話やデータ処理を柔軟に行うことができます。本記事では、Pythonを用いてネットワーク機器を検出し、インベントリ情報を自動で収集するための具体的な手法とスクリプト例をご紹介します。インフラ自動化に長けているものの、ネットワーク機器の操作に不慣れな読者の皆様に向けて、Pythonスキルを活かせる実践的なアプローチを解説いたします。
ネットワーク機器インベントリ収集の基本的なアプローチ
ネットワーク機器のインベントリ情報を収集するには、いくつかの基本的なアプローチがあります。Pythonでこれらを組み合わせることで、網羅的かつ効率的な収集スクリプトを構築できます。
-
機器の検出:
- 特定のIPアドレス範囲に対してPingを送信し、応答がある機器を特定します。
- 一般的なネットワークプロトコルのポート(SSH: 22, Telnet: 23, SNMP: 161, HTTPS: 443など)への接続を試行し、アクティブな機器やサービスを検出します。
- ARPテーブルやMACアドレステーブルを利用して、通信している機器の情報を収集します。
-
インベントリ情報の取得:
- 検出した機器に対し、SSHやTelnetで接続し、CLIコマンドを実行して必要な情報を取得します。(例:
show version
,show inventory
,show ip interface brief
など) - SNMPプロトコルを使用して、MIB(Management Information Base)から機器情報(sysDescr, sysObjectID, ifDescrなど)を取得します。
- 機器がサポートしていれば、RESTConfやNETConfといったネットワーク機器向けのAPIを利用して構造化されたデータを取得します。
- 検出した機器に対し、SSHやTelnetで接続し、CLIコマンドを実行して必要な情報を取得します。(例:
-
データの整形と保存:
- 収集した生データ(CLI出力など)を、必要なインベントリ項目(ホスト名、OS、シリアル番号、インターフェース情報など)に解析・整形します。
- 整形したデータを、CSV, JSON, データベース, またはCMDBツールが取り込める形式で保存します。
Pythonによる機器検出のスクリプト例
ここでは、Python標準ライブラリや一般的なライブラリを使った簡単な機器検出の例を示します。
1. Pingによる疎通確認
特定のIPアドレスに対してPingコマンドを実行し、応答があるかを確認する基本的な方法です。
import subprocess
import platform
import os
def check_ping(ip_address):
"""
指定されたIPアドレスにPingを実行し、応答があればTrueを返します。
"""
# OSによってpingコマンドのオプションが異なるため調整
param = '-n 1' if platform.system().lower() == 'windows' else '-c 1'
command = ['ping', param, ip_address]
try:
# subprocess.runでコマンドを実行し、出力をキャプチャしない
# check=Trueで非ゼロの終了コードの場合はCalledProcessErrorを送出
# timeoutでタイムアウトを設定
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=1)
return True
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
return False
except FileNotFoundError:
print(f"Error: 'ping' command not found. Please ensure it's in your PATH.")
return False
# 例:特定のサブネットの機器をスキャン
subnet = "192.168.1."
print(f"Scanning subnet: {subnet}...")
active_hosts = []
for i in range(1, 255): # 1から254までスキャン
ip = f"{subnet}{i}"
if check_ping(ip):
print(f"{ip} is reachable.")
active_hosts.append(ip)
# else:
# print(f"{ip} is unreachable.") # 疎通しないIPも表示する場合はコメント解除
print(f"\nActive hosts found: {active_hosts}")
解説:
subprocess.run
を使用してOSのPingコマンドを実行します。- OS (
platform.system()
) に応じてPingコマンドのオプションを切り替えています (-n 1
for Windows,-c 1
for Linux/macOS)。 check=True
により、Pingが失敗した場合(終了コードが0以外)に例外が発生するようにしています。これにより、Pingの成否判定を簡潔に行えます。timeout=1
は、Ping応答がない場合に無限に待機しないための重要な設定です。try...except
ブロックで、Ping失敗時の例外やコマンド実行時のエラー(例:ping
コマンドが見つからないFileNotFoundError
)を捕捉しています。
このスクリプトは基本的な疎通確認に過ぎませんが、アクティブなIPアドレスリストを作成する出発点となります。
2. SSHポートへの接続試行
多くのネットワーク機器は管理にSSHを使用します。SSHポート(TCP 22)への接続可能性を確認することで、管理可能なネットワーク機器を絞り込むことができます。paramiko
ライブラリを使用すると、SSH接続の試行が容易です。
import paramiko
import socket
import threading
from queue import Queue
def check_ssh_port(ip_address, port=22, timeout=0.5):
"""
指定されたIPアドレスのSSHポートに接続を試行し、接続できればTrueを返します。
"""
try:
# ソケットを作成し、タイムアウトを設定
sock = socket.create_connection((ip_address, port), timeout=timeout)
sock.close()
return True
except (socket.timeout, ConnectionRefusedError, OSError):
# 接続タイムアウト、接続拒否、ネットワーク関連のエラーを捕捉
return False
except Exception as e:
# その他の予期しないエラー
print(f"Error checking SSH on {ip_address}:{port} - {e}")
return False
# 例:アクティブなホストリストに対してSSHポートを確認(並列処理)
ip_list_to_check = active_hosts # 上記Pingスクリプトで取得したリストを使用
ssh_enabled_hosts = []
print(f"\nChecking SSH port (22) on {len(ip_list_to_check)} hosts...")
# 並列処理のためのQueueとスレッド
q = Queue()
for ip in ip_list_to_check:
q.put(ip)
def worker():
while not q.empty():
ip = q.get()
if check_ssh_port(ip):
print(f"{ip}: SSH port 22 is open.")
ssh_enabled_hosts.append(ip)
# else:
# print(f"{ip}: SSH port 22 is closed or unreachable.")
q.task_done()
# スレッドの作成と開始
threads = []
num_threads = 10 # 同時実行するスレッド数
for _ in range(num_threads):
t = threading.Thread(target=worker)
t.daemon = True # メインスレッド終了時に一緒に終了
threads.append(t)
t.start()
# キューの全てのタスクが完了するのを待つ
q.join()
print(f"\nSSH enabled hosts: {ssh_enabled_hosts}")
解説:
socket.create_connection
を使用して、指定されたIPアドレスとポートへのTCP接続を試みます。timeout
パラメータで接続試行の最大時間を設定します。これにより、応答しないホストでのスクリプト停止を防ぎます。- 接続が成功すればポートが開いていると判断し
True
を返します。 try...except
ブロックで、タイムアウトや接続拒否などの一般的なエラーを捕捉しています。- 多数のホストを効率的にチェックするために、
threading
モジュールを使った簡易的な並列処理を導入しています。Queue
を利用してタスク(チェック対象IP)を管理し、複数のワーカータスクがキューからIPを取り出して処理を行います。
Pythonによるインベントリ情報の取得
SSHポートが開いていることが確認できた機器に対して、実際に接続して情報を取得します。ここではnetmiko
ライブラリを使用するのが最も一般的で効率的です。netmiko
は多数のベンダーのCLIに対応しており、コマンド実行結果のパースも容易にします。
Netmikoを使ったCLIコマンド実行
netmiko
を使用するには、事前にインストールが必要です(pip install netmiko
)。
from netmiko import ConnectHandler
import json
import yaml # YAML形式で認証情報を管理することも多い
import os
# 認証情報を別途ファイル等で管理することを推奨します
# 例: credentials.yaml
# devices:
# cisco_ios:
# device_type: cisco_ios
# port: 22
# username: admin
# password: password123
# secret: enable123
def get_device_info(ip_address, device_type, username, password, secret=None):
"""
Netmikoを使って機器に接続し、基本情報を取得します。
"""
device_params = {
'device_type': device_type, # 例: 'cisco_ios', 'arista_eos', 'juniper_junos'など
'host': ip_address,
'username': username,
'password': password,
'secret': secret, # 特権EXECモードへのパスワード(必要な場合)
'port': 22,
'timeout': 10, # 接続タイムアウト
'global_delay_factor': 2 # コマンド実行間の遅延調整(必要な場合)
}
try:
print(f"Connecting to {ip_address}...")
with ConnectHandler(**device_params) as net_connect:
# 特権EXECモードへ遷移(必要な場合)
if secret:
net_connect.enable()
# インベントリ情報を取得するコマンドを実行
# ベンダーやOSによってコマンドは異なります
commands = [
'show version',
'show inventory' # または別のインベントリ表示コマンド
# 他に取得したい情報があればコマンドを追加
# 'show ip interface brief',
# 'show running-config'
]
device_info = {ip_address: {}}
for cmd in commands:
print(f" Executing command: {cmd}")
output = net_connect.send_command(cmd)
device_info[ip_address][cmd] = output
print(f"Successfully got info from {ip_address}.")
return device_info
except Exception as e:
print(f"Failed to get info from {ip_address}: {e}")
return {ip_address: {"error": str(e)}}
# 例:SSHが有効なホストリストに対して情報収集
# 実際には、各IPに対応するデバイスタイプや認証情報が必要です
# ここではSSH_enabled_hostsリストと、仮の認証情報・デバイスタイプを使用
inventory_data = {}
# 実際の運用では、IPと認証情報、デバイスタイプを紐付けたリストや設定ファイルを用意します
# 例: [{"ip": "192.168.1.10", "type": "cisco_ios", "user": "admin", "pass": "pass", "secret": "enable"}, ...]
dummy_inventory_targets = [
{"ip": ip, "type": "cisco_ios", "user": "dummy_user", "pass": "dummy_password", "secret": "dummy_enable_password"}
for ip in ssh_enabled_hosts # 上記スクリプトで取得したSSH有効ホストリスト
]
# dummy_inventory_targets が空の場合は処理をスキップ
if not dummy_inventory_targets:
print("\nNo SSH enabled hosts found to collect inventory.")
else:
print("\nCollecting inventory information...")
for target in dummy_inventory_targets:
info = get_device_info(target['ip'], target['type'], target['user'], target['pass'], target.get('secret'))
inventory_data.update(info)
# 収集したデータの表示(またはファイル保存)
print("\n--- Collected Inventory Data ---")
print(json.dumps(inventory_data, indent=2))
# データの保存例 (JSON形式)
# with open('network_inventory.json', 'w') as f:
# json.dump(inventory_data, f, indent=2)
# print("\nInventory data saved to network_inventory.json")
解説:
netmiko.ConnectHandler
は、ネットワーク機器へのSSH/Telnet接続を抽象化するクラスです。device_params
ディクショナリに接続に必要な情報を渡します。device_type
は、netmiko
が機器のCLI特性を理解するために重要です。対応デバイスタイプはnetmiko.supported_devices
で確認できます。with ConnectHandler(...) as net_connect:
構文を使用すると、接続が自動的にクローズされるため推奨されます。net_connect.enable()
は、特権EXECモード(#
プロンプト)に遷移する場合に使用します。net_connect.send_command(cmd)
は、指定されたCLIコマンドを実行し、その出力を文字列として返します。- 取得したコマンド出力は、IPアドレスをキーとしたディクショナリに格納しています。
実践的な考慮点:
- 認証情報管理: スクリプト内に認証情報を直接書くのはセキュリティリスクが高いです。環境変数、設定ファイル(YAML, JSON)、または専用の秘密情報管理ツール(HashiCorp Vault, Ansible Vaultなど)を使用することを強く推奨します。
- エラーハンドリング: ネットワーク障害、認証失敗、コマンド実行失敗など、さまざまなエラーが想定されます。
try...except
ブロックを適切に配置し、エラー発生時にもスクリプトが異常終了せず、問題のある機器を特定できるようにする必要があります。 - デバイスタイプ: 正確な情報を収集するためには、各機器のベンダーとOSに応じた
device_type
を指定する必要があります。事前にIPアドレスとデバイスタイプのマッピング情報を用意しておくと良いでしょう。 - コマンド出力のパース:
show version
などのコマンド出力は、そのままでは構造化されていません。インベントリ項目として利用するためには、Pythonの文字列処理(正規表現、文字列分割など)や、より高度なライブラリ(textfsm
,genie
,ttp
など)を使って構造化されたデータに変換する必要があります。NAPALMライブラリは、特定のインベントリ情報を構造化データとして取得する機能を提供しており、このステップを効率化できます。
SNMPを使ったインベントリ情報の取得
機器がSNMPをサポートしている場合、SNMPプロトコルを使ってより構造化されたインベントリ情報を取得できます。pysnmp
などのライブラリを使用します(pip install pysnmp
)。
from pysnmp.hlapi import *
def get_snmp_info(ip_address, community='public', oid='1.3.6.1.2.1.1.1.0'): # sysDescr.0
"""
指定されたIPアドレスからSNMPを使って情報を取得します。
デフォルトではsysDescr (MIB-2のシステム説明) を取得します。
"""
iterator = getCmd(
SnmpEngine(),
CommunityData(community, mpModel=0), # v1の場合はmpModel=0
UdpTransportTarget((ip_address, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid))
)
errorIndication, errorStatus, errorIndex, varBinds = next(iterator)
if errorIndication:
print(f"SNMP Error on {ip_address}: {errorIndication}")
return None
elif errorStatus:
print(f"SNMP Error on {ip_address}: {errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or '?'}")
return None
else:
# varBindsはOIDと値のリスト
for varBind in varBinds:
# OIDと値を文字列で返す
return f"{varBind[0].prettyPrint()} = {varBind[1].prettyPrint()}"
return None # 結果がない場合
# 例:アクティブなホストリストに対してSNMP情報を取得
# 実際には、SNMPコミュニティ名や取得したいOIDを指定する必要があります
snmp_enabled_hosts = ssh_enabled_hosts # SSH有効ホストリストを流用(仮)
snmp_inventory_data = {}
if not snmp_enabled_hosts:
print("\nNo hosts found to check SNMP.")
else:
print("\nCollecting SNMP information (sysDescr)...")
# 実際には、各ホストのコミュニティ名を知っている必要があります
# ここでは全てのホストで 'public' を試行 (非推奨: 実際の環境ではセキュリティリスク)
for ip in snmp_enabled_hosts:
sysdescr = get_snmp_info(ip, community='public', oid='1.3.6.1.2.1.1.1.0')
if sysdescr:
print(f"{ip}: {sysdescr}")
snmp_inventory_data[ip] = {"sysDescr": sysdescr}
# else:
# print(f"{ip}: Failed to get sysDescr via SNMP.")
print("\n--- Collected SNMP Inventory Data ---")
print(json.dumps(snmp_inventory_data, indent=2))
解説:
pysnmp.hlapi
を使用してSNMP操作を行います。CommunityData
でSNMPコミュニティ名とSNMPバージョンを指定します(mpModel=0
はv1,mpModel=1
はv2c,mpModel=2
はv3)。UdpTransportTarget
で対象IPアドレスとポート(通常161/UDP)を指定します。ObjectType(ObjectIdentity(oid))
で取得したいMIBオブジェクトのOIDを指定します。デフォルトではsysDescr.0
(システムの簡単な説明) を指定しています。getCmd
を実行し、next()
で結果を取得します。- エラー発生時は
errorIndication
やerrorStatus
に情報が格納されます。 - 成功時は
varBinds
にOIDと値のリストが格納されます。
実践的な考慮点:
- セキュリティ: SNMPコミュニティ名(特にv1/v2cのリードオンリーコミュニティ)は認証なしで情報を提供するため、適切に設定・管理しないとセキュリティリスクになります。デフォルトのコミュニティ名(public/private)を運用環境で使用することは避けるべきです。可能であればSNMPv3の利用を検討してください。
- OIDの特定: 取得したい情報に対応するOIDを知っている必要があります。ベンダー固有のMIB情報が必要な場合もあります。
- UDPプロトコル: SNMPは通常UDPを使用します。PingやSSH(TCP)とは挙動が異なる点に注意が必要です。
収集したインベントリデータの活用と管理
収集したインベントリデータは、単に集めるだけでなく、その後の自動化や運用プロセスで活用できる形式で管理することが重要です。
- データの整形: CLI出力のような非構造化データは、正規表現やテンプレート(TextFSM, Genie Parserなど)を使ってJSONやYAMLといった構造化データに変換します。これにより、プログラムからの参照や検索が容易になります。
- データの保存:
- 単純なリストやスプレッドシート形式であればCSVファイル。
- 構造化されたデータであればJSONやYAMLファイル。
- より大規模、永続的、検索可能なデータとして管理したい場合は、リレーショナルデータベース(PostgreSQL, MySQLなど)やNoSQLデータベース(MongoDBなど)に格納します。
- 専用のCMDB(Configuration Management Database)ツールやネットワークインベントリ管理ツール(NetBoxなど)にAPI経由で登録・連携させるのが最も理想的です。これらのツールは、機器間の接続情報(ケーブル情報)やIPアドレス管理、仮想マシンとの関連付けなど、より広範な情報を一元管理できます。
- データの更新頻度: インベントリ情報は時間とともに変化します(OSアップグレード、ハードウェア増設・交換、設定変更など)。定期的に自動収集スクリプトを実行し、最新の状態を維持する必要があります。差分を検出し、変更点を通知する機能を追加すると、運用上のメリットが大きくなります。
- 他の自動化ツールとの連携: 収集・管理したインベントリデータは、Ansible, Chef, PuppetなどのIaCツールや、CI/CDパイプライン、監視ツールと連携させることで、自動化の範囲を広げ、効率をさらに向上させることができます。例えば、Ansibleのインベントリファイルとして動的に生成したり、監視対象リストを自動更新したりすることが考えられます。
まとめ
本記事では、Pythonを使用してネットワーク機器の検出からインベントリ情報の自動収集・管理までを行うための基本的なアプローチと実践的なスクリプト例をご紹介しました。PingやSSHポートスキャンによる機器検出、Netmikoを使ったCLIからの情報取得、SNMPによる情報収集といった手法を組み合わせることで、手動では困難な大規模なインベントリ収集を効率化できます。
スクリプトを現場で活用する際には、認証情報の安全な管理、様々なベンダー/OSへの対応、取得データの適切なパースと構造化、そして収集したデータの永続的な管理(CMDB連携など)が重要な考慮点となります。
Pythonはネットワーク自動化のための強力なツールです。今回ご紹介したスクリプトはあくまで基本的な例ですが、これらを基盤として、皆様の環境や要件に合わせたより高度なインベントリ自動収集システムを構築していただければ幸いです。