Cisco IOSのshowコマンドをk-meansでクラスタリング

# はじめに
Open Source の機械学習ライブラリも充実して、割と簡単にクラスタリングなどを試すことができます。
そこで、Cisco IOS の show interfaces からいくつかのフィールドを取り出して、k-means のクラスタリングでグループ分けをするプログラムを書いてみました。
同じ interface で時間をおいて取得した show interfaces や、複数機器から取得した show interfaces の結果などをクラスタリングできます。
# 使用した言語とライブラリ
python 2.7.5を使用しています。

主に使用したライブラリは以下になります。
textfsm==0.3.2
numpy==1.13.3
scikit-learn==0.19.1

# コード解説
### showコマンドから値の取得
textfsm を使うと、text で出力された show コマンドの結果から必要なフィールドの値を取り出すのが便利ときいたので、使ってみました。
以下に、Cisco IOS の show interfaces 用のテンプレートがあったので、それをベースにしました。足らないものがあったので、自分で適当に足しています。
https://github.com/networktocode/ntc-templates

```
Value Required INTERFACE (\S+)
Value LINK_STATUS (\w+)
Value PROTOCOL_STATUS (.*)
Value HARDWARE_TYPE ([\w ]+)
Value ADDRESS ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+)
Value BIA ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+)
Value DESCRIPTION (.*)
Value IP_ADDRESS (\d+\.\d+\.\d+\.\d+\/\d+)
Value MTU (\d+)
Value DUPLEX (.+?)
Value SPEED (.+?)
Value BANDWIDTH (\d+\s+\w+)
Value DELAY (\d+\s+\w+)
Value RELIABILITY (\d+)
Value TXLOAD (\d+)
Value RXLOAD (\d+)
Value ENCAPSULATION (\w+)
Value INPUT_DROPS (\d+)
Value OUTPUT_DROPS (\d+)
Value QUEUE_STRATEGY (.*)
Value INPUT_RATE (\d+)
Value OUTPUT_RATE (\d+)
Value INPUT_PACKETS (\d+)
Value INPUT_BRAODCAST (\d+)
Value INPUT_ERRORS (\d+)
Value OUTPUT_PACKETS (\d+)
Value OUTPUT_ERRORS (\d+)
Value INTERFACE_RESETS (\d+)


Start
  ^${INTERFACE} is ${LINK_STATUS}.*protocol is ${PROTOCOL_STATUS}
  ^\s+Hardware is ${HARDWARE_TYPE} -> Continue
  ^.*address is ${ADDRESS}.*bia ${BIA}
  ^\s+Description: ${DESCRIPTION}
  ^\s+Internet address is ${IP_ADDRESS}
  ^\s+MTU ${MTU}.*BW ${BANDWIDTH}.*DLY ${DELAY}
  ^\s+reliability ${RELIABILITY}.+txload ${TXLOAD}.+rxload ${RXLOAD}
  ^\s+Encapsulation ${ENCAPSULATION}
  ^\s+Input queue: \d+/\d+/${INPUT_DROPS}/\d+ -> Continue
  ^.*Total output drops: ${OUTPUT_DROPS}
  ^\s+Queueing strategy: ${QUEUE_STRATEGY}
  ^\s+${DUPLEX}, ${SPEED}, media type
  ^.*input rate ${INPUT_RATE}
  ^.*output rate ${OUTPUT_RATE}
  ^\s+${INPUT_PACKETS} packets input
  ^\s+Received ${INPUT_BRAODCAST} broadcasts
  ^\s+${INPUT_ERRORS} input errors
  ^\s+${OUTPUT_PACKETS} packets output
  ^\s+${OUTPUT_ERRORS} output errors -> Continue
  ^.* ${INTERFACE_RESETS} interface resets -> Record
```
取り出しているのは、ドロップやエラーなどの統計情報です。統計情報ならSNMPの方が実用的かもしれません。また、最近のIOSであれば、Netconf でこの辺りの情報を取得することも可能です。
https://github.com/YangModels/yang/tree/master/vendor/cisco
ただ、textfsm なら、がんばって正規表現を書けば、様々なshow コマンドに対応できるのが利点です。

###ベクトル化
show interfaces から取り出した値を並べてベクトルを作成します。ドロップやエラーのカウントはトータルのカウントで割って、比率になおします。今回のベクトルは9個の要素からなるリストです。
それを vector_list というリストに追加します。リストのリストのなので、2次元配列になります。N x 9 になります。

これを Numpy のarray 関数を使って、N x 9 の Numpy の行列にします。Numpy は pythonで行列演算などをサポートするためのライブラリです。今回は、scikit-learn が Numpy の行列を想定しているようなので、使っています。また、行列の各要素を列ごとの min max normalization で、0から1の値に正規化するためにも使用しています。

### k-means によるクラスタリング
データをグループ分けするすることをクラスタリングとかクラスタ分析とかいうようですが、k-means はその手法の一つです。以下に解説があります。
https://qiita.com/kurehajime/items/774883eb6c967ff073bb

今回は、scikit-learn という機械学習用の python package に含まれる cluster.KMeans クラスを使いました。行列さえ用意すれば、数行のコードを書くだけでクラスタリングしてくれます。詳しい使い方は、以下を見てください。
http://pythondatascience.plavox.info/scikit-learn/%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E5%88%86%E6%9E%90-k-means

