kooshinlab / コーシンラボ

現役ネットワークエンジニアが、ネットワーク運用で必要になった技術の記事を書くブログです。

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種類を用意しました。

f:id:KOOSHIN:20200204203516p:plain
全体像

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時間に一回の棚卸しができるようになります。 また、もっと作り込めば、監視登録の自動化もできます。 監視登録漏れによる、重大事故を防ぐためにも、皆様、積極的に自動化していきましょう。