kooshinlab / コーシンラボ

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

コマンドの実行結果によってネットワーク監視灯(パトライト)を点滅させてみた

コマンドを実行した際の終了状態によって、ネットワーク監視灯(通称:パトライト)に操作して、視覚的に通知してみました。 例として、LinuxUbuntu)のfpingをインストールして、Ping OKなら緑点滅、Ping NGなら赤点滅する動画です。

はじめに

コマンドの実行に時間がかかった場合(例えば、rbenvによるRubyのインストールや、ネットワークが遅くてdocker pullが異常に時間がかかる場合など)、終わるまでTwitterなどで時間をつぶすことがあると思います。しかしながら、コマンドの終了に気が付かなく、Twitterで時間を溶かしてしまうことが私は多々あります。 このため、コマンドの実行終了をパトライト経由で通知してみました。

ネットワーク監視表示灯(以下、パトライト)にコマンドの実行が終了した場合に、緑点滅や赤点滅で気づけるように、ラッパーする感じのシェルスクリプトを作成してみました。

何かしらのコマンドを実行して、終了状態(exit status/return code)が、正常であればパトライトを緑点滅、異常であればパトライトを赤点滅させてみます。

パトライトをシェルから操作する方法は、下記を参照してください、

kooshin.hateblo.jp

動作環境

動作環境は下記のとおりです。 今回はLinux上で動作させています。

仕組み

自作したシェルスクリプトpatliteスクリプトの引数に、コマンドを指定すると、コマンドの終了状態に応じてパトライトを操作します。 コマンドの終了状態によって、パトライトを緑点滅や赤点滅させます。

f:id:KOOSHIN:20200211210434p:plain
仕組み

実行の流れ

大まかに下記のような流れでpatliteスクリプトは動作しています。

  1. patliteスクリプトの引数に、終了を通知したいコマンドを指定
  2. patliteスクリプトを実行
  3. 開始時にパトライトを緑点灯。それ以外は消灯
  4. 引数のコマンドを実行
  5. コマンドの終了状態(exit status/return code)をもとに判断
  6. 終了状態が0ならパトライトを緑点滅。0以外ならパトライトを赤点滅
  7. 割込シグナルで途中終了した場合、パトライトを赤点滅
  8. 終了

patliteスクリプト

シェルスクリプトpatliteスクリプトのコードは下記のとおりです。

変数として定義した、パトライトIPアドレスIPADDTCPポート番号PORTを変更することで、他の環境でも使えると思います。

終了状態 パトライトの操作
正常終了(0) 緑点滅(点滅パターン1)
異常終了(0以外) 赤点滅(点滅パターン2)
割込終了(シグナル割込) 赤点滅(点滅パターン1)
#!/bin/sh

# パトライトのIPアドレスとTCPポート番号
IPADDR="192.168.11.101"
PORT="10000"

# 緑の表示灯のみ点灯
/bin/echo -ne '\x58\x58\x53\x00\x00\x06\x00\x00\x01\x00\x00\x00' | /bin/nc -q0 $IPADDR $PORT

# 途中終了の場合、異常と判断し、赤の表示灯を点滅(点滅パターン1)
trap "/bin/echo -ne '\x58\x58\x53\x00\x00\x06\x02\x00\x00\x00\x00\x00' | /bin/nc -q0 $IPADDR $PORT; exit 1" 1 2 3 15

# 引数をコマンドとして実行
$*
RETURN_CODE=$?

case "$RETURN_CODE" in
    # return codeが0の場合は、正常と判断し、緑の表示灯を点滅(点滅パターン1)
    0 ) /bin/echo -ne '\x58\x58\x53\x00\x00\x06\x00\x00\x02\x00\x00\x00' | /bin/nc -q0 $IPADDR $PORT ;;

    # return codeが0以外の場合は、異常と判断し、赤の表示灯を点滅(点滅パターン2)
    * ) /bin/echo -ne '\x58\x58\x53\x00\x00\x06\x03\x00\x00\x00\x00\x00' | /bin/nc -q0 $IPADDR $PORT ;;
esac

exit $RETURN_CODE

実行例

サンプルとして、fpingをインストールして、pingしてみました。 patiliteスクリプトの引数に、各コマンドを指定しています。

# patliteスクリプトに実行権付与
$ chmod +x patlite

# fpingをインストール(ちょっと時間がかかる)
$ ./patlite sudo apt install fping

# Ping OKな例
$ ./patlite fping 192.168.10.1 -c 1

# Ping NGな例
$ ./patlite fping 192.168.10.2 -c 1

実行結果(ターミナル画面)

実行結果は、patliteスクリプトに実行コマンドを指定する以外、 特に変化はありません。

f:id:KOOSHIN:20200211211556p:plain
実行結果

