2011年9月20日火曜日

TRAP(トラップ)の受信と監視サーバ(nagios)との連携

目的
監視対象ノードからのトラップを監視サーバのsnmptrapdで受理し、
その内容を同じく監視サーバで稼動しているサーバ(今回はnagiosを利用)へ通知させる。
nagiosの分散監視方法についてはこちらを参照のこと。


概要(トラップ処理の流れ)
1. トラップが飛んでくる

(トラップ例)
host101
UDP: [10.0.0.1]:59360
.1.3.6.1.2.1.1.3.0 87:21:51:52.92
.1.3.6.1.6.3.1.1.4.1.0 .1.3.6.1.6.3.1.1.5.3
.1.3.6.1.2.1.2.2.1.1.10106 10106
.1.3.6.1.2.1.2.2.1.2.10106 "eth0"
.1.3.6.1.2.1.2.2.1.3.10106 ethernet-csmacd
.1.3.6.1.4.1.9.2.2.1.1.20.10106 "down"
.1.3.6.1.6.3.18.1.3.0 10.24.7.14
.1.3.6.1.6.3.18.1.4.0 "public"
.1.3.6.1.6.3.1.1.4.3.0 .1.3.6.1.6.3.1.1.5


2. /usr/sbin/snmptrapdデーモンがトラップを受理


3. snmptrapd.conf内のtraphandleディレクティブで指定したコマンドが実行される
実行されるコマンドはトラップ内容を解析し、その結果を元にsend_nscaを実行して
パッシブなデータの転送処理を担当するNSCA(Nagios Service Check Acceptor)に通知させる。
そしてNSCA経由でNagiosは結果を受け取る。

※snmptrapd.confファイルの場所はsnmptrapdの起動スクリプト/etc/init.d/snmptrapdで指定される。



準備(監視サーバ側)
● インストール
# yum -y install net-snmp

# yum -y install nagios
# yum -y install nagios-nsca


トラップ受信時に一部データベースを使うためである(後ほど説明する)。
# yum -y install mysql mysql-server



● /etc/init.d/snmptrapdファイルの設定
飛んできたtrapを名前変換させないようにする。
# cp /etc/init.d/snmptrapd /etc/init.d/snmptrapd.yyyymmdd
# vi /etc/init.d/snmptrapd
(前)
OPTIONS="-Lsd -p /var/run/snmptrapd.pid"
(後)
OPTIONS="-On -Lsd -p /var/run/snmptrapd.pid"



● snmptrapd.confファイルの設定
飛んできたtrapの認証は不要とする。
traphandle ディレクティブで実行するコマンドを指定する。
# cp /etc/snmp/snmptrapd.conf /etc/snmp/snmptrapd.conf.yyyymmdd
# vi /etc/snmp/snmptrapd.conf
  disableAuthorization yes

  # traphandle default /usr/local/bin/trap.sh ← (a)
  traphandle default /usr/local/bin/trap.rb ← (b)



● traphandleディレクティブで指定したツールの概要
(a)
単純にトラップの内容をログに追記する。
(a)は今回は使わないが、単純にトラップの内容をログに出すだけならこれで十分だろう。
コードは別途下記に記載する。

(b)
(a)と同様にトラップの内容をログに追記していくのだが、
トラップの内容からデータベースを参照してoidの数字をテキストに変換、
また発報させるかさせないのかを判断をさせる処理を加えている。

データベースを用意する目的
1) 数字のoidでは内容が直感的に分からないため、テキストに変換する役割
2) 監視アラームとして発報させる、させないを制御する役割


そして、nagiosへの通知手段としてnagios-nscaをツール内で実行。


● トラップ処理ツールのコード
(a)
# vi /usr/local/bin/trap.sh
#!/bin/sh

echo --------------------------------- >> /var/log/snmptrap/traphandle.log
echo  "`date  '+%Y%m%d%H%M%S'`" >> /var/log/snmptrap/traphandle.log

while read line
do
  echo  "$line" >> /var/log/snmptrap/traphandle.log
done

# chmod 755 /usr/local/bin/trap.sh


(b)
# vi /usr/local/bin/trap.rb
#!/usr/bin/ruby

require 'mysql'

def error(exc)
  f = open('/var/log/snmptrap/traphandle_error.log','a+')
    f.puts '---------------'
    f.puts(Time.now.strftime("%Y%m%d%H%M%S"))
    f.puts(exc)
  f.close
  exit 1
end


def no_alert(host, mib_num)
  f = open('/var/log/snmptrap/traphandle_no_alert.log','a+')
    f.puts '---------------'
    f.puts(Time.now.strftime("%Y%m%d%H%M%S"))
    f.puts("host      :  #{host}")
    f.puts("mib_num   :  #{mib_num}")
  f.close
  exit 0
end


