Nagios Event Broker を使ってみた

Nagios Event Broker (NEB) は Nagios からイベントを受け取るためのモジュール。
Nagios からの通知に対して特殊な操作を行いたいときに使う。

NEB のドキュメント

Nagios: Developer Information から取得できる。バージョンが古いけれども、基本的な部分は変わっていない。

ただ、このリファレンスに書かれているプロパティだけだとほとんど重要なデータが取れないので、最終的には Nagios のヘッダとソースを眺めて、どういったプロパティやイベントがあるのかを頑張って調べる必要がある。
そういった部分について調べたりとかもしたので、まとめておくことにする。

あとは iVoyeur: you should write an NEB module, revisited とかも最初は参考になる。

Nagios の make install に失敗する件

これは NEB ではなく Nagios の話なんだけれども、現在の最新である 3.3.1 をビルドして make install しようとすると、なぜか失敗した。
これは html/Makefile(./configure する前なら html/Makefile.in)を以下のように編集することで修正できる(赤文字が追加した部分)。

78:    for file in includes/rss/*.*; \
80:    for file in includes/rss/extlib/*.*; \

NEB のビルド

NEB は共有オブジェクトとしてビルドし、nagios.cfg の broker_module にその共有オブジェクトのパスを指定することで動作する。

broker_module=/usr/local/hoge/fuga.so

デフォルトだとコメントアウトされてるだけなのですぐに分かると思う。
あとビルドする際、コンパイルオプションに -DNSCORE を指定しておくこと。

基本的な使い方

モジュールがロードされた時に nebmodule_init 関数が呼ばれるので、その中で neb_register_callback 関数を使ってイベントが起こったときにどの関数を呼び出して欲しいかを登録する。

static int cbfunc(int type, void* data);

// モジュールロード時に呼ばれる
extern "C" int nebmodule_init(int flags, char* args, nebmodule* handle) {
    neb_register_callback(NEBCALLBACK_SERVICE_CHECK_DATA, handle, 0, &cbfunc);
    return OK;
}

あとはこのコールバックを受け取ったときにいろいろするだけ。
だけとは言っても、例えば今回は NEBCALLBACK_SERVICE_CHECK_DATA という種類のコールバックを指定したけど、他にも様々な種類のコールバックがあるので、それぞれのコールバック毎に違う処理を書く必要があって結構大変。

static int cbfunc(int type, void* data) {
    // NEBCALLBACK_SERVICE_CHECK_DATA
    if (type == NEBCALLBACK_SERVICE_CHECK_DATA) {
        // type が NEBCALLBACK_SERVICE_CHECK_DATA だった場合は data は必ずこの構造体になっている
        auto p = static_cast<const nebstruct_service_check_data*>(data);
        // p を使っていろいろする
    }
}

モジュールがアンロードされる時に nebmodule_deinit が呼ばれるので、後はここで終了処理を行うだけでいい。

// モジュールアンロード時に呼ばれる
extern "C" int nebmodule_deinit(int flags, int reason) {
    // コールバック登録の解除
    neb_deregister_callback(NEBCALLBACK_SERVICE_CHECK_DATA, &cbfunc);
    return OK;
}

ちなみにコールバックの解除はわざわざやらなくてもモジュールアンロード時に勝手にやってくれるので必要なかったりもする。


基本的なところはこれだけ。あとはひたすらコールバック関数内でやりたいことをやるだけ。

ログ出力

Nagios はいろんな情報をログへ出力している。自分の環境だと /usr/local/nagios/var/nagios.log にあったけど、これは nagios.cfg にある log_file を弄ることで自由にパスを変更できる。
このログ出力は NEB からも呼び出すことができるので、そういうラッパーを作っておくと便利。

#include <logging.h>

struct log {
    static void put(const char* message) {
        write_to_log(const_cast<char*>(message), NSLOG_INFO_MESSAGE, NULL);
    }
};

const_cast とかしてるけど、インターフェースが char* になってるんだから仕方がない。これだから C 言語は(ry
NSLOG_INFO_MESSAGE 固定にしてるけど、他の値も指定できるようにしておくと便利かも(logging.h に定義してあるので見ること)。


あとこれとは別に、デバッグトレース用のログ出力というのがある。
これは nagios.cfg の debug_level, debug_verbosity, debug_file, max_debug_file_size あたりを弄ればいい。コメントが書かれているのでこれらを出力する方法はすぐ分かるはず。
NEB 側からこのデバッグ用ログに出力するには logit 関数を使えばいい。

コールバックに自由にデータを渡す

neb_register_callback には int (*)(int, void*) な関数しか渡せないので、普通にやると外部の情報を渡すにはグローバル変数を経由しないといけなくなる。
さすがにそれは微妙なので、こんなクラスを作っておくと便利。

class neb_module {
    nebmodule* handle_;

    template<class T>
    struct cbfunc_t {
        typedef T data_type;
        typedef std::function<void (data_type&)> func_type;
        static func_type& func() {
            static func_type obj;
            return obj;
        }
    };

    template<class T>
    static int cbfunc(int, void* data) {
        if (!data) return ERROR;

        auto sd = static_cast<typename T::data_type*>(data);
        try {
            T::func()(*sd);
        } catch (std::exception& e) {
            log::put((std::string("exception occured: ") + e.what()).c_str());
            return ERROR;
        } catch (...) {
            log::put("unknown exception occured");
            return ERROR;
        }

        return OK;
    }

    template<class T, class F>
    void internal_register(int callback_type, F f) {
        typedef cbfunc_t<T> type;
        type::func() = f;
        neb_register_callback(callback_type, handle_, 0, &neb_module::cbfunc<type>);
    }
    template<class T>
    void internal_deregister(int callback_type) {
        typedef cbfunc_t<T> type;
        neb_deregister_callback(callback_type, &neb_module::cbfunc<type>);
    }

public:
    neb_module(nebmodule* handle) : handle_(handle) { }

    // NEBCALLBACK_PROCESS_DATA
    // void F(const nebstruct_process_data&);
    template<class F>
    void register_process(F f) {
        internal_register<nebstruct_process_data>(NEBCALLBACK_PROCESS_DATA, f);
    }
    void deregister_process() {
        internal_deregister<nebstruct_process_data>(NEBCALLBACK_PROCESS_DATA);
    }

    // NEBCALLBACK_HOST_CHECK_DATA
    // void F(const nebstruct_host_check_data&);
    template<class F>
    void register_host_check(F f) {
        internal_register<nebstruct_host_check_data>(NEBCALLBACK_HOST_CHECK_DATA, f);
    }
    void deregister_host_check() {
        internal_deregister<nebstruct_host_check_data>(NEBCALLBACK_HOST_CHECK_DATA);
    }

    // NEBCALLBACK_SERVICE_CHECK_DATA
    // void F(const nebstruct_service_check_data&);
    template<class F>
    void register_service_check(F f) {
        internal_register<nebstruct_service_check_data>(NEBCALLBACK_SERVICE_CHECK_DATA, f);
    }
    void deregister_service_check() {
        internal_deregister<nebstruct_service_check_data>(NEBCALLBACK_SERVICE_CHECK_DATA);
    }

    ...
};

c_function と同じような方法で実装している。
これで登録する際に自由に関数オブジェクトを渡すことができるようになる。

nebstruct_service_check_data 内のデータについて

Nagios の設定ファイルで、

define host {
    host_name      host1
    ...
}
define host {
    host_name      host2
    ...
}
define hostgroup {
    hostgroup_name group
    members        host1,host2
}
define service {
    host_name      host1,host2
    ...
}

とやってサービスの監視を行うけれども、これの各ホスト毎のチェック結果が NEBCALLBACK_SERVICE_CHECK_DATA で登録したコールバック関数に渡ってくる。
今回の場合だと、host1 をチェックした結果、host2 をチェックした結果がそれぞれコールバックされることになる。


コールバック関数に渡される nebstruct_service_check_data 構造体には様々なデータが入っている。
nebstruct_service_check_data の宣言を見ればすぐに分かると思うかもしれない。

/* service check structure */
typedef struct nebstruct_service_check_struct {
    int             type;
    int             flags;
    int             attr;
    struct timeval  timestamp;

    char            *host_name;
    char            *service_description;
    int             check_type;
    int             current_attempt;
    int             max_attempts;
    int             state_type;
    int             state;
    int             timeout;
    char            *command_name;
    char            *command_args;
    char            *command_line;
    struct timeval  start_time;
    struct timeval  end_time;
    int             early_timeout;
    double          execution_time;
    double          latency;
    int             return_code;
    char            *output;
    char            *long_output;
    char            *perf_data;

    void            *object_ptr;
} nebstruct_service_check_data;

