【net-snmp】snmpwalk使ってトラフィック量をサクッと集計

(2022.04.20追記)
%のエスケープについて一部追記。
cronにawkコマンドを直に実行する場合はエスケープ必要ですが、シェルスクリプトで実行する場合は%のエスケープは不要です。

---

# はじめに

コロナウイルスの影響でテレワーク化が進みましたが、
それによりいつものネットワークトラフィックが変化したかと思います。

現にこちらの現場でもそのような声が挙がりました。
現運用では疎通レベルの監視はしていても、トラフィック遷移までは見てませんでした。

**それならばCactiなどのトラフィック可視化ツールを入れればいい！！**
・・・というわけにもいかず、それを打ち砕く要件が追加されました。

そうです、**「サクッと集計したい」** のです、一時的なものなので。

今後も同じような要望もらいそうな気がするので備忘録がてら残します。

# どんな人向け？

- 特定機器のトラフィック量をサクッと取得、集計したい
- SNMP MIBの概要はわかるけど実際に活用したことない！
- 普段NW機器にSNMP設定してるけどどうやって取得しているのか気になる

# やりたいこと

## 要件

頂いた要望は以下のとおりです(個人的に追加したものもあったり)。
サクッとやりたいのに要件はサクッとしていないことはよくあることですね。

- 既存のサーバに送受信トラフィック量を定期的に取得する仕組みを作って欲しい
- ミドルウェアが必要なツールの導入はしたくない
    - 上記二つの要件により、net-snmp-utilsにあるsnmpwalkコマンドで定期的に情報取得する
- Excelでデータ加工したいので取得したデータは生データが良い
    - ~~素晴らしい要件！！！ Excelは神~~
- ログは圧縮してほしい、勝手に消えるの嫌なのでローテートしなくていい

## 構成

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392066/1f0e3bf1-9f4d-3012-98a8-2656bc289daf.png)



おうちにあるNETGEARスイッチのルータ向けポート(Po1)の上り下りを集計してみたいと思います。
バッファローじゃなくてお古でもいいから遊べるルータが欲しいですね・・・。

