PlaggerでNW管理ってできないかな

はじめに

大それたことを考えています.nagiosを入れるほど大規模じゃないんだけど,何もないのはつらい,ってな規模の監視ができないかなと思ってます.でも,最初から『ぱきっと』モジュールを作れるわけではないので,まずネタをまとめて簡単に仕様作りなどと考えてみました.

やってみたいのは…

  1. 各種設定はYAMLで記述する
  2. POEを使って管理対象から定期的に状態を取得する
  3. Plaggerとのインタフェースを持つ
  4. 問題が起きた時にはイベントを投げる

ってなところ.今迷っているのは,「Plaggerとのインタフェース」の部分です.PlaggerモジュールでPOEを使っているモジュールには,Plagger::Plugin::Aggregator::XangoとかPlagger::Plugin::Notify::IRCなんてのがあるんですが,それぞれ使い方が違ってます.Plagger::Plugin::Aggregator::Xangoの方は(たぶん)イベントとしてurlリストを渡してやると,データを取得し,呼び出し元に返したら,制御がモジュールから離れるんだけど,Plagger::Plugin::Notify::IRCの方は,POEがPlaggerとは別のプロセスとして永続的に動いていて,POE::Component::IKC::ClientLiteというプロトコルで,PlaggerがPOEを呼び出すことでIRCに投稿します.
状態を取得するのはAggregatorだからXangoチックにしたいんだけど,監視系自体は永続的に動いていてPlaggerが偶に状況を聞きに来るという作りにしたいからNotify::IRCっぽくIKEで通信してデータを取得するか,それともめんどくさいことは考えずに,POEが監視データをシリアライズしてファイルかDBに格納,Plaggerはそれを復元して表示なんて方法がいいのかなぁ,とまぁそんなところです.

で,とりあえず…

悩むのに飽きたので,作れるところから作ってみました.まずは,YAMLで設定を書いておいて,条件があったらIPMessengerでイベントを飛ばすスクリプトYAMLで書く設定はこんな感じ.

--- 10.0.0.1
---
community: public
version: SNMPv2
msgto:
  - kdaiba.foo.co.jp
oids:
  .1.3.6.1.2.1.31.1.1.1.6.50: >
    [% bps = (last - prev) div (lasttime - prevtime) %]
    [% IF bps > 1000000000 %]
    C6506 IF50's IN traffic is now [% ifspeed(speed = bps) %]
    [% END %]
  .1.3.6.1.2.1.31.1.1.1.10.50: >
    [% IF bps > 1000000000 %]
    [% bps = (last - prev) div (lasttime - prevtime) %]
    C6506 IF50's OUT traffic is now [% ifspeed(speed = bps) %]
    [% END %]
  .1.3.6.1.2.1.2.2.1.7.50: >
    [% IF last != prev %]
    C6506 IF50's status changed to
    [% SWITCH last %]
    [% CASE 1 %]up
    [% CASE 2 %]down
    [% CASE 3 %]testing
    [% END %]
    [% END %]
timespan: 60

この設定でやろうとしていることは,

  • 監視対象機器は10.0.0.1
  • SNMPv2cを使い,publicというコミュニティネームでアクセス
  • 50番目のインタフェース(実際にこれがどの物理ポートになるのかは,ここの情報だけではわからない)のCounter64タイプでの入出力オクテット数を取得し,1Gbpsを越えたら通知
  • 50番目のインタフェース状態に変更があったら通知
  • IPmessengerでの通知先はkdaiba.foo.co.jpというホスト
  • 情報収集は60秒ごとに実施

となります.YAMLフォーマットは全然わからなかったんですが,

#!/bin/perl
use YAML qw (DumpFile);
use strict;
use warnings;
my %data;
%data = ( ... );
print DumpFile "conf.yaml", %data;

とやれば必要なフォーマットが作れることに気がついてからは,さくっと作れるようになりました.ちなみに'>'は,マニュアルを見ていて気がついた改行を無視してさせるための記述方法で,スクリプトが読み込んだときには連続した文章になります.