def alert(host, mib_text)
  begin
    command = "/usr/lib/nagios/plugins/submit_check_result #{host} 'TRAP [#{mib_text}]' 1 'check /var/log/snmptrap/traphandle.log'"
    #system command
    puts command

  rescue  => exc
    error(exc)
  end
  exit 0
end


# TRAP RECEIVE
begin
  f = open('/var/log/snmptrap/traphandle.log','a+')
    f.puts '---------------'
    f.puts(Time.now.strftime("%Y%m%d%H%M%S"))

    host = gets.chomp!

    # /etc/hosts ファイルへホスト名の登録が必要
    if host  =~ /.*: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\d)/  ||  host  =~ /<UNKNOWN>/
      host = 'other'
    end

    f.puts(host)
    f.puts(gets)
    f.puts(gets)
    oid = gets
    f.puts(oid.chomp!)
    mib_num = oid.scan(/.* (.*)/)

    while line = gets
      f.puts(line)
    end

  f.close

rescue  => exc
  error(exc)
end


# DATABASE
begin
  db = Mysql::new('localhost', 'trapuser', 'password', 'trapdb')

  res = db.query("select id from ignore_table where mib_id = (select id from mib_table where mib_num = '#{mib_num}')")
  id = nil
  res.each { | i | id = i }

  no_alert(host, mib_num) if id != nil

  res = db.query("select host, mib_text from host_table as h, mib_table as m, host_mib_table as hm where h.id = hm.host_id and m.id = hm.mib_id and h.host = '#{host}' and mib_num = '#{mib_num}'")
  mib_text = nil
  res.each { | h, m | mib_text = m }

  mib_text = 'other' if mib_text == nil
  alert(host, mib_text)

rescue  => exc
  error(exc)

ensure
  db.close
end

# chmod 755 /usr/local/bin/trap.rb


# vi /usr/lib/nagios/plugins/submit_check_result
#!/bin/sh

printfcmd="/usr/bin/printf"
NscaBin="/usr/sbin/send_nsca"
NscaCfg="/etc/nagios/send_nsca.cfg"
NagiosHost1=10.0.0.1

$printfcmd "%s\t%s\t%s\t%s\n" "$1" "$2" "$3" "$4" | $NscaBin $NagiosHost1 -p 5667 -c $NscaCfg

# chmod 755 /usr/lib/nagios/plugins/submit_check_result



● 各種プロセスの起動
# /etc/init.d/snmptrapd start
# /etc/init.d/mysqld start

# chkconfig snmptrapd on
# chkconfig mysqld on



● データベースの準備
mysqlの初期設定
# mysql_secure_installation
Set root password? [Y/n]                      ← 空ENTER(rootパスワード設定)
New password:                                 ← rootパスワード応答
Re-enter new password:                        ← rootパスワード応答(確認)
Remove anonymous users? [Y/n]                 ← 空ENTER(匿名ユーザー削除)
Disallow root login remotely? [Y/n]           ← 空ENTER(リモートからのrootログイン禁止)
Remove test database and access to it? [Y/n]  ← 空ENTER(testデータベース削除)
Reload privilege tables now? [Y/n]            ← 空ENTER


ルートユーザでログイン
# mysql -u root -p
Enter password:


ユーザの作成
> GRANT ALL PRIVILEGES ON trapdb.* TO trapuser@localhost IDENTIFIED BY 'password';
> quit


トラップDBを操作するユーザで再ログイン
# mysql -u trapuser -p
Enter password:


データベースの作成
> CREATE DATABASE trapdb;


データベースへ接続
> USE trapdb;


テーブルの作成
ホスト管理テーブル
> CREATE TABLE host_table (
id INT NOT NULL AUTO_INCREMENT,
host VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY(id));

> SHOW COLUMNS FROM host_table;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| host  | varchar(255) | NO   | UNI | NULL    |                |
+-------+--------------+------+-----+---------+----------------+


mib管理テーブル
> CREATE TABLE mib_table (
id INT NOT NULL AUTO_INCREMENT,
mib_num VARCHAR(255) NOT NULL UNIQUE,
mib_text VARCHAR(255) NOT NULL,
PRIMARY KEY(id));

> SHOW COLUMNS FROM mib_table;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| mib_num  | varchar(255) | NO   | UNI | NULL    |                |
| mib_text | varchar(255) | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+


ホストとmib管理テーブル
> CREATE TABLE host_mib_table (
host_id INT NOT NULL,
mib_id INT NOT NULL,
FOREIGN KEY(host_id) REFERENCES host_table(id),
FOREIGN KEY(mib_id) REFERENCES mib_table(id));

> SHOW COLUMNS from host_mib_table;
+---------+---------+------+-----+---------+-------+
| Field   | Type    | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| host_id | int(11) | NO   | MUL | NULL    |       |
| mib_id  | int(11) | NO   | MUL | NULL    |       |
+---------+---------+------+-----+---------+-------+


