2014年8月18日月曜日

zabbixでLLD(Low Level Discovery)を使いネットワーク情報を取得


目的
zabbixでL2やL3のネットワーク機器のトラフィック情報を取得しグラフ化する。
また設定した閾値を超過した場合に検知できる仕組みを導入する。



環境と方針
zabbixのバージョンは2.X台を用いる。

ネットワーク機器のインターフェース毎の設定(アイテムの登録など)は
ローレベルディスカバリ、LLD(Low Level Discovery)を利用する。
多数のインターフェースをもっているスイッチに手動で
設定を入れていく、などということは当然しない。

ディスカバリのタイプはJSONファイルを作成しそこから独自のものを定義する。

カスタムで作成したLLD用のテンプレートにホストを紐づけると
ディスカバリが動くタイミングで指定したアイテムなどが自動で登録される。
ディスカバリは一度動かせばその後は無効にしてよい。
自動で登録されたアイテムごとの情報が定期的に動けばデータは得られる。

むしろ無効にしなかった場合、無駄な通信が走るばかりではなく、
例えば、インターフェースの状態がupからdown変わると、
up時のデータは削除され、新規にdownを正として登録されなおされてしまう。



設定
● JSONファイルを作成するツールの作成
独自のディスカバリのタイプを定義するためにJSONファイルを出力するツールを用意しておく。

先に出力例を見てもらったほうが理解が早いだろう。
#からマクロはLLDが判別するものである。通常の$で始まるマクロではない。
大文字でないとzabbixが認識しない。

$ /usr/lib/zabbix/externalscripts/interface.rb 192.0.2.1
{
        "data": [
        {
        "{#IFINDEX}": "10001",
        "{#IFALIAS}": "server001_eth0",
        "{#IFOPERSTATUS}": "up",
        "{#IFDESCR}": "GigabitEthernet1/0/1",
        "{#IFSPEED}": "1000000000",
        "{#IFINERRORS}": "0",
        "{#IFOUTERRORS}": "0",
        "{#ifHCInOctets}": "2653751290",
        "{#ifHCOutOctets}": "1363344818"
        },
        {
        "{#IFINDEX}": "10002",
        "{#IFALIAS}": "server002_eth0",
        "{#IFOPERSTATUS}": "up",
        "{#IFDESCR}": "GigabitEthernet1/0/2",
        "{#IFSPEED}": "1000000000",
        "{#IFINERRORS}": "0",
        "{#IFOUTERRORS}": "0",
        "{#ifHCInOctets}": "592074312",
        "{#ifHCOutOctets}": "1781710973"
        },
        {
        "{#IFINDEX}": "10003",
        "{#IFALIAS}": "server003_eth0",
        "{#IFOPERSTATUS}": "up",
        "{#IFDESCR}": "GigabitEthernet1/0/3",
        "{#IFSPEED}": "1000000000",
        "{#IFINERRORS}": "0",
        "{#IFOUTERRORS}": "0",
        "{#ifHCInOctets}": "2407211212",
        "{#ifHCOutOctets}": "2877179406"
        }
        ]
}


簡単なツールのはずだが実装は幾分ややこしくなっている。
ユニークなキーとして使うためにインデックス値(IF-MIB::ifIndex)を
使うつもりだったが、使用しなかった。これは説明が必要だろう。

インデックスを取得してみる。
$ snmpwalk  -v 2c -c public 192.0.2.1 IF-MIB::ifIndex
IF-MIB::ifIndex.5001 = INTEGER: 5001
IF-MIB::ifIndex.5002 = INTEGER: 5002
IF-MIB::ifIndex.5003 = INTEGER: 5003
IF-MIB::ifIndex.5004 = INTEGER: 5004
IF-MIB::ifIndex.10101 = INTEGER: 10101
IF-MIB::ifIndex.10102 = INTEGER: 10102
IF-MIB::ifIndex.10103 = INTEGER: 10103


適当にインターフェースごとの情報を取得する。
一部インデックスが見えなくなっている。
$ snmpwalk  -v 2c -c public 192.0.2.1 IF-MIB::ifHCOutOctets
IF-MIB::ifHCOutOctets.5001 = Counter64: 1687221597
IF-MIB::ifHCOutOctets.10101 = Counter64: 1363455181
IF-MIB::ifHCOutOctets.10102 = Counter64: 1781816238
IF-MIB::ifHCOutOctets.10103 = Counter64: 1954345413