妙にごちゃごちゃしてるけど…

この設定ファイルを読み込んでメッセージを飛ばすスクリプトは次のようになります.毎度のことながら長くてすみません.

#!/bin/perl
use POE qw(Component::SNMP);
use YAML;
use Template;
use strict;
use warnings;

my ($file, %conf, $t, $output, $macro);

$file = shift || "./snmpconf.yaml";
if (-e $file && -r _){
	%conf = YAML::LoadFile($file);
}else{
	die "$file: $!";
}

$t = Template->new({
  PRE_CHOMP   => 1,
  POST_CHOMP  => 1,
  OUTPUT      => \$output,
}) || die "$Template::ERROR\n";

$macro = <<M_END;
[% MACRO ifspeed BLOCK %]
[% IF speed >= 1000000000 %]
  [% gspeed = speed div 1000000000 %]
  [% gspeed %] Gbps
[% ELSIF speed >= 1000000 %]
  [% mspeed = speed div 1000000 %]
  [% mspeed %] Mbps
[% ELSIF speed >= 1000 %]
  [% kspeed = speed div 1000 %]
  [% kspeed %] Kbps
[% ELSE %]
  [% speed %] bps
[% END %]
[% END %]
M_END

POE::Session->create(inline_states => {
  _start      => \&_start,
  trigger     => \&trigger,
  snmp_get_cb => \&snmp_get_cb,
  tipmsg      => \&tipmsg,
});
POE::Kernel->run;

sub _start{
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

  for my $host (keys %conf){
    my $community = $conf{$host}{community};
    my $version   = $conf{$host}{version};
    my $timespan  = $conf{$host}{timespan};
    my $oids_info = $conf{$host}{oids};
    my @oids      = keys %$oids_info;

    POE::Component::SNMP->create(
      -hostname  => $host,
      -version   => $version,
      -community => $community,
      -alias     => $host,
      -timeout   => 5,
    );
    $heap->{fields}{$host}{oids} = \@oids;
    $heap->{fields}{$host}{timespan} = $timespan;
    map{
      $heap->{fields}{$host}{oid}{$_} = $conf{$host}{oids}{$_}
    }@oids;
    $kernel->yield('trigger' => $host);
  }
}

sub trigger {
  my ($kernel, $heap, $host) = @_[KERNEL, HEAP, ARG0];
  $kernel->post($host => 'get',
    'snmp_get_cb',
    -varbindlist => $heap->{fields}{$host}{oids});
}

sub snmp_get_cb {
  my ($kernel, $heap, $input_ref, $result_ref) = 
    @_[KERNEL, HEAP, ARG0, ARG1];
  my ($aliase, $host) = @$input_ref;
  my ($results, $error) = @$result_ref;

  if(ref $results){
    for my $oid (@{$heap->{fields}{$host}{oids}}){
      my (%vars, $text);
      $vars{prevtime} = $heap->{fields}{$host}{vars}{$oid}{time} || 0;
      $vars{lasttime} = time;
      $vars{prev}     = $heap->{fields}{$host}{vars}{$oid}{last};
      $vars{last}     = @{$results}{$oid};
      $text = $macro . $heap->{fields}{$host}{oid}{$oid};

      if($vars{prevtime} != 0){
        $t->process(\$text, \%vars) || die $t->error(), "\n";
        $kernel->yield('tipmsg' => $output, $host) if ($output);
        $output = '';
      }

      $heap->{fields}{$host}{vars}{$oid}{last} = $vars{last};
      $heap->{fields}{$host}{vars}{$oid}{time} = $vars{lasttime};
    }
  }elsif ($results =~ /No response from remote host/){
    warn "No responce\n";
  }else{
    warn "something wrong: $results\n";
  }
  $kernel->delay_add('trigger' => $heap->{fields}{$host}{timespan}, $host);
}

sub tipmsg {
  my ($record, $host) = @_[ARG0, ARG1];
  for my $to (@{$conf{$host}{msgto}}){
    open TIPMSG, "|/usr/local/bin/tipmsg -s $to";
    print TIPMSG $record;
    close TIPMSG;
  }
}