不要トラップ管理テーブル
> CREATE TABLE ignore_table (
id INT NOT NULL AUTO_INCREMENT, 
mib_id INT NOT NULL UNIQUE, 
PRIMARY KEY(id), 
FOREIGN KEY(mib_id) REFERENCES mib_table(id));

> SHOW COLUMNS FROM ignore_table;
+--------+---------+------+-----+---------+----------------+
| Field  | Type    | Null | Key | Default | Extra          |
+--------+---------+------+-----+---------+----------------+
| id     | int(11) | NO   | PRI | NULL    | auto_increment |
| mib_id | int(11) | NO   | MUL | NULL    |                |
+--------+---------+------+-----+---------+----------------+


登録
ホストの登録
> INSERT INTO host_table (host) VALUES('host101');
> INSERT INTO host_table (host) VALUES('host102');
> INSERT INTO host_table (host) VALUES('host103');

> SELECT * FROM host_table;
+----+---------+
| id | host    |
+----+---------+
|  1 | host101 |
|  2 | host102 |
|  3 | host103 |
+----+---------+


MIBの登録
> INSERT INTO mib_table (mib_num, mib_text) VALUES('.1.3.6.1.6.3.1.1.5.3', 'linkDown');
> INSERT INTO mib_table (mib_num, mib_text) VALUES('.1.3.6.1.6.3.1.1.5.4', 'linkUp');

> SELECT * FROM mib_table;
+----+----------------------+-----------------------+
| id | mib_num              | mib_text              |
+----+----------------------+-----------------------+
|  1 | .1.3.6.1.6.3.1.1.5.3 | linkDown              |
|  2 | .1.3.6.1.6.3.1.1.5.4 | linkUp                |
|  3 | .1.3.6.1.6.3.1.1.5.5 | authenticationFailure |
+----+----------------------+-----------------------+


不要トラップの登録
> INSERT INTO ignore_table (mib_id) VALUES(13);

> SELECT * FROM mib_table;
+----+--------+
| id | mib_id |
+----+--------+
|  1 |      3 |
+----+--------+


ホストとMIBの登録
下のようにホストとMIBを結びつけるようにデータベースに登録する。
host101 ⇔ linkDown
host101 ⇔ linkUp
host102 ⇔ linkDown

host_mib_tableをこのようにしたいわけである。
+---------+--------+
| host_id | mib_id |
+---------+--------+
|       1 |      1 |
|       1 |      2 |
|       2 |      1 |
+---------+--------+



アラーム発報
上記のようにホストとMIBを対応させた場合にどのように発報するのかまとめておく。

・ host101
 linkDown              → host101, TRAP [lindDown] として発報する
 linkUp                → host101, TRAP [lindUp] として発報する
 authenticationFailure  発報しない
 未定義TRAP             → host101, TRAP [other] として発報する

・ host102
 linkDown              → host102, TRAP [lindDown] として発報する
 linkUp                → host102, TRAP [other] として発報する
 authenticationFailure → 発報しない
 未定義TRAP             → host102, TRAP [other] として発報する

・ host103
 linkDown              → host101, TRAP [other] として発報する
 linkUp                → host101, TRAP [other] として発報する
 authenticationFailure → 発報しない
 未定義TRAP             → host103, TRAP [other] として発報する

・ 未定義ホスト
 linkDown              → other, TRAP [other] として発報する
 linkUp                → other, TRAP [other] として発報する
 authenticationFailure → 発報しない
 未定義TRAP             → other, TRAP [other] として発報する


idごとにひもづけるのだが手間がかかるのでツールを用意しておく。

# /usr/lib/nagios/plugins/update_db.rb [ホスト名] [mib(oid)] [insert|delete]

# /usr/lib/nagios/plugins/update_db.rb host101 .1.3.6.1.6.3.1.1.5.3 insert
# /usr/lib/nagios/plugins/update_db.rb host101 .1.3.6.1.6.3.1.1.5.4 insert
# /usr/lib/nagios/plugins/update_db.rb host102 .1.3.6.1.6.3.1.1.5.3 insert
削除したい場合は"insert"を"delete"に変える


# vi /usr/lib/nagios/plugins/update_db.rb 
#!/usr/bin/ruby

require 'mysql'

host = ARGV[0]
mib = ARGV[1]
action = ARGV[2]

if ARGV.length !=  3
  puts "argument error : ./update_db.rb [host] [mib number] [insert | delete]"
  exit 1
end

$db = Mysql::new('localhost', 'trapuser', 'password', 'trapdb')


def select(q)
  begin
    id = nil
    query = q
    res = $db.query(query);

    res.each do | i |
      id = i
    end
    return id

  rescue  => exc
    puts exc
  end