ちなみに取得対象のNETGEARスイッチは[GS108T-200JPS](https://www.jp.netgear.com/business/products/switches/smart/GS108T.aspx)になります。

サーバはVagrantとVirtual Box使いますが、構築出来たらなんでもOKです。
また、CentOSを使ってますが、例えばWindowsでもnet-snmpはありますのでsnmp部分は参考にできるかと思います。

トラフィック集計の大雑把な流れは以下の通りです(上のオレンジ部分)。

- サーバにて、定期的にsnmpwalkでsnmp情報取得し、ログに出力
- 出力したログを圧縮
- 圧縮したログをPCにダウンロード(ここではTeratermでSSH SCP利用してます)
- Excelとかでデータ加工

注意する点は、**取得する情報はデータ転送レートではなく、合計トラフィック量**となります。
そのため、取得したログから計算する必要があります。
そこら辺についてはsnmpwalkコマンドのところやExcel加工で補足します。

# 手順

## 0.ざっくりとした作業の流れ

1. 既存サーバの準備(VagrantでCentOS/7構築)
2. トラフィック取得するNW機器準備(SNMPコミュニティ設定)
3. ``net-snmp-utils``のインストール
4. SNMP取得確認
5. 出力ログの準備
6. cron設定(SNMP取得＆ログ出力、ログ圧縮)
7. (おまけ)Excelでデータ加工

## 1.既存サーバの準備

適当なサーバを用意してください。
ここではVagrant(ver.2.2.6)でサクッと構築しますが、適宜自分のやりやすいように
サーバを準備してもらえたらと思います。

以下に自分が使ったvagrantfileを折りたたんでますので、コピペして使ってもらっても平気です。
(※)どのインターフェイス使ってブリッジ接続するのか聞いてくるので良い感じに返してあげてください。


<details>
<summary>CentOS/7用vagrantfile</summary><div>

```ruby:vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

# コンフィグ関数
def ConfigCentos7Easy(config , hostname , ip)
  config.vm.define hostname do |node|
    # ホスト名、IPアドレス(ブリッジ接続)
    node.vm.hostname = hostname
    node.vm.network "public_network", ip: ip
    # 起動時実行コマンド
      # - タイムゾーンを日本に設定
      # - パスワード認証有効化(鍵不要)
      # - sshd_config変更に伴うsshd再起動
    node.vm.provision "shell", inline: <<-SHELL
      timedatectl set-timezone Asia/Tokyo
      sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config
      systemctl restart sshd
      SHELL
  end

  config.vm.provider "virtualbox" do |vb|
    # メモリ512MB
    vb.memory = "512"
    # CPU数1台
    vb.cpus = "1"
  end

  # タイムアウト値の設定
  config.vm.boot_timeout = 600
  
end

# 本処理
Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  # server01
  ConfigCentos7Easy(config, "server01" , "192.168.11.71")
end
```

</div></details>


以降は以下のCentOSで構築していきます。

```
[root@server01 vagrant]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
```

## 2.トラフィック取得するNW機器準備

取得したい機器のsnmpコミュニティ設定をします。
ここでは読み取り専用で「hakoniwa」で設定しました。

![snmpの設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392066/b9865837-9f77-7fbb-3129-4c164ef29e0f.png)

なお、書き込み専用にするとsnmpsetなどのツールによりパラメータ変更出来たりしますがここでは割愛。

## 3.net-snmp-utilsのインストール

今回取得するコマンド``snmpwalk``が入っている``net-snmp-utils``をインストールします。
``net-snmp-libs``はデフォルトでだいたい入ってます。

```
# yum install net-snmp-utils

# yum list installed | grep net-snmp-utils
net-snmp-libs.x86_64                1:5.7.2-43.el7_7.3          @updates
net-snmp-utils.x86_64               1:5.7.2-43.el7_7.3          @updates
```

ちなみに、``net-snmp``はSNMPエージェントです。
今回は既存サーバ自体のSNMP情報取得するわけではないので、インストール不要です。

## 4.SNMP取得確認

これでサーバからNW機器にSNMP取得できるようになったので、早速``snmpwalk``を使ってみます。
MIBとかOIDさっぱりな人は、補足を後述してますので、参照してみてください。


``snmpwalk``のコマンド体系は以下の通り。

```
# snmpwalk -v バージョン -c コミュニティ名 IPアドレス OID(.1.3.6...または値の名称でも可)
```

バージョンはNW機器側に合わせてください、とりあえず2cだったら問題ないかと思います。
※1は脆弱すぎるので論外、3の場合は認証が必要な場合があるので適宜設定してください

今回の場合取得したい情報はトラフィック送受信量なので、
トラフィック送信量：``IF-MIB::ifHCOutOctets``
トラフィック受信量：``IF-MIB::ifHCInOctets``

それぞれ指定して実行すると、全インターフェイスのトラフィック送受信量が出力します。
今回は機器のIPアドレスが192.168.11.15、コミュニティ名がhakoniwaなので以下のコマンドとなります。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCInOctets
IF-MIB::ifHCInOctets.1 = Counter64: 7080332
IF-MIB::ifHCInOctets.2 = Counter64: 4731100
IF-MIB::ifHCInOctets.3 = Counter64: 444476
IF-MIB::ifHCInOctets.4 = Counter64: 0
IF-MIB::ifHCInOctets.5 = Counter64: 23000
IF-MIB::ifHCInOctets.6 = Counter64: 0
IF-MIB::ifHCInOctets.7 = Counter64: 0
IF-MIB::ifHCInOctets.8 = Counter64: 0
IF-MIB::ifHCInOctets.13 = Counter64: 1111177
IF-MIB::ifHCInOctets.14 = Counter64: 0
IF-MIB::ifHCInOctets.15 = Counter64: 0
IF-MIB::ifHCInOctets.16 = Counter64: 0
IF-MIB::ifHCInOctets.17 = Counter64: 0
```

上のように指定すると全ポート出力するので、対象ポートのみに絞ります。
どの番号が対象ポートか判断する必要があるので、``IF-MIB::ifDescr``を指定してポート名称を確認します。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifDescr
IF-MIB::ifDescr.1 = STRING: Slot: 0 Port: 1 Gigabit - Level
IF-MIB::ifDescr.2 = STRING: Slot: 0 Port: 2 Gigabit - Level
IF-MIB::ifDescr.3 = STRING: Slot: 0 Port: 3 Gigabit - Level
IF-MIB::ifDescr.4 = STRING: Slot: 0 Port: 4 Gigabit - Level
IF-MIB::ifDescr.5 = STRING: Slot: 0 Port: 5 Gigabit - Level
IF-MIB::ifDescr.6 = STRING: Slot: 0 Port: 6 Gigabit - Level
IF-MIB::ifDescr.7 = STRING: Slot: 0 Port: 7 Gigabit - Level
IF-MIB::ifDescr.8 = STRING: Slot: 0 Port: 8 Gigabit - Level
IF-MIB::ifDescr.13 = STRING:  CPU Interface for Slot: 5 Port: 1
IF-MIB::ifDescr.14 = STRING:  Link Aggregate
IF-MIB::ifDescr.15 = STRING:  Link Aggregate
IF-MIB::ifDescr.16 = STRING:  Link Aggregate
IF-MIB::ifDescr.17 = STRING:  Link Aggregate
```

このようにPort-channelや、他の機器だとVLAN interfaceも機器によっては取得できます。
ただ、どの番号がわからない・・・というときに``IF-MIB::ifDescr``を指定するとわかりやすくなります。

Po1が``ifDescr.1``だったので、``IF-MIB::ifHCInOctets.1``と指定するとPo1のみ表示されます。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCInOctets.1
IF-MIB::ifHCInOctets.1 = Counter64: 7269120
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCOutOctets.1
IF-MIB::ifHCOutOctets.1 = Counter64: 5190521
```

### 補足.SNMPに関する補足

**「急にわけのわからん文字出てきた！　数字の羅列とかじゃないの！？」**とか
**「転送レート直接取得できないの！？」**な人向けの補足説明です。

理解している人は読み飛ばしてください。

#### 補足1.SNMPのMIBやOIDがよくわからない

とっつきにくいSNMPのOIDについての補足です、知ってる人は読み飛ばしてOKです。

SNMPを使って機器の様々な情報を取得する際、機器の中にあるSNMP MIBというデータベースを参照しに行きます。
そしてこのデータベースはツリー状になっており、その枝ごとに番号と項目が紐付けられております。

例えば、ホスト名を取得したい場合google先生に聞くと「.1.3.6.1.2.1.1.5.0」と出てくるのでそれをOIDに入れて実行すると・・・

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 .1.3.6.1.2.1.1.5.0
SNMPv2-MIB::sysName.0 = STRING: netgear_sw
```

となります、基本どの機器でも同じです。
というのも参照しているMIBが**標準MIB**であり、一般的によく用いられる情報(※)がここに入ってます。
※ホスト名などの情報やCPU使用率、インターフェイスの各種統計情報など

逆に、機器独自の情報などは**拡張MIB**で取得できますが、今回は割愛します。

OIDは数字の羅列ですが、出力結果の先頭にある文字``SNMPv2-MIB::sysName.0``をOIDに
指定しても同じ出力結果となります。
なので、項目名がわかっていればそれを指定して取得することも出来ます。
(数字の羅列より圧倒的に覚えやすい・・・項目名も結局すぐ忘れそうだけど)

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 SNMPv2-MIB::sysName.0
SNMPv2-MIB::sysName.0 = STRING: netgear_sw
```

ちなみに、OIDはツリー状で枝分かれしているので、例えば
``.1.3.6.1.2.1.1.5.0``の末尾``.5.0``を除いた``.1.3.6.1.2.1.1``で実行すると

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 .1.3.6.1.2.1.1
SNMPv2-MIB::sysDescr.0 = STRING: GS108Tv2
SNMPv2-MIB::sysObjectID.0 = OID: SNMPv2-SMI::enterprises.4526.100.4.18
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (2710163400) 313 days, 16:13:54.00
SNMPv2-MIB::sysContact.0 = STRING:
SNMPv2-MIB::sysName.0 = STRING: netgear_sw
SNMPv2-MIB::sysLocation.0 = STRING:
...
```

以下のように、システムに関する情報一覧が出力しました。
これは、``system``で取得した結果と同じになります。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 SNMPv2-MIB::system
SNMPv2-MIB::sysDescr.0 = STRING: GS108Tv2
SNMPv2-MIB::sysObjectID.0 = OID: SNMPv2-SMI::enterprises.4526.100.4.18
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (2710172700) 313 days, 16:15:27.00
SNMPv2-MIB::sysContact.0 = STRING:
SNMPv2-MIB::sysName.0 = STRING: netgear_sw
SNMPv2-MIB::sysLocation.0 = STRING:
...
```

というわけで、OIDに``.``を指定して実行すると、ツリーの頂点以下全部になるため、全ての情報が出力することになります。
そのまま出力すると情報量が多すぎるので、``wc``で行数を出力したりすると良いかもしれません。
※ちなみに自身の環境で実行したらタイムアウトしました・・・それだけ取得できる量多いってことですね。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 .
```

#### 補足2.転送レートは計算しないといけない

結論からいうと、転送レートは直接出すことはできません**(もしもあったら是非教えてください！ 拡張MIB以外で！！)**
SNMPの取得項目に``ifSpeed``というそれっぽいものがありますが、
これは有効な帯域速度なので100MbpsとかGbpsしか出てきません。
そのため、データ転送レートを取得する場合は、以下項目を取得します。

- ifHCInOctets　・・・　受信した総バイト数
- ifHCOutOctets　・・・　送信した総バイト数

**それぞれの取得値の差分÷取得間隔時間＝転送レート**の式で計算が可能です。
そして、これは粒度が細かい方がより詳細に出ますが、どの程度にするかは環境に応じて設定してください。
とりあえず``cron``で簡単に設定できる1分間隔でも十分な精度かと思います。

また、``ifSpeed``の帯域速度を用いればトラフィック使用率も計算可能です。

まとめると、転送レートの計算式は以下の通りです。

Δ：前回取得結果との差分
Δtime：取得間隔(秒)

- データ転送レート(Mbps)
    - 受信：ΔifHCInOctets(byte)*8/1024/1024/Δtime
    - 送信：ΔifHCOutHCOctets(byte)*8/1024/1024/Δtime
- トラフィック使用率(%)
    - 受信：ΔifHCInOctets(byte)*8/Δtime/ifSpeed
    - 送信：ΔifHCOutOctets(byte)*8/Δtime/ifSpeed
    - 合計：(受信+送信)/2

## 5.出力ログの準備

### 5.1.出力ディレクトリの作成

``snmpwalk``を使って取得した値をログに出力させる必要があるので、出力ディレクトリを作成します。
ここでは``/var/log``配下に``snmp_log``ディレクトリを作成します。

```
# mkdir /var/log/snmp_log
```

## 6.cron設定(SNMP取得＆ログ出力、ログ圧縮)

### 6.1.SNMP取得コマンドの精査

SNMP取得したログをそのまま書き込ませると、項目と値のみしか書き込まれません。
このままではその取得した結果がどのタイミングで取得されたかわからないので、
``awk``コマンドを用いて、先頭に日時を挿入しましょう。

なお、cronで実行するには%を\でエスケープする必要があるので注意。
※シェルスクリプトにsnmpwalkコマンドを入れる場合は%のエスケープ不要

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCInOctets.1 | awk '{print strftime("\%Y/\%m/\%d \%H:\%M:\%S"), $0}'
awk: cmd. line:1: warning: escape sequence `\%' treated as plain `%'
2020/04/03 22:23:12 IF-MIB::ifHCInOctets.1 = Counter64: 241011307670
```

ログファイルは1日ごとで分けたいので、出力先ファイルは``date``コマンドで日付を取得します。
``date``コマンドのオプションでフォーマット指定しますが、
そこで使われる%もやっぱりエスケープする必要があるのでお忘れなく。

```
# snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCInOctets.1 | awk '{print strftime("\%Y/\%m/\%d \%H:\%M:\%S"), $0}' >> /var/log/snmp_log/snmp_in_`date +\%Y\%m\%d`
awk: cmd. line:1: warning: escape sequence `\%' treated as plain `%'
# cat /var/log/snmp_log/snmp_in_20200403
2020/04/03 22:24:03 IF-MIB::ifHCInOctets.1 = Counter64: 241011322173
```

### 6.2.ログ圧縮コマンドの精査

気休め程度ですが、ログ圧縮用コマンドも用意します。
毎週月曜日朝の時点で、先週分のログ(月～日)が圧縮できたらいいので、
「毎週月曜1時に、「*.gz」以外且つ更新日時が1時間より前のファイルをすべてgzipで圧縮する」設定を行います。

gzipで圧縮しますが、対象は``find``コマンドで取得します。

※以下は23:00頃に実施した結果です。dateコマンド打ち忘れた・・・。

```
# ll
total 12
-rw-r--r--. 1 root root 37 Mar 26 21:00 snmp_in_20200326.gz
-rw-r--r--. 1 root root 37 Mar 27 21:00 snmp_in_20200327.gz
-rw-r--r--. 1 root root  0 Mar 28 21:00 snmp_in_20200328
-rw-r--r--. 1 root root  0 Mar 29 21:00 snmp_in_20200329
-rw-r--r--. 1 root root  0 Mar 30 21:00 snmp_in_20200330
-rw-r--r--. 1 root root  0 Mar 31 21:00 snmp_in_20200331
-rw-r--r--. 1 root root  0 Apr  1 21:00 snmp_in_20200401
-rw-r--r--. 1 root root  0 Apr  2 21:00 snmp_in_20200402
-rw-r--r--. 1 root root 69 Apr  3 22:24 snmp_in_20200403
-rw-r--r--. 1 root root  0 Apr  3 21:00 snmp_in_20200403_
# find /var/log/snmp_log/ -type f -mmin +60 ! -name "*.gz" |  xargs gzip
# ll
total 40
-rw-r--r--. 1 root root 37 Mar 26 21:00 snmp_in_20200326.gz
-rw-r--r--. 1 root root 37 Mar 27 21:00 snmp_in_20200327.gz
-rw-r--r--. 1 root root 37 Mar 28 21:00 snmp_in_20200328.gz
-rw-r--r--. 1 root root 37 Mar 29 21:00 snmp_in_20200329.gz
-rw-r--r--. 1 root root 37 Mar 30 21:00 snmp_in_20200330.gz
-rw-r--r--. 1 root root 37 Mar 31 21:00 snmp_in_20200331.gz
-rw-r--r--. 1 root root 37 Apr  1 21:00 snmp_in_20200401.gz
-rw-r--r--. 1 root root 37 Apr  2 21:00 snmp_in_20200402.gz
-rw-r--r--. 1 root root 69 Apr  3 22:24 snmp_in_20200403
-rw-r--r--. 1 root root 38 Apr  3 21:00 snmp_in_20200403_.gz
```

### 6.3.cron設定

今までのコマンドの精査結果を踏まえ、以下のように設定します。
設定したらプロセス再起動も忘れずに。

```
# cat /etc/crontab
～省略～
# 受信用
  *  *  *  *  * root snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCInOctets.1 | awk '{print strftime("\%Y/\%m/\%d \%H:\%M:\%S"), $0}' >> /var/log/snmp_log/snmp_in_`date +\%Y\%m\%d`
# 送信用
  *  *  *  *  * root snmpwalk -v 2c -c hakoniwa 192.168.11.15 IF-MIB::ifHCOutOctets.1 | awk '{print strftime("\%Y/\%m/\%d \%H:\%M:\%S"), $0}' >> /var/log/snmp_log/snmp_in_`date +\%Y\%m\%d`
# ログ圧縮(毎週月曜1時に昨日以前全て圧縮)
  0  1  *  *  1 root find /var/log/snmp_log/ -type f -mmin +60 ! -name "*.gz" |  xargs gzip

# systemctl restart crond

# systemctl status crond
● crond.service - Command Scheduler
   Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2020-04-03 22:31:41 JST; 20s ago
...
```

しばらくそのままにしてログが出力していたら問題なしです。
あとは月曜朝にログインして先週以前のログがすべてgzipで圧縮されていることが確認できたらOKです。

## 7.(おまけ)Excelでデータ加工

ここからは都度やりやすい方法で加工してください。
以降はExcelで良い感じにグラフを出す例です。

![excel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392066/e52cae8b-d84f-c86a-3e1e-ac1469fd3a2e.png)

基本はログをそのまま貼り付けたら、``RIGHT``関数や``FIND``関数で加工して
転送レートを出すようにするといい感じです。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392066/c2837456-5e48-274b-4501-8f72d8dea55a.png)

今日の午前中にトラフィック起こせばいい感じに出るかな・・・と思ったらそこしか出ないというね！！
Switchにつながってる機器ほとんど通信してないのでそりゃそうですね・・・SNMPつかえる無線ルータが欲しいです・・・。

# 気になったこと(改善すること)

## 集計値の初期化タイミングがわからない

今回取得しているSNMPのifHCInOctetsですが、どのタイミングで値が初期化されるかがよくわからないんですよね。
``snmpset``で値初期化できるかといろいろ試してみましたがうまくいかず・・・。

## 集計のマクロ化

いくら期間限定でも、これを手でやるのは面倒なのでマクロ化したいところですね、Excelなのでなおさらです。
いい感じにできるようになったら、追記したいと思います。

# おわりに

一番あるべき姿なのは、それなりのツールを入れて常にトラフィックの遷移を取得することですが、
今回のように**手をかけずにサクッと情報取得してほしい**という要望に対していい感じに答えられたのかと思ってます。

あとSNMP MIBの中を探索するの楽しい。

# 参考文献
- [MIB（Management Information Base）](https://www.itbook.info/network/nwope10.html)
- [net-snmpインストール](https://www.server-memo.net/server-setting/snmp/net-snmp.html)
- [cronのエスケープについて](https://blog.manabusakai.com/2013/05/crontab-escape-percent/)
- [トラフィック監視に関するOID](https://www.manageengine.jp/support/kb/OpManager/?p=481)