Pythonでルータにpingする
疎通できる構築したルータ一覧を作成する必要があり、 Pythonでルータにpingして、その結果を一覧にしてみました。
はじめに
ある日、Prometheusの監視対象ルータ一覧を作成することにしましたが、 管理台帳上ではルータは120台ぐらいあるように見えます。
しかしながら、よくよく確認すると、構築が延期になったり、そもそも欠番になったりして、 実際に構築したルータが、どれかよくわからない状態になっていました。 構築されていないルータを監視対象に入れるのはもったいないので、 疎通できるルータの一覧を作成することにしました。
疎通確認はpingでしますが、 1回目のpingでは応答しないルータもあるため、必ず2回pingを実行して、 ルータ一覧を作成することにします。
今回は雑に宛先リストを作成して、Pingの結果(OK or NG)を、宛先リストに追記します。
後々、YAML形式に変換して出力したいと考えたので、Pythonでルータにpingして、 その結果を一覧としてCSVに保存することにします。
やりかた
Pythonには、ICMP echo request/reply用のpyping
などのライブラリがあります。
ただし、難点なのが管理者権限(root)が必要なため、踏み台サーバ経由など、お手軽に実行できない場合があります。
このため、Windowsのping.exe
コマンドをsubprocess
ライブラリを
利用して外部プロセスとして起動し、ルータにpingします。
今回は、Win10の環境のため、Win10のping.exe
を実行することにしました。
下記の通り、Win10の環境に、Anacondaをインストールした環境を用意して検証します。
項目 | 詳細 |
---|---|
OS | Windows 10 |
Python | Anaconda 2019.10 (Python 3.7.4) |
pingコマンド | Windows 10 標準のping.exe コマンドを利用 |
下図の通り、宛先リストCSV生成⇒ping実行⇒Prometheus SNMP Exporter用YAMLファイルの生成という段階で作業します。 ping実行については、逐次実行パターンと、並列実行パターンの2種類を用意しました。
PythonでWin10上のping.exeの実行
下記は、subprocess
ライブラリから、ping.exe
コマンドを実行した例になります。
ping.exe
は、何かしらのping応答があった場合、戻り値(return code
/ exit status
)が、0
になる問題があります。
Ping NGでも、ICMP Unreachableが返ってくると、戻り値が0
になります。
このため、戻り値では、Ping OKと判断できません。
参考:【バッチ】戻り値で結果を判定する場合に注意が必要なコマンド(tarやping)【シェル】 - 俺のメモ帖
このため、ping.exe
の標準出力から文字列を抜き出して、Ping NGかPing OKかを判断する必要があります。
今回はpingが成功した場合、下記のような文字列が表示され、必ずTTL=
が含まれます。
TTL=
が含まれていた場合は、Ping OKで、含まれない場合はPing NGとして取り扱います。
>ping -n 2 -w 1000 127.0.0.1 127.0.0.1 に ping を送信しています 32 バイトのデータ: 127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128 127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128 127.0.0.1 の ping 統計: パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、 ラウンド トリップの概算時間 (ミリ秒): 最小 = 0ms、最大 = 0ms、平均 = 0ms
プログラム:ping0_test.py
subprocess
ライブラリで、ping.exe
の実行結果を正しく解釈できるか、下記のようなサンプルプログラムを作成しました。
subprocess.run
で、ping.exe -n 2 -w 1000 192.168.10.1
とping.exe -n 2 -w 1000 192.168.10.2
を実行し、
ping.exe
の戻り値(return code)と標準出力を、それぞれ表示します。
import subprocess print("=" * 60) print("PING OKの場合") print("=" * 60) commands = ["ping", "-n", "2", "-w", "1000", "192.168.10.1"] print(" ".join(commands)) proc = subprocess.run( commands, stdout=subprocess.PIPE, # 標準出力は保存 stderr=subprocess.DEVNULL # 標準エラーは捨てる ) print(f"return code : {proc.returncode}") result = proc.stdout.decode("cp932") print(result) print() print("=" * 60) print("PING NGの場合") print("=" * 60) commands = ["ping", "-n", "2", "-w", "1000", "192.168.10.2"] print(" ".join(commands)) proc = subprocess.run( commands, stdout=subprocess.PIPE, # 標準出力は保存 stderr=subprocess.DEVNULL # 標準エラーは捨てる ) print(f"return code : {proc.returncode}") result = proc.stdout.decode("cp932") print(result) print()
実行結果:ping0_test.py
下記の実行結果から、Ping OKとPing NGの場合、戻り値(return code)が0
であることが確認できます。
このため、判定条件は下記のとおりとなります。
状態 | 判定条件 |
---|---|
Ping OK | 標準出力proc.stdout.decode("cp932") にTTL= が含まれる |
Ping NG | 標準出力proc.stdout.decode("cp932") にTTL= が含まれない |
> python ping0_test.py ============================================================ PING OKの場合 ============================================================ ping -n 2 -w 1000 192.168.10.1 return code : 0 192.168.10.1 に ping を送信しています 32 バイトのデータ: 192.168.10.1 からの応答: バイト数 =32 時間 =3ms TTL=255 192.168.10.1 からの応答: バイト数 =32 時間 =3ms TTL=255 192.168.10.1 の ping 統計: パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、 ラウンド トリップの概算時間 (ミリ秒): 最小 = 3ms、最大 = 3ms、平均 = 3ms ============================================================ PING NGの場合 ============================================================ ping -n 2 -w 1000 192.168.10.2 return code : 0 192.168.10.2 に ping を送信しています 32 バイトのデータ: 要求がタイムアウトしました。 192.168.10.111 からの応答: 宛先ホストに到達できません。 192.168.10.2 の ping 統計: パケット数: 送信 = 2、受信 = 1、損失 = 1 (50% の損失)、
pingの宛先リスト(CSV)を作成
次に、Pingの宛先リストを作成します。
今回は、192.168.10.1
~192.168.10.9
までのIPアドレスリストを作成します。
IPアドレス以外にも連続性がある場合は、ホスト名に連番をつけて、作成しても良いと思います。
ファイルは編集しやすいようにCSV形式で保存します。 CSV形式は下記のような2つのカラム(列)を定義しました。
カラム名 | 詳細 |
---|---|
description | ルータを識別する名前、愛称など |
target | Pingの宛先。IPアドレスやホスト名を指定 |
プログラム:generate_csv.py
実行すると、target
として、192.168.10.1
~192.168.10.9
までのリストをCSVとして、
ping_targets.csv
ファイルに保存します。
このとき、description
には、host01
~host09
が同時につけられます。
import csv CSV_COLUMNS = ["description", "target"] OUTPUT_CSV = "ping_targets.csv" with open(OUTPUT_CSV, "w", encoding="cp932", newline="") as f: writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS) writer.writeheader() # 192.168.10.1~192.168.10.9までを生成する for i in range(1, 10): description = f"host{i:02}" target = f"192.168.10.{i}" row = { "description": description, "target": target } writer.writerow(row) print(row)
実行結果:generate_csv.py
実行結果は下記のとおりです。宛先が9個出力されています。
同時にCSVファイルping_targets.csv
にも保存されています。
>python generate_targets.py {'description': 'host01', 'target': '192.168.10.1'} {'description': 'host02', 'target': '192.168.10.2'} {'description': 'host03', 'target': '192.168.10.3'} {'description': 'host04', 'target': '192.168.10.4'} {'description': 'host05', 'target': '192.168.10.5'} {'description': 'host06', 'target': '192.168.10.6'} {'description': 'host07', 'target': '192.168.10.7'} {'description': 'host08', 'target': '192.168.10.8'} {'description': 'host09', 'target': '192.168.10.9'}
出力結果:ping_targets.csv
下記のように、宛先リストがCSVで出力されます。
description,target host01,192.168.10.1 host02,192.168.10.2 host03,192.168.10.3 host04,192.168.10.4 host05,192.168.10.5 host06,192.168.10.6 host07,192.168.10.7 host08,192.168.10.8 host09,192.168.10.9
宛先リストにpingする(逐次実行するパターン)
いよいよ、宛先リストにpingします。 まずはじめに、宛先リストから1行ずつ取り出して、pingし、結果をCSVに保存します。
プログラム:ping1_singlethread.py
ping
関数で、ping.exe
を実行します。
引数には宛先リストのCSV行データであるrow
を渡しています。
row["target"]
が含まれていることを期待し、row["target"]
宛にpingします。
実行結果はrow["result"]
にOK
またはNG
として保存します。
import subprocess import csv import time CSV_COLUMNS = ["description", "target", "result"] INPUT_CSV = "ping_targets.csv" OUTPUT_CSV = "ping_results.csv" def ping(row): # windowsのpingコマンドを実行する # ICMP Echo Requestを2回送出 "-n 2" # タイムアウトは1000ミリ秒 "-w 1000" # target宛に送信 # ping -n 2 -w 1000 target proc = subprocess.run( ["ping", "-n", "2", "-w", "1000", row["target"]], stdout=subprocess.PIPE, # 標準出力は判断のため保存 stderr=subprocess.DEVNULL # 標準エラーは捨てる ) # pingコマンドの実行結果(標準出力)に「TTL=」の文字列があれば、PING OKと判断 succeed = proc.stdout.decode("cp932").find("TTL=") > 0 row["result"] = "OK" if succeed else "NG" return row def main(): start = time.time() # 実行時間計測用 with open(INPUT_CSV, "r", encoding="cp932", newline="") as fin: with open(OUTPUT_CSV, "w", encoding="cp932", newline="") as fout: reader = csv.DictReader(fin) writer = csv.DictWriter(fout, fieldnames=CSV_COLUMNS) writer.writeheader() for row in reader: row = ping(row) writer.writerow(row) print(row) print(f"実行時間 {time.time() - start:,.2f} 秒") # 実行時間計測用 if __name__ == "__main__": main()
実行結果:ping1_singlethread.py
実行結果は、下記のとおりです。宛先9個のうち、2個がOKで、その他はNGとなっています。 このときの実行時間は、22秒と実行に時間がかかっています。 これは、Ping NGが多く、タイムアウトを待っていたためです。
>python ping1_singlethread.py OrderedDict([('description', 'host01'), ('target', '192.168.10.1'), ('result', 'OK')]) OrderedDict([('description', 'host02'), ('target', '192.168.10.2'), ('result', 'NG')]) OrderedDict([('description', 'host03'), ('target', '192.168.10.3'), ('result', 'NG')]) OrderedDict([('description', 'host04'), ('target', '192.168.10.4'), ('result', 'OK')]) OrderedDict([('description', 'host05'), ('target', '192.168.10.5'), ('result', 'NG')]) OrderedDict([('description', 'host06'), ('target', '192.168.10.6'), ('result', 'NG')]) OrderedDict([('description', 'host07'), ('target', '192.168.10.7'), ('result', 'NG')]) OrderedDict([('description', 'host08'), ('target', '192.168.10.8'), ('result', 'NG')]) OrderedDict([('description', 'host09'), ('target', '192.168.10.9'), ('result', 'NG')]) 実行時間 22.87 秒
出力結果:ping_results.csv
ping_targets.csv
にresult
カラムが追加され、
Ping OKか、Ping NGかわかるようになりました。
description,target,result host01,192.168.10.1,OK host02,192.168.10.2,NG host03,192.168.10.3,NG host04,192.168.10.4,OK host05,192.168.10.5,NG host06,192.168.10.6,NG host07,192.168.10.7,NG host08,192.168.10.8,NG host09,192.168.10.9,NG
宛先リストにpingする(並行実行するパターン)
逐次実行の場合、タイムアウトが発生すると、実行時間がかなりかかるようになります。
数十秒程度であれば、待てますが、対象が多くなり、Ping NGが多発すると現実的ではありません。
このため、ping.exe
を並列実行するように変更しました。
プログラム:ping2_multithread.py
同時実行のために、
concurrent.futures.ThreadPoolExecutor
を利用しました。
THREAD_MAX_WORKER = 30
で、30個の宛先に同時にpingします。
大まかな動きとして、 CSVの宛先リストを読み込み、pingを実行するスレッドプールにジョブ(ping宛先)を登録し、すべてのジョブが完了したらCSVに結果を出力します。
import subprocess import csv import time from concurrent.futures import ThreadPoolExecutor CSV_COLUMNS = ["description", "target", "result"] INPUT_CSV = "ping_targets.csv" # カラム名にdescription、targetを期待。targetに対してping OUTPUT_CSV = "ping_results.csv" # カラム名はCSV_COLUMNS THREAD_MAX_WORKER = 30 def ping(row): # windowsのpingコマンドを実行する # ICMP Echo Requestを2回送出 "-n 2" # タイムアウトは1000ミリ秒 "-w 1000" # target宛に送信 # ping -n 2 -w 1000 target proc = subprocess.run( ["ping", "-n", "2", "-w", "1000", row["target"]], stdout=subprocess.PIPE, # 標準出力は判断のため保存 stderr=subprocess.DEVNULL # 標準エラーは捨てる ) # pingコマンドの実行結果(標準出力)に「TTL=」の文字列があれば、PING OKと判断 succeed = proc.stdout.decode("cp932").find("TTL=") > 0 row["result"] = "OK" if succeed else "NG" return row def main(): start = time.time() # 実行時間計測用 # THREAD_MAX_WORKER分のスレッドプールを起動してpingコマンドを実行する with ThreadPoolExecutor(max_workers=THREAD_MAX_WORKER) as executor: # CSVを読み込み、スレッドプールにキューイングする with open(INPUT_CSV, "r", encoding="cp932", newline="") as fin: reader = csv.DictReader(fin) # CSVの1行ずつping関数で実行 results = executor.map(ping, reader) # スレッドプールの実行結果を、CSVに書き込む with open(OUTPUT_CSV, "w", encoding="cp932", newline="") as fout: writer = csv.DictWriter(fout, fieldnames=CSV_COLUMNS) writer.writeheader() for result in results: row = result writer.writerow(row) print(row) print(f"実行時間 {time.time() - start:,.2f} 秒") # 実行時間計測用 if __name__ == "__main__": main()
実行結果:ping2_multithread.py
逐次実行と比べると、実行時間が7倍早くなっていることが確認できます。
>python ping2_multithread.py OrderedDict([('description', 'host01'), ('target', '192.168.10.1'), ('result', 'OK')]) OrderedDict([('description', 'host02'), ('target', '192.168.10.2'), ('result', 'NG')]) OrderedDict([('description', 'host03'), ('target', '192.168.10.3'), ('result', 'NG')]) OrderedDict([('description', 'host04'), ('target', '192.168.10.4'), ('result', 'OK')]) OrderedDict([('description', 'host05'), ('target', '192.168.10.5'), ('result', 'NG')]) OrderedDict([('description', 'host06'), ('target', '192.168.10.6'), ('result', 'NG')]) OrderedDict([('description', 'host07'), ('target', '192.168.10.7'), ('result', 'NG')]) OrderedDict([('description', 'host08'), ('target', '192.168.10.8'), ('result', 'NG')]) OrderedDict([('description', 'host09'), ('target', '192.168.10.9'), ('result', 'NG')]) 実行時間 3.08 秒
出力結果:ping_results.csv
出力結果は、逐次実行と同じ内容となります。
description,target,result host01,192.168.10.1,OK host02,192.168.10.2,NG host03,192.168.10.3,NG host04,192.168.10.4,OK host05,192.168.10.5,NG host06,192.168.10.6,NG host07,192.168.10.7,NG host08,192.168.10.8,NG host09,192.168.10.9,NG
SNMP Exporter用のYAMLファイルを作成
最後に、PrometheusのSNMP Exporter用のYAMLファイルを作成します。
Ping結果が記録されたping_results.csv
を読み込み、Ping OKの行のみ、snmp_targets.yaml
に出力します。
プログラム:generate_snmp_targets.py
今回、YAMLは単純な列挙だけであったため、ライブラリを使わず、直接出力しました。
import csv INPUT_CSV = "ping_results.csv" OUTPUT_YAML = "snmp_targets.yaml" with open(INPUT_CSV, "r", encoding="cp932", newline="") as fin: with open(OUTPUT_YAML, "w", encoding="utf-8", newline="") as fout: reader = csv.DictReader(fin) fout.write("---\n") fout.write("- targets:\n") for row in reader: if row["result"] == "OK": print(row) fout.write(f' - {row["target"]}\n') fout.write(" labels:\n") fout.write(" group: tokyo-routers\n")
実行結果:generate_snmp_targets.py
下記の通り、Ping OKであった宛先のみ、出力されています。
>python generate_snmp_targets.py OrderedDict([('description', 'host01'), ('target', '192.168.10.1'), ('result', 'OK')]) OrderedDict([('description', 'host04'), ('target', '192.168.10.4'), ('result', 'OK')])
出力結果:snmp_targets.yaml
下記の通り、Ping OKであった宛先をtargets
に記載しました。
--- - targets: - 192.168.10.1 - 192.168.10.4 labels: group: tokyo-routers
おわりに
数十台なら、手作業でYAMLファイルを作成したほうが、早くできると思います。 ただし、単純作業で楽しくないです。
今回のように単純な作業から徐々に自動化して、どんどん楽をしていきましょう。 一度、自動化の仕組みを作れば、月1回の棚卸しから、毎日の棚卸しや、1時間に一回の棚卸しができるようになります。 また、もっと作り込めば、監視登録の自動化もできます。 監視登録漏れによる、重大事故を防ぐためにも、皆様、積極的に自動化していきましょう。