結構いろいろ取れるけれども、これらはほとんどがチェックの結果であって、例えばチェックを行ったホストがどのホストグループに所属しているか、といった設定情報は簡単に取得することができない。どのようにしてそれらを取得すればいいのだろうか。

実は object_ptr は service 構造体になっているので、ここから取得すればいい。

static int cbfunc(int type, void* data) {
    auto pd = static_cast<const nebstruct_service_check_data*>(data);
    auto sv = static_cast<const service*>(pd->object_ptr);
}

この service 構造体は、先ほど define service で書いた定義と対応している。一部だけ抜粋するとこんな感じ。

/* SERVICE structure */
struct service_struct {
    char    *host_name;
    char    *description;
    ...
    customvariablesmember *custom_variables;
    ...
    host *host_ptr;
    ...
    objectlist *servicegroups_ptr;
    ...
};

host_name, description は設定ファイルの定義と同じである。custom_variables はカスタムデータを取り出す部分で説明する。
host_ptr は define host に対応した host 構造体へのポインタになっている。設定ファイルで service を定義した時は host1,host2 という複数のホストを指定したが、実際のところは service と host は 1 対 1 で対応している。そのため、今回であれば、host1 か host2 のどちらかの host の定義が入っていることになる。
servicegroups_ptr には、そのサービスが所属している define servicegroup の一覧が単方向リンクリストで入っている。今回の場合だと servicegroup は定義していないので何も入っていない。