end


def insert(host_id, mib_id)
  begin
    query = "insert into host_mib_table (host_id, mib_id) values(#{host_id}, #{mib_id})"
    $db.query(query);
    puts "OK : #{query}"
    exit 0

  rescue  => exc
    puts exc
  end

end


def delete(host_id, mib_id)
  begin
    query = "delete from host_mib_table where host_id = #{host_id} and mib_id = #{mib_id}"
    $db.query(query);
    puts "OK : #{query}"
    exit 0

  rescue  => exc
    puts exc
  end
end


def check_duplicate(host_id, mib_id)
  begin
    query = "select host_id, mib_id from host_mib_table where host_id = #{host_id} and mib_id = #{mib_id}"
    res = $db.query(query);
    flag = nil
    res.each do | i |
      flag = 1
    end

    if flag == 1
      puts("ERROR : Already registered");
      exit 1
    end

  rescue  => exc
    puts exc
  end
end


query = "select id as host_id from host_table where host = '#{host}'"
host_id = select(query)
if host_id == nil
  puts "ERROR : '#{host}' is not registered!!"
  exit 1
end


query = "select id as mib_id from mib_table where mib_num = '#{mib}'"
mib_id = select(query)
if mib_id == nil
  puts "ERROR : '#{mib}' is not registered!!"
  exit 1
end


if action == "insert"
  check_duplicate(host_id, mib_id)
  insert(host_id, mib_id)

elsif action == "delete"
  delete(host_id, mib_id)

else
  puts "ERROR : Action is bad."
  puts "Action is [insert|delete]."
  exit 1
end



● nagiosの準備
設定
トラップを受け付けるサービス設定は以下でよい。
細かい設定に関しては省略する。

define service{
   use                     generic-service
   host_name               host01, host02
   service_description     TRAP [LinkUp]
   check_period            none
   active_checks_enabled   1
   max_check_attempts      1
   is_volatile             1
   check_command           check_dummy!0!"OK"
}

define service{
   use                     generic-service
   host_name               host03
   service_description     TRAP [LinkDown]
   check_period            none
   active_checks_enabled   1
   max_check_attempts      1
   is_volatile             1
   check_command           check_dummy!0!"OK"
}



文法チェック
# nagios -v /etc/nagios/nagios.cfg


起動
# /etc/init.d/nagios restart
# chkconfig nagios on



● nscaの準備
設定
設定ファイルは下だが、基本ディフォルトのままでいいだろう。
/etc/nagios/nsca.cfg

nscaは5667/tcpポートでsend_nscaからの結果を受け取るので、
iptablesなど確認しておいた方がいいだろう。

起動
nacaも立ち上げておく
# /etc/init.d/nsca restart
# chkconfig nsca on



◆ 準備(監視対象ノード側)
監視対象ノードがLinuxサーバであれば、
/etc/snmp/snmpd.confファイルを編集する
下のディレクティブに監視サーバのIPアドレスを入れておけばいいだろう

trapsink 監視サーバのIPアドレス
trap2sink 監視サーバのIPアドレス



◆ 発報試験
1) 監視対象ノードからトラップを飛ばす


2) 監視サーバ側でログを確認する
2-1) trap.rbが処理するログの確認
アラームとして発報させるtrapのログ
/var/log/snmptrap/traphandle.log

trapが飛んできたけれどテーブルに登録していない(アラートをあげない)場合のログ
/var/log/snmptrap/traphandle_no_alert.log

例外発生時のログ
/var/log/snmptrap/traphandle_error.log


2-2) nagiosが処理するログ
/var/log/nagios/nagios.log


監視対象ノードからトラップを飛ばせないのであれば、
監視サーバ側で手動で試験できる。

ツールを実行
# /usr/local/bin/trap.rb
以下をまるごと貼り付け
host101
UDP: [10.0.0.1]:59360
.1.3.6.1.2.1.1.3.0 87:21:51:52.92
.1.3.6.1.6.3.1.1.4.1.0 .1.3.6.1.6.3.1.1.5.3
.1.3.6.1.2.1.2.2.1.1.10106 10106
.1.3.6.1.2.1.2.2.1.2.10106 "eth0"
.1.3.6.1.2.1.2.2.1.3.10106 ethernet-csmacd
.1.3.6.1.4.1.9.2.2.1.1.20.10106 "down"
.1.3.6.1.6.3.18.1.3.0 10.24.7.14
.1.3.6.1.6.3.18.1.4.0 "public"
.1.3.6.1.6.3.1.1.4.3.0 .1.3.6.1.6.3.1.1.5
Ctrl + D で抜ける

"1 data packet(s) sent to host successfully."

と出ればツールは正常に動作している。
それでもnagios側で表示されないようであれば、
nagiosの設定の問題であろう。