原因は何だろうか。ディスクリプションを見てみる。
$ snmpwalk  -v 2c -c public 192.0.2.1 IF-MIB::ifDescr
IF-MIB::ifDescr.5001 = STRING: StackPort1
IF-MIB::ifDescr.5002 = STRING: StackSub-St1-1
IF-MIB::ifDescr.5003 = STRING: StackSub-St1-2
IF-MIB::ifDescr.10101 = STRING: GigabitEthernet1/0/1
IF-MIB::ifDescr.10102 = STRING: GigabitEthernet1/0/2
IF-MIB::ifDescr.10103 = STRING: GigabitEthernet1/0/3

StackSubポートが表示されていなかったようだ。他にも表示されなくなる条件はあるだろう。
そのため、確実に得たい情報、たとえばトラフィック情報を取得できるインターフェースを正とし、
そこからIDをとることにした。


ややこしいのはそれだけである。あとはすぐに分かるだろう。
$ vim /usr/lib/zabbix/externalscripts/interface.rb 
#!/usr/bin/ruby

$host = ARGV.shift
$community = "public"


class Snmpwalk
  @@indexes = Array.new
  attr_reader :results

  def initialize(mib_text, mib_num)
    @mib_text = mib_text
    @mib_num = mib_num
    @results = Array.new
    walk()
  end
end


class Index < Snmpwalk
  def walk()
    `snmpwalk -Oq -v 2c -c #{$community} #{$host} #{@mib_num}`.split("¥n").each do |line|
    line.scan(/\.(.*?) (.*)/) do |index, value|
      @@indexes.push(index)
      end
    end
  end

  def indexes
    @@indexes
  end
end


class Info < Snmpwalk
  def walk()
    `snmpwalk -Oq -v 2c -c #{$community} #{$host} #{@mib_num}`.split("¥n").each do |line|
      line.scan(/\.(.*?) (.*)/) do |index, value|
        value.slice!(/^"/); value.slice!(/"$/)  # ※1
        value = "nil_#{index}" if value == ""  # ※2
        @results.push(value) if @@indexes.include?(index)
      end
    end
  end
end


#$ifIndex = Index.new("ifIndex", ".1.3.6.1.2.1.2.2.1.1")
$ifIndex = Index.new("ifIndex", ".1.3.6.1.2.1.31.1.1.1.6")

$ifAlias = Info.new("ifAlias", ".1.3.6.1.2.1.31.1.1.1.18")
$ifOperStatus = Info.new("ifOperStatus", ".1.3.6.1.2.1.2.2.1.8")
$ifDescr = Info.new("ifDescr", ".1.3.6.1.2.1.2.2.1.2")
$ifSpeed = Info.new("ifSpeed", ".1.3.6.1.2.1.2.2.1.5")
$ifInErrors = Info.new("ifInErrors", ".1.3.6.1.2.1.2.2.1.14")
$ifOutErrors = Info.new("ifOutErrors", ".1.3.6.1.2.1.2.2.1.20")
#$ifInOctets = Info.new("ifInOctets", ".1.3.6.1.2.1.2.2.1.10")
#$ifOutOctets = Info.new("ifOutOctets", ".1.3.6.1.2.1.2.2.1.16")
$ifHCInOctets = Info.new("ifHCInOctets", ".1.3.6.1.2.1.31.1.1.1.6")
$ifHCOutOctets = Info.new("ifHCOutOctets", ".1.3.6.1.2.1.31.1.1.1.10")


def output()
  first = 1

  print "{\n"
  print "\t\"data\": [ \n"

  all = $ifAlias.results.length - 1
  for num in 0 .. all do
    next if $ifAlias.results[num] =~ /nil/
    print "\t},\n" if first != 1
    first = 0

    print "\t{\n"
    print "\t\"{#IFINDEX}\": \"#{$ifIndex.indexes[num]}\",\n"
    print "\t\"{#IFALIAS}\": \"#{$ifAlias.results[num]}\",\n"
    print "\t\"{#IFOPERSTATUS}\": \"#{$ifOperStatus.results[num]}\",\n"
    print "\t\"{#IFDESCR}\": \"#{$ifDescr.results[num]}\",\n"
    print "\t\"{#IFSPEED}\": \"#{$ifSpeed.results[num]}\",\n"
    print "\t\"{#IFINERRORS}\": \"#{$ifInErrors.results[num]}\",\n"
    print "\t\"{#IFOUTERRORS}\": \"#{$ifOutErrors.results[num]}\",\n"
    print "\t\"{#IFHCINOCTETS}\": \"#{$ifHCInOctets.results[num]}\",\n"
    print "\t\"{#IFHCOUTOCTETS}\": \"#{$ifHCOutOctets.results[num]}\"\n"
  end

  print "\t}\n"
  print "\t]\n";
  print "}\n";