実行結果(パトライト

パトライトとターミナル画面を撮影した動画です(Twitterを参照)。

おわりに

簡単な仕組みでパトライトを操作できるため、様々なことに応用できると思います。

きっと、ご自宅に必須のアイテムになること間違いなしです。 この機会に皆さんご自宅用に買いましょう。

ネットワーク監視表示灯(パトライト)をシェルから操作してみた

@wakamotojpさんのncコマンドを利用する例をもとにして、ネットワーク監視表示灯(通称:パトライト)をシェルから操作してみました。

はじめに

ネットワーク監視表示灯は、SOCKET通信による操作をサポートしています。 TCPUDPで、専用のコマンドを送信することで、パトライトの表示灯を操作できます。

下記ではSNMPを利用してパトライトを操作しましたが、もっと簡単にSOCKET通信を利用して、パトライトの表示灯を操作できます。

kooshin.hateblo.jp

ソケット通信(SOCKET通信)

ヤフオクで入手したパトライトのNHシリーズ「NHP-3FB1」は、SOCKET通信をサポートしています。

SOCKET通信で専用コマンドをパトライトに送信することで、パトライトの表示灯を操作できます。 NH-FBシリーズでは、2種類のコマンドをサポートしています。

コマンド 詳細
PHNコマンド PHN-3FBシリーズの制御プロトコル。 対応する表示灯は緑・黄・赤のみ。点滅パターン2が利用できない
PNSコマンド NHシリーズの表示灯・ブザーを制御する専用コマンド。すべての表示灯と状態に対応

ソケット通信設定

SOCKET通信を利用するために、Webブラウザからパトライトの設定を下記のようにします。 デフォルト値のまま利用します。

  • セットアップ項目>ソケット通信設定

f:id:KOOSHIN:20200210215059p:plain
ソケット通信設定

PHNコマンド

PHNコマンドは、古いパトライトのPNH-3FBシリーズ用のコマンドです。 2バイトのバイナリデータをTCP/UDPで送信することで、表示灯を操作します。

下図の通り、1バイト目は固定値で、2バイト目のビットで表示灯の状態を指定します。

古い機種用のため、NHシリーズの点滅パターン2などに追従できていません。対応していない表示灯の状態は下記のとおりです。

  • 表示灯の赤・黄・緑の点滅パターン2
  • 表示灯の青・白の点灯・点滅パターン1・点滅パターン2
  • ブザーの鳴動パターン3、鳴動パターン4

f:id:KOOSHIN:20200210215631p:plain
PNHコマンド

表示灯の状態をビットで指定

nc(netcat)コマンドで、表示灯の状態指定のバイナリデータをTCPで送信します。 ここでは、指定しやすいよう、2バイト目は2進数のビットで指定できるようにしています。

echo -ne '\x57\x'$(echo "obase=16;ibase=2;00000001" | bc) | nc -q1 192.168.11.101 10000

echoコマンドで、バイナリデータをncコマンドにパイプで渡しています。 \x57は固定値です。

下記の部分で表示灯の状態を2進数のビットで指定しています。

$(echo "obase=16;ibase=2;00000001" | bc)

00000001部分で、表示灯の状態を2進数のビットを指定しています。この例は、赤の表示灯を点灯する意味となります。

表示灯の制御コマンドを送信

ncコマンドで、宛先IPアドレスと宛先TCPポート番号を指定しています。

nc -q1 192.168.11.101 10000

192.168.11.101が宛先IPアドレスで、10000が宛先TCPポート番号です。

実行例

下記の例は、表示灯の赤を制御しています。 点灯・消灯・点滅パターン1のみ指定できます。

# 赤点灯、それ以外は消灯
echo -ne '\x57\x'$(echo "obase=16;ibase=2;00000001" | bc) | nc -q1 192.168.11.101 10000

# 赤点滅(パターン1)、それ以外は消灯
echo -ne '\x57\x'$(echo "obase=16;ibase=2;00100000" | bc) | nc -q1 192.168.11.101 10000

# 緑・黄・赤 消灯
echo -ne '\x57\x'$(echo "obase=16;ibase=2;00000000" | bc) | nc -q1 192.168.11.101 10000
PNSコマンド

PNSコマンドは、NHシリーズの表示灯・ブザーを制御するコマンドです。 PNHコマンドと同じポート番号でバイナリデータを送信します。

下図の通り、合計12バイトのバイナリデータで、表示灯の状態指定は後半6バイトを利用します。 表示灯ごとに状態を1バイトで指定できるため、点滅パターン2に対応しています。

f:id:KOOSHIN:20200210215257p:plain
PNSコマンド

表示灯の状態を16進数で指定

echoコマンドで、16進数のバイナリデータをベタ打ちしています。 表示灯の状態指定の場合、前半6バイトx58\x58\x53\x00\x00\x06は固定値です。 後半6バイトで表示灯ごとの状態を指定しています。

表示灯の状態をクリアするコマンドのみ、全体で6バイトの\x58\x58\x43\x00\x00\x00となります。

# 赤点灯
echo -ne '\x58\x58\x53\x00\x00\x06\x01\x09\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 赤点滅パターン1
echo -ne '\x58\x58\x53\x00\x00\x06\x02\x09\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 赤点滅パターン2
echo -ne '\x58\x58\x53\x00\x00\x06\x03\x09\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 黄点灯
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x01\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 黄点滅パターン1
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x02\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 黄点滅パターン2
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x03\x09\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 緑点灯
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x09\x01\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 緑点滅パターン1
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x09\x02\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 緑点滅パターン2
echo -ne '\x58\x58\x53\x00\x00\x06\x09\x09\x03\x09\x09\x09' | nc -q1 192.168.11.101 10000

# 表示灯をクリア(リセット)
echo -ne '\x58\x58\x43\x00\x00\x00' | nc -q1 192.168.11.101 10000
表示灯の操作結果

表示灯の操作で、下記の状態になるように指定した結果は下記のとおりです。

  • 赤:点滅パターン1
  • 黄:点滅パターン2
  • 緑:点灯
  • その他消灯
# 赤点滅パターン1、黄点滅パターン2、緑点灯、その他消灯
echo -ne '\x58\x58\x53\x00\x00\x06\x02\x03\x01\x00\x00\x00' | nc -q1 192.168.11.101 10000

おわりに

nc(netcat)コマンドを利用することで、シェルから簡単にパトライトを操作することができます。 長時間かかるコマンドの実行が完了したら、 パトライトを光らせてみたり、 CI/CDの失敗時に光らせてみたり、様々なことに活用できます。

皆さん、ぜひとも自宅でパトライトを活用してみてください。

ネットワーク監視表示灯(パトライト)をSNMPで操作してみた

三色に光るネットワーク監視表示灯(通称:パトライト)を買ったのでSNMPでリモートから操作してみました。パトライトの光は結構眩しいです。

はじめに

パトライト社のネットワーク監視表示灯(以下、パトライト)をヤフオクで落札し、自宅で使ってみました。

ヤフオクで入手した「NHP-3FB1」はすでに生産終了品のため新規購入はできません。 説明書やファームウェアは、パトライト社の公式サイトからダウンロード可能です(2020年2月9日時点)。

パトライトの設定

取扱説明書に様々な設定方法や操作方法が載っています。 パトライト社で無料の会員登録をすることで、取扱説明書はダウンロードできます。 一応、個人でも会員登録可能でした。

パトライトの初期化

パトライトに設定が残っていたので、 工場出荷時状態に初期化します。 初期化の手順は下記のとおりです。

  1. 電源ケーブルを抜く
  2. ボリューム切り替えスイッチ(VOL.)を「OFF」にする
  3. CLEARスイッチとTESTスイッチを押しながら、電源ケーブルを差し込む
  4. 表示灯が全点灯した状態でブザー音が鳴ります。ブザー音が止まったら、2つのスイッチを離す
  5. 表示灯が消灯すると起動完了
Webブラウザパトライトにログイン

初期化後のパトライトIPアドレスは192.168.10.1/24です。 パソコンに192.168.10.2/24など、同じセグメントのIPアドレスを割り当て、LANケーブルを接続します。 Webブラウザから「http://192.168.10.1」にアクセスすると、下記のログイン画面が表示されます。 初期化後のパスワードはpatliteで、ログインできます。

f:id:KOOSHIN:20200209174319p:plain
ログイン画面

WebブラウザSNMP設定

ログイン後は、Webブラウザで「SNMP設定」をします。 今回は動作試験が目的のため、下記の通り、デフォルトの値のまま利用します。

  • セットアップ項目>SNMP設定
    • SNMP機能:有効
    • SETコミュニティ:private
    • GETコミュニティ:public

f:id:KOOSHIN:20200209171314p:plain
SNMP設定画面

WebブラウザIPアドレス設定

「システム設定」からIPアドレスが変更できます。 今回は、下記のようにIPアドレスを設定しました。 IPアドレス設定を変更した場合は、設定反映のため再起動します。

f:id:KOOSHIN:20200209175138p:plain
システム設定画面

f:id:KOOSHIN:20200209221322p:plain
再起動画面

Webブラウザで表示灯操作

「表示灯操作」から表示灯の操作ができるため、手軽に動作確認できます。 下記の画面では、下記のように操作しています。

  • 本体操作>表示灯操作
    • 赤:点滅パターン1
    • 黄:点灯

f:id:KOOSHIN:20200209175434p:plain
表示灯操作画面

SNMPの準備

SNMPで表示灯を操作してみます。 動作環境は、Windows10上のWSLにインストールしたUbuntu18.04で、net-snmpを利用します。

net-snmpのインストール

Ubuntu 18.04のnet-snmpsnmp-mibs-downloaderを インストールします。 ついでに、Cisco社のMIBもダウンロードします。 MIBファイルのダウンロードはsnmp-mibs-downloaderを利用します。

# パッケージをインストール
$ sudo apt install net-snmp snmp-mibs-downloader

# Cisco社のMIBのダウンロード設定
$ cat << END | sudo tee /etc/snmp-mibs-downloader/cisco.conf
HOST=ftp://ftp.cisco.com
DIR=pub/mibs/v2/
CONF=ciscolist
DEST=cisco
END

# Cisco社のダウンロードするMIBを指定
$ cat << END | sudo tee /etc/snmp-mibs-downloader/ciscolist
CISCO-IMAGE-MIB.my CISCO-IMAGE-MIB
CISCO-SMI.my CISCO-SMI
MPLS-VPN-MIB.my MPLS-VPN-MIB
CISCO-BGP4-MIB.my CISCO-BGP4-MIB
END

# MIBのダウンロード
$ sudo download-mibs cisco

# エラーを吐くMIBを削除
$ sudo rm /var/lib/snmp/mibs/ietf/IPSEC-SPD-MIB
$ sudo rm /var/lib/snmp/mibs/ietf/SNMPv2-PDU
$ sudo rm /var/lib/snmp/mibs/ietf/IPATM-IPMC-MIB
$ sudo rm /var/lib/snmp/mibs/iana/IANA-IPPM-METRICS-REGISTRY-MIB

# SNMPコマンドの共通設定
$ cat << END | sudo tee /etc/snmp/snmp.conf
mibdirs /var/lib/snmp/mibs/iana:/var/lib/snmp/mibs/ietf:/var/lib/snmp/mibs/cisco
mibs all
showMibErrors no
mibWarningLevel 0
strictCommentTerm no
mibAllowUnderline no
END
MIBファイル

SNMP用のMIBファイルは、ファームウェアに同梱されており、パトライト社から会員登録することでダウンロードできます。 「NH-FBシリーズ用ファームウェア」から「バージョン:1.46」をダウンロードしました。

ファームウェアにMIBファイルpatlite_nhSPL_20170105.mibが同梱されています。 今回はMIBファイルを/var/lib/snmp/mibs/iana/に保存しました。

f:id:KOOSHIN:20200209171550p:plain
ファームウェアのダウンロード画面

SNMPで操作

取扱説明書にSNMPのOID情報が記載されています。 今回はnet-snmpツールのsnmpsetコマンドで表示灯を操作してみます。

SNMPオブジェクト

表示灯を制御するSNMPオブジェクトは下記のとおりです。

  • 表示灯の状態を変える場合、必ずcontrolLightControlStatecontrolLightControlTimerの2つの指定が必要です。
  • 表示等をクリア(リセット)する場合、controlLightSnmpClearの1つのみの指定です。
SNMPオブジェクト名 OID 内容 操作
controlLightControlState .1.3.6.1.4.1.20440.4.1.5.1.2.1.2 各表示灯の状態を設定。遅延時間とセットで利用する必要あり turn-on、turn-off、blinking-pattern1、blinking-pattern2
controlLightControlTimer .1.3.6.1.4.1.20440.4.1.5.1.2.1.3 状態変化の遅延時間を設定 秒数を指定
controlLightSnmpClear .1.3.6.1.4.1.20440.4.1.5.1.3 状態をクリアする 値はexecute(1)
色指定

表示灯の色は下記のパラメータが用意されています。 上記のSNMPのオブジェクト名の末尾に追加することで、 表示灯を指定できます。 赤の表示灯はcontrolLightControlState.redcontrolLightControlState.yellowで指定できます。

パラメータ 値(Integer) 表示灯
red 1 赤色の表示灯
yellow 2 黄色の表示灯
green 3 緑色の表示灯
blue 4 青色の表示灯
clear 5 白色の表示灯
buzzar 6 ブザー
状態指定

controlLightControlStateに指定可能な状態は下記のとおりです。 表示灯とブザーの場合で、動作が異なります。

パラメータ 値(Integer) 表示灯の動作 ブザーの動作
turn-on 1 表示灯を点灯 消音
turn-off 2 表示灯を消灯 サウンドパターン1
blinking-pattern1 3 表示灯を点滅(点滅パターン1) サウンドパターン2
blinking-pattern2 5 表示灯を点滅(点滅パターン2) サウンドパターン3
soud-pattern4 6 - サウンドパターン4
nop 4 状態変化なし 状態変化なし
snmpsetコマンドで表示灯を操作

snmpsetコマンドで、赤の表示灯を操作してみます。 他の表示灯に変更する場合は、controlLightControlState.redcontrolLightControlTimer.redの末尾の.red.yellow.greenに変更します。

# 表示灯をすべて消灯(クリア)する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightSnmpClear.0 i execute
PATLITE-NH-SERIES::controlLightSnmpClear.0 = INTEGER: execute(1)

# 赤をすぐに点灯する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.red i turn-on  controlLightControlTimer.red i 0
PATLITE-NH-SERIES::controlLightControlState.red = INTEGER: turn-on(2)
PATLITE-NH-SERIES::controlLightControlTimer.red = INTEGER: 0

# 赤をすぐに点滅パターン1で点滅する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.red i blinking-pattern1  controlLightControlTimer.red i 0
PATLITE-NH-SERIES::controlLightControlState.red = INTEGER: blinking-pattern1(3)
PATLITE-NH-SERIES::controlLightControlTimer.red = INTEGER: 0

# 赤をすぐに点滅パターン2で点滅する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.red i blinking-pattern2  controlLightControlTimer.red i 0
PATLITE-NH-SERIES::controlLightControlState.red = INTEGER: blinking-pattern2(5)
PATLITE-NH-SERIES::controlLightControlTimer.red = INTEGER: 0

# 赤をすぐに消灯する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.red i turn-off  controlLightControlTimer.red i 0
PATLITE-NH-SERIES::controlLightControlState.red = INTEGER: turn-off(1)
PATLITE-NH-SERIES::controlLightControlTimer.red = INTEGER: 0
表示灯の操作結果

表示灯の操作で、下記の状態になるように指定した結果は下記のとおりです。

  • 状態クリア
  • 赤:点滅パターン1
  • 黄:点滅パターン2
  • 緑:点灯
# 表示灯をすべて消灯(クリア)する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightSnmpClear.0 i execute
PATLITE-NH-SERIES::controlLightSnmpClear.0 = INTEGER: execute(1)

# 赤をすぐに点滅パターン1で点滅する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.red i blinking-pattern1  controlLightControlTimer.red i 0
PATLITE-NH-SERIES::controlLightControlState.red = INTEGER: blinking-pattern1(3)
PATLITE-NH-SERIES::controlLightControlTimer.red = INTEGER: 0

# 黄をすぐに点滅パターン2で点滅する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.yellow i blinking-pattern1  controlLightControlTimer.yellow i 0
PATLITE-NH-SERIES::controlLightControlState.yellow = INTEGER: blinking-pattern1(3)
PATLITE-NH-SERIES::controlLightControlTimer.yellow = INTEGER: 0

# 緑をすぐに点灯する
$ snmpset -v2c -cprivate 192.168.11.102 controlLightControlState.green i turn-on  controlLightControlTimer.green i 0
PATLITE-NH-SERIES::controlLightControlState.green = INTEGER: turn-on(2)
PATLITE-NH-SERIES::controlLightControlTimer.green = INTEGER: 0

おわりに

ネットワーク監視表示灯を利用して、様々なイベントの通知を視覚的にわかりやすく通知することが可能になります。色々使いみちがたくさんあるので、活用できると思います。

ネットワークエンジニアのためのIPアドレス表現 for Python

Pythonipaddressライブラリで、 ネットワークエンジニアがよく使うIPアドレスを表現する方法を例示します。

from ipaddress import ip_interface

>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

はじめに

PythonIPアドレスを取り扱う方法を調査してみました。 過去に、Rubyで調査したので、ほぼ同じことをできるか、Pythonで調査しました。

Python3.3から標準モジュールとしてipaddressライブラリが追加となっています。 より詳細な解説は、ライブラリのドキュメントを参照してください。

ここでは、Python3.7.4の環境で調査しました。

IPアドレスの表現方法

ipaddressライブラリには、IPアドレスを表現するために、3つの関数(ファクトリ関数)が定義されています。

ファクトリ関数 生成されるクラス 用途
ip_address() IPv4Address, IPv6Address IPv4/IPv6のホストアドレスを表現する。マスク情報は付加できない。
ip_network() IPv4Network, IPv6Network IPv4/IPv6のネットワークアドレスを表現する。IPプレフィックスとしてよく使う表現。ホストアドレスは表現できない。
ip_interface() IPv4Interface, IPv6Interface IPv4/IPv4のインタフェースアドレスを表現する。ルータのIPアドレスを扱うのならこっち。

ここでは、取り扱いやすいip_interface()関数を主で利用します。必要に応じて、他の関数を利用してください。 ip_interface()関数から生成されるIPv4Interfaceクラスから、IPv4AddressクラスとIPv4Networkクラスが生成できます。

from ipaddress import (
    ip_network,
    ip_address,
    ip_interface
)

# ホストアドレス(IPv4Address)
>>> ip_address("192.168.1.124")
IPv4Address('192.168.1.124')

# ネットワークアドレス(IPv4Network)
>>> ip_network("192.168.1.0/24")
IPv4Network('192.168.1.0/24')

# インタフェースアドレス(IPv4Interface)
>>> ip_interface("192.168.1.124/24")
IPv4Interface('192.168.1.124/24')


# ip_network()はネットワークアドレスを指定しないといけない
# ホストアドレスを指定すると、デフォルトはエラーとなる
>>> ip_network("192.168.1.124/24")
ValueError: 192.168.1.124/24 has host bits set

# strict=Falseを指定すると、ネットワークアドレスに変換する
>>> ip_network("192.168.1.124/24", strict=False)
IPv4Network('192.168.1.0/24')


# IPv4Interfaceクラスから、IPv4Addressクラスを生成
#  ip_interface() => ip_address()に変換
>>> ip = ip_interface("192.168.1.124/24")
>>> ip.ip
IPv4Address('192.168.1.124')

# IPv4Interfaceクラスから、IPv4Networkクラスを生成
#  ip_interface() => ip_network()に変換
>>> ip = ip_interface("192.168.1.124/24")
>>> ip.network
IPv4Network('192.168.1.0/24')
色々なIPアドレスの読み込み

ip_interface()関数を前提として、IPアドレスを読み込みます。

from ipaddress import ip_interface


# プレフィックス長を指定したIPアドレス
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

# プレフィックス長を省略すると/32になる
>>> ip = ip_interface("192.168.1.124")
>>> ip
IPv4Interface('192.168.1.124/32')
>>> str(ip)
'192.168.1.124/32'

# サブネットマスクで指定も可能
>>> ip = ip_interface("192.168.1.124/255.255.255.0")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> str(ip)
'192.168.1.124/24'

# 10進数からIPアドレスに変換
>>> ip = ip_interface(0)
>>> ip
IPv4Interface('0.0.0.0/32')
>>> str(ip)
'0.0.0.0/32'

# 16進数からIPアドレスに変換
>>> ip = ip_interface(0xffffffff)
>>> ip
IPv4Interface('255.255.255.255/32')
>>> str(ip)
'255.255.255.255/32'
よく使うIPアドレスの表現
from ipaddress import ip_interface


# IPアドレス指定
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')


# プレフィックス表記(1)
>>> str(ip)
'192.168.1.124/24'

# プレフィックス表記(2)
>>> ip.with_prefixlen
'192.168.1.124/24'

# サブネットマスク表記
>>> ip.with_netmask
'192.168.1.124/255.255.255.0'

# ホストアドレス
>>> ip.ip
IPv4Address('192.168.1.124')
>>> str(ip.ip)
'192.168.1.124'

# サブネットマスク
>>> ip.netmask
IPv4Address('255.255.255.0')
>>> str(ip.netmask)
'255.255.255.0'

# プレフィックス長
>>> ip.network.prefixlen
24

# ネットワークアドレス
>>> ip.network
IPv4Network('192.168.1.0/24')
>>> str(ip.network)
'192.168.1.0/24'
>>> ip.network.network_address
IPv4Address('192.168.1.0')
>>> str(ip.network.network_address)
'192.168.1.0'

# ホストマスク/ワイルドカードマスク
>>> ip.hostmask
IPv4Address('0.0.0.255')
>>> str(ip.hostmask)
'0.0.0.255'

# ブロードキャストアドレス(1)
>>> ip.network.broadcast_address
IPv4Address('192.168.1.255')
>>> str(ip.network.broadcast_address)
'192.168.1.255'

# ブロードキャストアドレス(2)
>>> ip_interface("0.0.0.0/0").network.broadcast_address
IPv4Address('255.255.255.255')
>>> str(ip_interface("0.0.0.0/0").network.broadcast_address)
'255.255.255.255'
その他の表現
from ipaddress import ip_interface


# IPアドレス指定
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')


# 数値
>>> int(ip)
3232235900

# DNS逆引きレコード
>>> ip.reverse_pointer
'124/24.1.168.192.in-addr.arpa'

# ホストアドレスの配列(ネットワークアドレスとブロードキャストを除く)
>>> ip.network.hosts()
<generator object _BaseNetwork.hosts at 0x7f87841d2430>
>>> list(ip.network.hosts())
[IPv4Address('192.168.1.1'), IPv4Address('192.168.1.2'), ~略~, IPv4Address('192.168.1.254')]


# 192.168.1.254~192.168.2.2までを表現
>>> ip = ip_interface("192.168.1.254/24")
>>> ip + 1
IPv4Interface('192.168.1.255/32')
>>> ip + 2
IPv4Interface('192.168.2.0/32')

>>> for i in range(5):
...     ip + i
...
IPv4Interface('192.168.1.254/32')
IPv4Interface('192.168.1.255/32')
IPv4Interface('192.168.2.0/32')
IPv4Interface('192.168.2.1/32')
IPv4Interface('192.168.2.2/32')
プレフィックスの表記

プレフィックスの計算には、ip_network()関数が便利なため、 ここでは、ip_network()を利用します。

サマライズする関数はcollapse_addresses()関数です。 collapse_addressesに、IPv4Network形式のアドレスリストを渡すことで、 適時サマライズしてくれます。

プレフィックスの分割は、IPv4Network.subnets()関数です。 現在のプレフィックス長に値を足す場合はprefixlen_diff=パラメータを使います。 新しいプレフィックス長にする場合はnew_prefix=パラメータを使います。

from ipaddress import (
    ip_network, # ★ip_network()関数を利用
    collapse_addresses,
)

# 複数のプレフィックスをサマライズする
>>> prefixes = map(ip_network, [
...     "192.168.0.0/25",
...     "192.168.0.128/25",
...     "192.168.1.0/24",
...     "192.168.3.0/24",
...     "192.168.4.0/24",
...     "192.168.5.0/26",
...     "192.168.128.0/22",
...     "192.168.132.0/22",
...     "192.168.128.0/21",
... ])
>>> summarized_networks = collapse_addresses(prefixes)
>>> list(summarized_networks)
[IPv4Network('192.168.0.0/23'), 
IPv4Network('192.168.3.0/24'), 
IPv4Network('192.168.4.0/24'), 
IPv4Network('192.168.5.0/26'), 
IPv4Network('192.168.128.0/21')]



# プレフィックスを分割する(プレフィックス長を増やす)
>>> prefix = ip_network("192.168.0.0/24")
# プレフィックス長に+1して、プレフィックスを分割(/24 => /25)
>>> list(prefix.subnets(prefixlen_diff=1))
[IPv4Network('192.168.0.0/25'), IPv4Network('192.168.0.128/25')]
# プレフィックス長に+2して、プレフィックスを分割(/24 => /26)
>>> list(prefix.subnets(prefixlen_diff=2))
[IPv4Network('192.168.0.0/26'), IPv4Network('192.168.0.64/26'), IPv4Network('192.168.0.128/26'), IPv4Network('192.168.0.192/26')]
# 新しいプレフィックス長を27にして、プレフィックスを分割(/24 => /27)
>>> list(prefix.subnets(new_prefix=27))
[IPv4Network('192.168.0.0/27'), IPv4Network('192.168.0.32/27'), IPv4Network('192.168.0.64/27'), IPv4Network('192.168.0.96/27'), IPv4Network('192.168.0.128/27'), IPv4Network('192.168.0.160/27'), IPv4Network('192.168.0.192/27'), IPv4Network('192.168.0.224/27')]


# プレフィックスのスーパーネットを求める(プレフィックス長を減らす)
>>> prefix = ip_network("192.168.0.0/24")
# プレフィックス長を-1にする(/24 => /23)
>>> prefix.supernet(prefixlen_diff=1)
IPv4Network('192.168.0.0/23')
# プレフィックス長を-2にする(/24 => /22)
>>> prefix.supernet(prefixlen_diff=2)
IPv4Network('192.168.0.0/22')
# プレフィックス長を/21にする(/24 => /21)
>>> prefix.supernet(new_prefix=21)
IPv4Network('192.168.0.0/21')


# プレフィックスから、特定のプレフィックスを除外する
>>> prefix1 = ip_network("192.168.0.0/24")
>>> prefix2 = ip_network("192.168.0.32/27")
>>> prefix1.address_exclude(prefix2)
<generator object _BaseNetwork.address_exclude at 0x00000286C6AD4BC8>
>>> list(prefix1.address_exclude(prefix2))
[IPv4Network('192.168.0.128/25'), IPv4Network('192.168.0.64/26'), IPv4Network('192.168.0.0/27')]
IPアドレスの判定
from ipaddress import ip_network


# プレフィックスが、サブネットの範囲内か判定(右辺に左辺が含まれるか?)
>>> ip1 = ip_network("192.168.0.0/24")
>>> ip2 = ip_network("192.168.0.32/27")
>>> ip1.subnet_of(ip2)
False
>>> ip2.subnet_of(ip1)
True


# プレフィックスが、スパーネットの範囲内か判定(左辺に右辺が含まれるか?)
>>> ip1 = ip_network("192.168.0.0/24")
>>> ip2 = ip_network("192.168.0.32/27")
>>> ip1.supernet_of(ip2)
True
>>> ip2.supernet_of(ip1)
False

各クラスで使える関数一覧

各クラス(IPv4Interface、IPv4Network、IPv4Address)に定義されている変数と関数一覧です。

from ipaddress import (
    ip_network,
    ip_address,
    ip_interface
)


# IPv4Interface
>>> ip_interface("192.168.1.124/24")
IPv4Interface('192.168.1.124/24')
>>> ip = ip_interface("192.168.1.124/24")
>>> ip
IPv4Interface('192.168.1.124/24')
>>> ip.
ip.compressed      ip.is_global       ip.is_private      ip.netmask         ip.version
ip.exploded        ip.is_link_local   ip.is_reserved     ip.network         ip.with_hostmask
ip.hostmask        ip.is_loopback     ip.is_unspecified  ip.packed          ip.with_netmask
ip.ip              ip.is_multicast    ip.max_prefixlen   ip.reverse_pointer ip.with_prefixlen


# IPv4Network
>>> ip = ip_network("192.168.1.124/24", strict=False)
>>> ip
IPv4Network('192.168.1.0/24')
>>> ip.
ip.address_exclude(  ip.hosts(            ip.is_reserved       ip.overlaps(         ip.supernet_of(
ip.broadcast_address ip.is_global         ip.is_unspecified    ip.prefixlen         ip.version
ip.compare_networks( ip.is_link_local     ip.max_prefixlen     ip.reverse_pointer   ip.with_hostmask
ip.compressed        ip.is_loopback       ip.netmask           ip.subnet_of(        ip.with_netmask
ip.exploded          ip.is_multicast      ip.network_address   ip.subnets(          ip.with_prefixlen
ip.hostmask          ip.is_private        ip.num_addresses     ip.supernet(


# IPv4Address
>>> ip = ip_address("192.168.1.124")
>>> ip
IPv4Address('192.168.1.124')
>>> ip.
ip.compressed      ip.is_link_local   ip.is_private      ip.max_prefixlen   ip.version
ip.exploded        ip.is_loopback     ip.is_reserved     ip.packed
ip.is_global       ip.is_multicast    ip.is_unspecified  ip.reverse_pointer

付録:シスコルータのコンフィグを生成する

CSVファイルからコンフィグのパラメータを読み取り、 Jinja2テンプレートで、コンフィグを生成します。 今回は、インタフェースの設定と、OSPFの設定用のシスコルータのコンフィグを生成します。

入力CSVファイル:ospf_interfaces.csv

CSVのカラムは、下記の表のとおりです。

カラム名
interface インタフェース名、GigabitEthernet0/0
ipaddress インタフェースに割り当てるIPアドレスプレフィックス長付き)、192.168.0.1/24
ospfarea インタフェースが所属するOSPFエリア番号、0
interface,ipaddress,ospfarea
Loopback0,192.168.0.1/32,0
GigabitEthernet0/0,192.168.101.1/24,0
GigabitEthernet0/1,192.168.102.1/24,0
GigabitEthernet1/0,192.168.201.1/24,100
GigabitEthernet1/1,192.168.202.1/24,100
プログラム:ospf_template.py
import csv
from ipaddress import ip_interface
from jinja2 import Template


# CSVからパラメータ読み込み
# Excelで作成したCSVの場合、BOM付きのUTF-8で出力されるため、encoding="utf_8_sig"にする
params = []
with open("ospf_interfaces.csv", "r", encoding="utf_8_sig", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        ip = ip_interface(row["ipaddress"])
        print(row)
        param = {
            "ip": ip,
            "interface": row["interface"],
            "ospfarea": row["ospfarea"]
        }
        params.append(param)

# コンフィグのテンプレート
CONFIG_TEMPLATE = """
{%- for p in params %}
interface {{ p["interface"] }}
 ip address {{ p["ip"].ip }} {{ p["ip"].netmask }}
{% endfor %}

router ospf 1
 passive-interface default
{%- for p in params %}
 no passive-interface {{ p["interface"] }}
{%- endfor %}
{%- for p in params %}
 network {{ p["ip"].network.network_address }} {{ p["ip"].hostmask }} area {{ p["ospfarea"] }}
{%- endfor %}
"""

# テンプレートにパラメータを渡して、コンフィグに変換する
template = Template(CONFIG_TEMPLATE)
output = template.render({"params": params})
print("=" * 60)
print(output)
print("=" * 60)
実行結果

CSVファイルから各パラメータを読み取り、コンフィグが生成されました。

>python ospf_template.py
OrderedDict([('interface', 'Loopback0'), ('ipaddress', '192.168.0.1/32'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet0/0'), ('ipaddress', '192.168.101.1/24'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet0/1'), ('ipaddress', '192.168.102.1/24'), ('ospfarea', '0')])
OrderedDict([('interface', 'GigabitEthernet1/0'), ('ipaddress', '192.168.201.1/24'), ('ospfarea', '100')])
OrderedDict([('interface', 'GigabitEthernet1/1'), ('ipaddress', '192.168.202.1/24'), ('ospfarea', '100')])
============================================================

interface Loopback0
 ip address 192.168.0.1 255.255.255.255

interface GigabitEthernet0/0
 ip address 192.168.101.1 255.255.255.0

interface GigabitEthernet0/1
 ip address 192.168.102.1 255.255.255.0

interface GigabitEthernet1/0
 ip address 192.168.201.1 255.255.255.0

interface GigabitEthernet1/1
 ip address 192.168.202.1 255.255.255.0


router ospf 1
 passive-interface default
 no passive-interface Loopback0
 no passive-interface GigabitEthernet0/0
 no passive-interface GigabitEthernet0/1
 no passive-interface GigabitEthernet1/0
 no passive-interface GigabitEthernet1/1
 network 192.168.0.1 0.0.0.0 area 0
 network 192.168.101.0 0.0.0.255 area 0
 network 192.168.102.0 0.0.0.255 area 0
 network 192.168.201.0 0.0.0.255 area 100
 network 192.168.202.0 0.0.0.255 area 100
============================================================

おわりに

ipaddressライブラリは、Python3.3から標準ライブラリとなっているので、非常に使いやすいと思います。 下記の参考文献の通り、便利でした!

参考文献

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)が必要なため、踏み台サーバ経由など、お手軽に実行できない場合があります。

このため、Windowsping.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.1ping.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.1192.168.10.9までのIPアドレスリストを作成します。 IPアドレス以外にも連続性がある場合は、ホスト名に連番をつけて、作成しても良いと思います。

ファイルは編集しやすいようにCSV形式で保存します。 CSV形式は下記のような2つのカラム(列)を定義しました。

カラム名 詳細
description ルータを識別する名前、愛称など
target Pingの宛先。IPアドレスやホスト名を指定
プログラム:generate_csv.py

実行すると、targetとして、192.168.10.1192.168.10.9までのリストをCSVとして、 ping_targets.csvファイルに保存します。 このとき、descriptionには、host01host09が同時につけられます。

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

Python/Netmiko/TextFSM/NTC-Templatesでシスコルータのshow ip routeをExcel/CSVに変換してみた

Python/Netmiko/TextFSM/NTC-Templatesを利用して、シスコルータのshow ip routeの結果をExcel/CSVに変換してみました。 Netmiko v2.0.0(2017/12/15にリリース)の新機能(Integrate TextFSM to send_command and send_command_timing)を利用しています。

f:id:KOOSHIN:20171216233436p:plain
show ip routeのCSV変換結果

はじめに

ネットワークエンジニアの皆様、ルータのshow ip routeの結果をExcelにコピペする作業をよくやってないでしょうか? 私は経路差分のチェックやお客様に経路の一覧を提出するために、よくやってます。

100経路程度なら、なんとか手作業でもできると思います。 しかし、100経路を超えてくるとどうでしょうか? 非常に退屈な作業で、非常に時間がかかると思います。 さらに、コピペミスもしてしまうかもしれません。 ミスの対策として、ダブルチェックしないといけないかもしれません。

そんな退屈で面倒な作業をツールを使って解決します。 ツールで、show ip routeの結果を、Excel/CSVに変換します。

今回は、PythonライブラリのNetmikoを使います。 2017/12/15にリリースされたv2.0.0で、TextFSMを組み込まれたので使ってみます。

仕組み

show ip routeExcel/CSVに変換する仕組みは下記のとおりです。

  1. Netmikoで、シスコルータにTelnet/SSHでログイン
  2. show ip routeコマンド実行
  3. TextFSM+NTC-Templatesで実行結果から抽出
  4. 抽出したデータをExcel/CSV形式で保存

f:id:KOOSHIN:20171216233906p:plain
仕組み

TextFSM + NTC-Templates=最強

シスコルータのshow ip routeと、TextFSM+NTC-Templatesの相性は抜群です。

少なくとも、サブネットマスク長を補完してくれる機能は非常にありがたい機能です。

サブネットマスク長が同じサブネットがある場合、サブネットマスク長が省略されます。 show ip routeは、同じサブネットマスク長の経路があった場合、自動的にグループ化され、省略されます。 下記のように192.168.0.1/30は、192.168.0.1で、サブネットマスク/30は省略されています。

NTC-Templatesの場合、省略されたサブネットマスク長を補完してくれます。

 csr1000v-1#show ip route
~略~
     192.168.0.0/32 is subnetted, 8 subnets
O        192.168.0.1 [110/4] via 10.0.0.34, 1w1d, GigabitEthernet3
O        192.168.0.2 [110/2] via 10.0.0.34, 1w4d, GigabitEthernet3
〜略〜

CSV出力内容

取得できるフィールドは下記のとおりです。

フィールド名 内容
PROTOCOL 経路情報を学習したプロトコル
TYPE プロトコルの付加情報。OSPFならIAやE2など
NETWORK ネットワークアドレス
MASK サブネットマスク
DISTANCE AD(アドミニストレーティブディスタンス)
METRIC 経路のメトリック
NEXTHOP_IP ネクストホップのIPアドレス
NEXTHOP_IF 出力インタフェース
UPTIME 経路の学習時間

環境

今回、試してみた環境は下記のとおりです。

項目 詳細
Ubuntu 16.04 Win10ProのWSLで、Ubuntu16.04をインストールしました
Excel 2016 ネットワークエンジニアの大好きな定番ソフト
Python 3.6.3 プログラミング言語と実行環境
Anaconda 5.0.1 Pythonのオールインワンパッケージ
pandas 0.20.3 Pythonライブラリ。CSV操作用
Netmiko 2.0.0 Pythonライブラリ。ルータへTelnet/SSH
TextFSM 0.3.2 Pythonライブラリ。コマンドの実行結果を解析し値を抽出
NTC-Templates TextFSMの抽出テンプレート集
Cisco VIRL 1.3.296 ルータのシミュレータ

Pythonのインストールは、オールインワンパッケージのAnacondaを利用すると便利なので、お勧めします。

Netmikoの準備

もし、古いバージョンのNetmiko がインストールされている場合、下記のようにv2.0.0にアップグレードしてください。

$ pip install netmiko==2.0.0

現在のバージョンは下記のコマンドで確認できます。 Version: 2.0.0以上であればOKです。

$ pip show netmiko
Name: netmiko
Version: 2.0.0
Summary: Multi-vendor library to simplify Paramiko SSH connections to network devices
Home-page: https://github.com/ktbyers/netmiko
Author: Kirk Byers
Author-email: ktbyers@twb-tech.com
License: MIT
Location: /home/kooshin/anaconda3/lib/python3.6/site-packages
Requires: textfsm, paramiko, pyserial, scp, pyyaml

NTC-Templatesの準備

TextFSM用のテンプレートNTC-Templatesを事前にホームディレクトリに保存します。 下記のようにGitでクローンするか、ZIPファイルをダウンロードして、ホームディレクトリに展開してください。

$ cd ~
$ git clone https://github.com/networktocode/ntc-templates.git

コード:get_routes.py

シスコルータにSSHでログインして、show ip routeコマンドの実行結果をExcel/CSVに保存するコードは下記のとおりです。

Netmiko に、TextFSM+NTC-Templatesが組み込まれたことで、コードでは、ほとんど意識せずに利用することができます。

import netmiko
import pandas as pd

# シスコルータにログイン
params = {
        'device_type':  'cisco_ios',
        'ip':           '172.16.1.99',
        'username':     'cisco',
        'password':     'cisco',
        'secret':       'cisco',
        }
conn = netmiko.ConnectHandler(**params)
conn.enable()

# show ip routeの実行結果をTextFSMで抽出する
routes = conn.send_command('show ip route', use_textfsm=True)

# CSVで保存
df = pd.DataFrame(routes)
df.to_csv('routes.csv', index=False)

動作結果

下記の通り、コマンドを実行すると、 ルータにSSHでログインし、 show ip routeコマンドを実行し、 実行結果をCSVで保存します。

$ python get_routes.py

show ip routeコマンドの実行結果

csr1000v-1#show ip route
Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      10.0.0.0/8 is variably subnetted, 12 subnets, 2 masks
O        10.0.0.4/30 [110/2] via 10.0.0.34, 1w1d, GigabitEthernet3
O        10.0.0.8/30 [110/3] via 10.0.0.34, 1w1d, GigabitEthernet3
O        10.0.0.12/30 [110/2] via 10.0.0.34, 1w4d, GigabitEthernet3
O        10.0.0.16/30 [110/3] via 10.0.0.34, 1w1d, GigabitEthernet3
O        10.0.0.20/30 [110/43] via 10.0.0.34, 1w1d, GigabitEthernet3
O        10.0.0.24/30 [110/4] via 10.0.0.42, 1w4d, GigabitEthernet2
                      [110/4] via 10.0.0.34, 1w4d, GigabitEthernet3
O        10.0.0.28/30 [110/3] via 10.0.0.42, 1w4d, GigabitEthernet2
                      [110/3] via 10.0.0.34, 1w4d, GigabitEthernet3
C        10.0.0.32/30 is directly connected, GigabitEthernet3
L        10.0.0.33/32 is directly connected, GigabitEthernet3
O        10.0.0.36/30 [110/2] via 10.0.0.42, 1w4d, GigabitEthernet2
C        10.0.0.40/30 is directly connected, GigabitEthernet2
L        10.0.0.41/32 is directly connected, GigabitEthernet2
      192.168.0.0/32 is subnetted, 8 subnets
O        192.168.0.1 [110/4] via 10.0.0.34, 1w1d, GigabitEthernet3
O        192.168.0.2 [110/2] via 10.0.0.34, 1w4d, GigabitEthernet3
O        192.168.0.3 [110/3] via 10.0.0.34, 1w1d, GigabitEthernet3
O        192.168.0.4 [110/5] via 10.0.0.42, 1w4d, GigabitEthernet2
                     [110/5] via 10.0.0.34, 1w4d, GigabitEthernet3
O        192.168.0.5 [110/4] via 10.0.0.42, 1w4d, GigabitEthernet2
                     [110/4] via 10.0.0.34, 1w4d, GigabitEthernet3
O        192.168.0.6 [110/3] via 10.0.0.42, 1w4d, GigabitEthernet2
                     [110/3] via 10.0.0.34, 1w4d, GigabitEthernet3
C        192.168.0.7 is directly connected, Loopback0
O        192.168.0.8 [110/2] via 10.0.0.42, 1w4d, GigabitEthernet2

CSV出力結果:routes.csv

下記の通り、Excel/CSVに変換できました。 なお、show ip routeの結果に含まれないフィールドは表示されません。 プロトコルがLocal/Connectedの場合、ADやネクストホップのIPアドレスは表示されません。

f:id:KOOSHIN:20171216232111p:plain
Excel/CSV出力結果

protocol,type,network,mask,distance,metric,nexthop_ip,nexthop_if,uptime
O,,10.0.0.4,30,110,2,10.0.0.34,GigabitEthernet3,1w1d
O,,10.0.0.8,30,110,3,10.0.0.34,GigabitEthernet3,1w1d
O,,10.0.0.12,30,110,2,10.0.0.34,GigabitEthernet3,1w4d
O,,10.0.0.16,30,110,3,10.0.0.34,GigabitEthernet3,1w1d
O,,10.0.0.20,30,110,43,10.0.0.34,GigabitEthernet3,1w1d
O,,10.0.0.24,30,110,4,10.0.0.42,GigabitEthernet2,1w4d
O,,10.0.0.24,30,110,4,10.0.0.34,GigabitEthernet3,1w4d
O,,10.0.0.28,30,110,3,10.0.0.42,GigabitEthernet2,1w4d
O,,10.0.0.28,30,110,3,10.0.0.34,GigabitEthernet3,1w4d
C,,10.0.0.32,30,,,,GigabitEthernet3,
L,,10.0.0.33,32,,,,GigabitEthernet3,
O,,10.0.0.36,30,110,2,10.0.0.42,GigabitEthernet2,1w4d
C,,10.0.0.40,30,,,,GigabitEthernet2,
L,,10.0.0.41,32,,,,GigabitEthernet2,
O,,192.168.0.1,32,110,4,10.0.0.34,GigabitEthernet3,1w1d
O,,192.168.0.2,32,110,2,10.0.0.34,GigabitEthernet3,1w4d
O,,192.168.0.3,32,110,3,10.0.0.34,GigabitEthernet3,1w1d
O,,192.168.0.4,32,110,5,10.0.0.42,GigabitEthernet2,1w4d
O,,192.168.0.4,32,110,5,10.0.0.34,GigabitEthernet3,1w4d
O,,192.168.0.5,32,110,4,10.0.0.42,GigabitEthernet2,1w4d
O,,192.168.0.5,32,110,4,10.0.0.34,GigabitEthernet3,1w4d
O,,192.168.0.6,32,110,3,10.0.0.42,GigabitEthernet2,1w4d
O,,192.168.0.6,32,110,3,10.0.0.34,GigabitEthernet3,1w4d
C,,192.168.0.7,32,,,,Loopback0,
O,,192.168.0.8,32,110,2,10.0.0.42,GigabitEthernet2,1w4d

おわりに

簡単なコードを書くことで、面倒な作業から解放されます。 ぜひ、ネットワークエンジニアの皆様はコードを書いて、作業の自動化、省力化しましょう!

Pythonでシスコルータの型名とシリアル番号の収集を自動化してみた

ネットワークの自動化ネタのとして、シスコルータにログインして、型名とシリアル番号を自動的に収集してCSV形式でファイル保存するPythonスクリプトを試作してみました。 今回はPythonライブラリのnetmikotextfsm+ntc-templatesを利用しました。

f:id:KOOSHIN:20171206223220p:plain
シスコルータの型名とシリアル番号の一覧(CSV形式)

続きを読む