cluster.KMeans を、ほぼデフォルトのままで使っています。いくつのグループに分けるかというクラスタ数 n_clusters を指定する必要があります。以下にある、エルボー法を使って、今回のサンプルデータでは、n_clusters=10にしました。
https://qiita.com/deaikei/items/11a10fde5bb47a2cf2c2

### サンプルコード
cat *.log | ./show_int_cluster.py　のように標準入力からshow interfacesの結果を含むテキストファイルを入力することを想定しています。
textfsm用のテンプレート cisco_ios_show_interfaces.template も同じディレクトリにあると想定してます。

```python:show_int_cluster.py
import textfsm
import re
import sys
import numpy as np
from sklearn import cluster
# translate a list into a dictionary with show interfaces fields
def row2interface(header, row):
    interface = {}
    idx = 0
    for column in header:
        interface[column] = row[idx]
        idx = idx + 1
    return interface

# validate if this is a working ethernet type main interface
def valid_interface(interface):
    name = interface['INTERFACE']
    if re.search('thernet\s*[/\d]+$', name) and \
        (interface['PROTOCOL_STATUS'] == 'up' or  \
        interface['PROTOCOL_STATUS'] == 'up (connected)') and \
        float(interface['INPUT_PACKETS']) > 0 and \
        float(interface['OUTPUT_PACKETS']) > 0 :
        return True
    else:
        return False

# put fields for clustering into a vector
def create_vector(interface):
    vector = []
    vector.append(float(interface['TXLOAD']))
    vector.append(float(interface['RXLOAD']))
    vector.append(float(interface['RXLOAD']))
    vector.append(float(interface['INPUT_DROPS'])/ \
                  float(interface['INPUT_PACKETS']))
    vector.append(float(interface['INPUT_BRAODCAST'])/ \
                  float(interface['INPUT_PACKETS']))
    vector.append(float(interface['INPUT_ERRORS'])/ \
                  float(interface['INPUT_PACKETS']))
    vector.append(float(interface['OUTPUT_DROPS'])/ \
                  float(interface['OUTPUT_PACKETS']))
    vector.append(float(interface['OUTPUT_ERRORS'])/ \
                  float(interface['OUTPUT_PACKETS']))
    vector.append(float(interface['INTERFACE_RESETS']))
    return vector

# normalize a matrix based on min-max values
def min_max(matrix):
    min = matrix.min(axis=0)
    max = matrix.max(axis=0)
    result = (matrix - min)/(max - min)
    return result

# main function
# parse show interfaces
with open('cisco_ios_show_interfaces.template', 'r') as f:
    fsm = textfsm.TextFSM(f)
cli = sys.stdin.read()
table = fsm.ParseText(cli)

# create and normalize the data
vector_list = []
for row in table:
    interface = row2interface(fsm.header, row)
    if valid_interface(interface):
        vector = create_vector(interface)
        vector_list.append(vector)
matrix = np.array(vector_list)
matrix = min_max(matrix)

# cluster the data by k-means method
n_clusters = 10
km = cluster.KMeans(n_clusters=n_clusters)
km_pd = km.fit_predict(matrix)

k_map = np.zeros((n_clusters))
for cluster in km_pd:
    k_map[cluster] += 1

for cluster, samples in enumerate(k_map):
    print cluster, samples
```

# まとめ

約80のinterfaceに関するデータを読み込ませた結果、以下のように分布しました。

|グループ|interfaceの数|
|--:|--:|
|0|		47|
|1|		 6|
|2|		12|
|3|		 2|
|4|		 4|
|5|		 7|
|6|		 4|
|7|		 5|
|8|		 1|
|9| 		 1|

今回は、ラボから適当に集めたデータですが、それなりにグループ分けされたようです。
実際には、同一の役割のルータ同士で、時系列でデータを集めて分析することができれば、正常状態と異常状態や、通常負荷と高負荷など、複数の観点からのグループ分けが自動的にできるかもしれません。また、show interfaces 以外にも CPU やメモリ使用率などもベクトルに加えるのもおもしろいかもしれません。

また、routing table には、k-means は適さないかもしれませんが、prefix 間の近似度や next hop の方向に関する近似度などをモデル化して、doc2vector のように、table のサイズに無関係にベクトルできれば、巨大な routing table の比較や変化の度合いの計測などにも機械学習の手法は使用できるかもしれません。

# SOM
追加で、SOM (Self Organization Map) も試してみました。
以下の記事にある、sompy 0.1.1を使っています。
http://www.iandprogram.net/entry/2016/09/20/175441

sompyは、Mapの初期値に正規分布を使っています。一方、入力データは0-1で正規化しているので、Mapの一部しか使わない可能性があります。そのため、Mapは20x20と入力データに比較して大きめにしました。
show_int_cluster.py に以下のコードを追加しました。

```python
# cluster the data by SOM
from sompy import SOM
out_shape = (20, 20)
som = SOM(out_shape, matrix)
som.set_parameter(neighbor=0.25, learning_rate=0.10, input_length_ratio=0.25)
som.train(1000)

out_map = np.zeros(out_shape)
for row in matrix:
    idx = som._get_winner_node(row)
    out_map[idx[0], idx[1]] += 1

for row in out_map:
    print row
```

以下は、実行結果で、20x20のMapの中で、各ノードごとに、何個の入力データに関して、勝利ノード（最も入力のベクトルに近いベクトルを持つノード）になったかを出力しています。実行するたびに、勝利ノードの分布が若干変化しますが、大体8～11個ぐらいのグループに分かれました。

```
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[  0.   0.  11.  55.   0.   1.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
[ 0.  0.  0.  9.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  3.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  2.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
```