例によって,抜粋しながら解説します.

$file = shift || "./snmpconf.yaml";
if (-e $file && -r _){
	%conf = YAML::LoadFile($file);
}else{
	die "$file: $!";
}

YAMLで書いた設定ファイルを読み込むと元のデータに戻ります.次に,

$t = Template->new({
  PRE_CHOMP   => 1,
  POST_CHOMP  => 1,
  OUTPUT      => \$output,
}) || die "$Template::ERROR\n";

テンプレートを作ります.改行を含めた空白記号をきれいにするためにPRE_CHOMP, POST_CHOMPを入れています.テンプレートを適用した結果をグローバルな$output変数に入れているのはちょっと気持ちが悪い気もしますが,まぁしばらくは気にしない(w

$macro = <<M_END;
[% MACRO ifspeed BLOCK %]
[% IF speed >= 1000000000 %]
  [% gspeed = speed div 1000000000 %]
  [% gspeed %] Gbps
[% ELSIF speed >= 1000000 %]
  [% mspeed = speed div 1000000 %]
  [% mspeed %] Mbps
[% ELSIF speed >= 1000 %]
  [% kspeed = speed div 1000 %]
  [% kspeed %] Kbps
[% ELSE %]
  [% speed %] bps
[% END %]
[% END %]
M_END

Templateモジュールは簡単な言語を持ってます.制御用のIF, ELSIF, ELSE, ENDなんてキーワードが使えます.ここではifspeedというマクロを作っています.

sub _start{
  ...
  for my $host (keys %conf){
    ...
    $kernel->yield('trigger' => $host);
  }
}

監視対象のホストごとにtriggerという関数を呼び出してkernelに登録しています.ホントは分けた方がいいのかもしれないですが,1sessionで動かしています.

sub trigger {
  my ($kernel, $heap, $host) = @_[KERNEL, HEAP, ARG0];
  $kernel->post($host => 'get',
    'snmp_get_cb',
    -varbindlist => $heap->{fields}{$host}{oids});
}

oid(一度に複数個を指定可能)をエンコードして監視対象に投げています.監視対象からパケットが返ってきたときにそれを処理する関数が'snmp_get_cb'となります.

sub snmp_get_cb {
...
      if($vars{prevtime} != 0){
        $t->process(\$text, \%vars) || die $t->error(), "\n";
        $kernel->yield('tipmsg' => $output, $host) if ($output);
        $output = '';
      }
...
  $kernel->delay_add('trigger' => $heap->{fields}{$host}{timespan}, $host);
}Te

%varsに必要なデータを設定してからTemplate処理を実行します.で,その結果の$outputをtipmsgという関数に引き渡しています.最後にtimespanたったら,またtrigger関数を実行するようにKERNELに登録します.

sub tipmsg {
  my ($record, $host) = @_[ARG0, ARG1];
  for my $to (@{$conf{$host}{msgto}}){
    open TIPMSG, "|/usr/local/bin/tipmsg -s $to";
    print TIPMSG $record;
    close TIPMSG;
  }
}

tipmsgというのはText IP Messenger ver0.4にあるunix用のc言語で書かれたプログラムです.常駐部と呼び出し部からできていて,ここで出てくる'tipmsg -s'というのは呼び出しをしています.

おわりに

なんとなく動くスクリプトを作ってみてはものの,

  1. POEって奥が深い
  2. Commponent::SNMPって,trapの受信ができないよぉ〜ん
  3. IP messengerをしゃべるPOEってホスイ
  4. 管理対象から集めてきたデータをどういう風に集約すればPlaggerで集めたときに使いやすい構成になるのかなぁ

なんてことが次々に立ちはだかっていて,まだまだ先は長いです.POEベースのhttpサーバ上にrssフィードを定期的に吐き出すようにすれば,それをPlaggerでsubscribeしてやるだけですむなぁ,なんてことも考えてたり.