end

output()
※1
ディスクリプションに""が入ることがあるので、それを省く。

※2
どこのマクロ値もノード単位でユニークなキー名にできるようにしていおく。


● テンプレートへオートディスカバリを設定
ディフォルトのテンプレートには"Template SNMP Interfaces"というものがあるが、
このままでは使いにくい。
ただし、まったく目的に合致しないわけではないため、
上記をすべて複製し、必要な部分を変更していけばいいだろう。
下記は設定のリンク図である。
-Configuration
  -Templates
    Template SNMP Interfaces からをFull cloneして作成する。
    名前は適当でよい。
    今回は Template SNMP Interfaces original とする。
    -Template SNMP Interfaces original
      -Items
        不要
      -Discovery
        -Name
          -Network interfaces
             Type: External check
              key : interface.rb[{HOST.IP}]
              
              アップしているインタフェースだけを対象とするために、
              {#IFOPERSTATUS} が up のものを選択するようにもできる。
              ただし、今回はjsonファイルを作る段階で必要、不要を判定させたため、
              ここでは何も指定しない。
              Filter Macro: {#IFOPERSTATUS}, Regexp: -
        -Items
          -Item prototypes
             以下のようなものがあればいいだろう。
             -Alias of interface {#IFALIAS}
             -Description of interface {#IFALIAS}
             -ifSpeed {#IFALIAS}
             -Inbound errors on interface {#IFALIAS}
             -Incoming traffic on interface {#IFALIAS}
             -Operational status of interface {#IFALIAS}
             -Outbound errors on interface {#IFALIAS}
             -Outgoing traffic on interface {#IFALIAS}              
         
             アイテムの設定は一例のみを示す。
             -Outgoing traffic on interface {#IFALIAS}              
              Name: Outgoing traffic on interface $1 ※3
              Type: SNMPv2 agent
              Key: ifHCOutOctets[{#IFALIAS}] ※4
              SNMP OID: IF-MIB::ifHCOutOctets.{#IFINDEX}
              SNMP community: {$SNMP_COMMUNITY}
              Port: 161
              Type of information: Numeric(unsigned)
              Data type: Decimal
              Units: bps
              Use custom multiplier: 8 ※5
              Store value: Delta(speed per second)



※3
キー内で指定する[]配列内の要素

※4
キー名は各ノード単位でユニークでなければならない。
インターフェースの役割が分かりやすいようにキー名にエイリアス名を使ったが、
もしエイリアスが重複する場合は、別の例えばユニークなインデックス値などを使った方がいい。

※5
1octets = 8bit



トラフィックの変化の検知
例として、IN側のトラフィックが上限の7割を
超えたら警告させるトリガーを作成する。
-Configuration
  -Templates
    -Template SNMP Interfaces original
      -Discovery
        -Triggers
           Severity: Warning
           Name: Traffic will over on {HOST.NAME} interface {#IFALIAS}
           Expression:
            
             (固定定値埋め込みの場合)
             ポートごとに上限が異なると使いにくいか。
             {Template SNMP Interfaces original:ifHCInOctets[{#IFALIAS}].last(0)} > 70000000            
             
             (ポート上限値の70%で発報させる場合)
             {Template SNMP Interfaces original:ifHCInOctets[{#IFALIAS}].last(0)} / {Template SNMP Interfaces original:ifSpeed[{#IFALIAS}].last(0)} > 0.7
            
             (現時点での3点の平均と、24時間前の3点の平均を比較して、
              トラフィックが1.3倍に増えたら発報させる場合)
             {Template SNMP Interfaces original:ifHCInOctets[{#IFALIAS}].avg(#3,0)} / {Template SNMP Interfaces original:ifHCInOctets[{#IFALIAS}].avg(#3, 86400)} > 1.3


トラフィック値はsnmpget(snmpwalk)してきた際に得られるカウンター値ではない。
データベースへはアイテムの登録時に指定した差分値で、
オクテットをビットに修正したbps単位で格納される。
そのため、トリガー内で計算させる必要はない。




デバッグメモ
● discoveryの際に使ったjson用ファイルを作成する外部コマンドが
 実行される際に以下のエラーが出た場合

Timeout while executing a shell script

以下のチューニングをするとよい。
# vim /etc/zabbix/zabbix_server.conf
# Specifies how long we wait for agent, SNMP device or external check (in seconds).
# Timeout=3
Timeout=5