host 構造体を見てみよう。

/* HOST structure */
struct host_struct {
    char    *name;
    ...
    objectlist *hostgroups_ptr;
    ...
};

hostgroups_ptr には、そのホストが所属している define hostgroup の一覧が単方向リンクリストで入っている。今回の場合だと group という名前の hostgroup が取得できることになる。

ということで、「チェックを行ったホストがどのホストグループに所属しているか」の一覧を出力する場合は以下のようにすればいい。

static int cbfunc(int type, void* data) {
    auto pd = static_cast<const nebstruct_service_check_data*>(data);
    auto sv = static_cast<const service*>(pd->object_ptr);
    auto ho = sv->host_ptr;
    for (objectlist* p = ho->hostgroups_ptr; p; p = p->next) {
        auto hg = static_cast<const hostgroup*>(p->object_ptr);
        std::cout << hg->group_name << std::endl;
    }
}


ちなみにサービスのチェック結果がどうなったかというのは nebstruct_service_check_data 構造体の return_code を見ればいい。
プラグインに寄ると思うけれども、デフォルトでは)OK, WARNING, CRITICAL, UNKNOWN がそれぞれ 0〜3 に割り当てられている。

カスタムデータの取り出し方

Nagios の設定ファイルでは、

define service {
    ...
    _priority    10
}

とかやって自前のプロパティを定義することができる。これは設定ファイル上であれば普通に $_SERVICEPRIORITY$ とかして取得することができる。 Custom Object Variables あたりを参照すること。
NEB では grab_custom_object_macro を使うことで取得することができる。

boost::optional<std::string> get_custom_object(const std::string& key, customvariablesmember* cv) {
    char* value = 0;
    if (grab_custom_object_macro(const_cast<char*>(key.c_str()), cv, &value) != OK) {
        return boost::none;
    }
    std::shared_ptr<void> p(value, &std::free);

    std::string str(value);
    return str;
}
const service* sv = ...;
auto priority = get_custom_object("PRIORITY", sv->custom_variables);

全て大文字になっていることに注意。上記のドキュメントにあるように、自前のプロパティは全て大文字に変換される。

スレッドを使う

Nagios は(-d を指定して起動した場合)起動時にデーモン化を行う。デーモン化というのはよく分からなかったんだけど、どうやらターミナルから単純に起動するとターミナルが親プロセスになっちゃって残念なことになるから、fork() して子プロセスを作り親プロセスがそのまま死ぬことでターミナルとの接点を切る手法らしい。
完全なデーモン化をするには 2 回 fork() しないといけないみたいなんだけど、それは nagios.cfg で child_processes_fork_twice を 1 にすればいいらしい。
また、fork() する際にスレッドを作っていると大変なことになる(UNIX上でのC++ソフトウェア設計の定石 (3) - memologue)ので、作らないようにしておくべきである。


そして NEB モジュールのロードはデーモン化する前に行われる。そのため、nebmodule_init でスレッドを作ると、fork() する際にそのスレッドがコピーされない。
なので、fork() が終わった後にスレッドを作るべきである。
それがいつかというと、ソースを読んだ限りでは、 NEBCALLBACK_PROCESS_DATA イベントで、 nebstruct_process_data::type が NEBTYPE_PROCESS_EVENTLOOPSTART であるコールバックが来たときには必ずデーモン化が終わっているようなので、ここでスレッドを作るようにする。

extern "C" int nebmodule_init(int flags, char* args, nebmodule* handle) {
    neb_module mod(handle);

    // デーモン化の後に初期化する
    mod.register_process([handle](const nebstruct_process_data& sd) {
        if (sd.type != NEBTYPE_PROCESS_EVENTLOOPSTART) return;

        // ここら辺でスレッド作ったりする

        neb_module mod(handle);
        // スレッドに依存したコールバックをこの辺で登録する。
        mod.register_service_check(...);

        // 二度と呼ばれないように登録解除する
        // (自身のコールバック内部で解除するのが安全かどうかは分からないけど、とりあえずは動いてる)
        mod.deregister_process();
    });

    return OK;
}

これで NEB でも正しくスレッドが動作するようになる。