見出し画像

SNMPマネージャの開発

「実践SNMP教科書」の第9章の復刻版です。

SNMPマネージャは、SNMPエージェントから管理情報を取得して、ネットワークやネットワーク上に接続された機器を管理するためのソフトウェアです。最も有名なSNMPマネージャは、HP社のOpenViewだと思います。SNMPマネージャには、OpenViewのような大規模なものから、特定の機器(ルータ、UPSなど)をSNMPで管理するものまで様々な種類が存在します。この章では、SNMPマネージャの基本的な実装について、筆者が開発したTWSNMPマネージャというフリーウェアを元に説明します。

「実践SNMP教科書 原稿」

2024年の復刻時点では、Zabbixなどのオープンソースソフトなどがあります。TWSNMPもFCやシン・TWSNMPなど復刻版があります。

SNMPマネージャの構成

SNMPマネージャの基本的な構成を次の図に示します。

「実践SNMP教科書 原稿」
SNMPマネージャの基本的な構成

①TCP/IP
SNMPマネージャの通信手段です。
②SNMPライブラリ
SNMPのBER処理、MIBの管理、プロトコル処理を行います。通常は、SNMP開発キットを利用します。TWSNMPマネージャでは、NET-SNMPを使用しています。

③管理アプリケーション
管理者に対するGUIを提供する部分で、主なものを以下に示します。

自動発見:管理対象を検索し、MAPを自動作成する機能
管理MAP:監視対象の状態を図示する機能
TRAP受信:TRAPを受信する機能
ログ機能:監視イベント、TRAP受信を記録する機能
パネル表示:実機のイメージなどをグラフィカルに表示する機能
グラフ機能:トラフィックなどのグラフを表示する機能
MIBブラウザー:MIB情報のアクセスを行う機能
MIBに特化したアプリケーション:特定のMIB情報を表現するためのアプリケーション

管理アプリーションは、SNMPだけではなく、PINGやその他のプロトコル(TCPによる各種サーバへの接続など)を併用する場合が多いです。この理由は、SNMPの対応していない機器も管理対象とすることが多いかからです。

④MIBDB
SNMP MIBのデータベース、主に、名前とOIDの変換を行うために必要です。TWSNMPマネージャ(NET-SNMPライブラリを使用)では、ASN.1で記述したMIBモジュールファイルを使用します。

⑤MIBコンパイラ
ASN.1形式のMIB定義から、内部フォーマットのMIB情報に変換する機能です。NET-SNMPのライブラリをしようすれば、ASN.1形式のMIBファイルをそのまま読み込めるので、外部にMIBコンパイラは存在しません。ある意味、SNMPライブラリがMIBコンパラーを内蔵しているとも言えます。

⑥管理DB
管理対象のIPアドレス、Community、タイムアウトなどの情報を保持するDBです。簡易なSNMPマネージャ(TWSNMPマネージャ)では、テキストファイルで実現しています。高機能なものは、リレーショナルデータベースを利用しています。

「実践SNMP教科書 原稿」

SNMPアクセス方式

SNMPアクセス方式とは、SNMPマネージャからSNMPエージェントへリクエストを送信して、応答を受信する場合の方式です。SNMPアクセス方式には、同期アクセス方式と非同期アクセス方式があります。

「実践SNMP教科書 原稿」

同期アクセス方式

同期アクセス方式は、SNMPマネージャからリクエストを送信後、SNMPエージェントから応答が返るかタイムアウトになるまで待つ方式です。次の図に同期アクセス方式を示します。

「実践SNMP教科書 原稿」
同期アクセス方式

同期アクセス方式では、応答が返るまで他のリクエストを送信することができません。このため、複数のエージェントから複数のMIB情報を定期的に取得する必要のあるSNMPマネージャでは、有効ではありません。動作していないエージェントがあると、タイムアウトの発生回数分、他のエージェントへのリクエスト送信が遅くなるためです。つまり、ポーリング間隔が、停止ノードの数に影響して変化してしまうということです。同期アクセス方式は、プログラム上は、非常に簡単に実装できるので、snmpgetなどのコマンドツールでは利用されています。NET-SNMP APIを使用した同期アクセス方式の実装例を次のソースコードに示します。

「実践SNMP教科書 原稿」
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <string.h>

/* SNMPv3を使用しない場合は、この定義を削除 */
#define DEMO_USE_SNMP_VERSION_3

#ifdef DEMO_USE_SNMP_VERSION_3
const char *our_v3_passphrase = "The UCD Demo Password";
#endif

int main(int argc, char ** argv)
{
    struct snmp_session session, *ss;
    struct snmp_pdu *pdu;
    struct snmp_pdu *response;

    oid anOID[MAX_OID_LEN];
    size_t anOID_len = MAX_OID_LEN;

    struct variable_list *vars;
    int status;
    int count=1;

    /*
     * SNMPライブラリの初期化
     */
    init_snmp("snmpapp");

    /*
     * セッションの初期化
     */
    snmp_sess_init( &session );                   /* デフォルトに設定 */
    session.peername = strdup("test.net-snmp.org");

    /* 認証パラメータの設定 */

#ifdef DEMO_USE_SNMP_VERSION_3

    /* SNMPv3使用の場合 */

    /* SNMPバージョン番号の設定 */
    session.version=SNMP_VERSION_3;
        
    /* SNMPv3ユーザ名の設定 */
    session.securityName = strdup("MD5User");
    session.securityNameLen = strlen(session.securityName);

    /* セキュリティレベルの設定 */
    session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;

    /* 認証プロトコルをMD5に設定 */
    session.securityAuthProto = usmHMACMD5AuthProtocol;
    session.securityAuthProtoLen = sizeof(usmHMACMD5AuthProtocol)/sizeof(oid);
    session.securityAuthKeyLen = USM_AUTH_KU_LEN;

    /* 認証パスワードを設定 */
    if (generate_Ku(session.securityAuthProto,
                    session.securityAuthProtoLen,
                    (u_char *) our_v3_passphrase, strlen(our_v3_passphrase),
                    session.securityAuthKey,
                    &session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
        snmp_perror(argv[0]);
        snmp_log(LOG_ERR,
                 "Error generating Ku from authentication pass phrase. \n");
        exit(1);
    }
    
#else /*  SNMPv1の場合 */

    /* SNMPバージョン番号を設定 */
    session.version = SNMP_VERSION_1;

    /* set the SNMPv1 community name used for authentication */
    session.community = "demopublic";
    session.community_len = strlen(session.community);

#endif /* SNMPv1 */

    /*
     * セッションをオープン
     */
    SOCK_STARTUP; //これは、Windowsの場合、WinSockの初期化を行う。
    ss = snmp_open(&session);                     /* セッション */

    if (!ss) {
        snmp_perror("ack");
        snmp_log(LOG_ERR, "セッションオープンエラー!\n");
        exit(2);
    }
    
    /*
     *  PDUの作成
     *   1) system.sysDescr.0をGET
     */
    pdu = snmp_pdu_create(SNMP_MSG_GET);
    read_objid(".1.3.6.1.2.1.1.1.0", anOID, &anOID_len);

#if OTHER_METHODS
// オブジェクト名の変換は、他に以下の方法がある。
    get_node("sysDescr.0", anOID, &anOID_len);
    read_objid("system.sysDescr.0", anOID, &anOID_len);
#endif

    snmp_add_null_var(pdu, anOID, anOID_len);
  
    /*
     * リクエストの送信と応答待ち
     */
    status = snmp_synch_response(ss, pdu, &response);
    /*
     * 応答の処理
     */
    if (status == STAT_SUCCESS && response->errstat == SNMP_ERR_NOERROR) {
      /*
       * 成功: 応答を表示
       */
      for(vars = response->variables; vars; vars = vars->next_variable)
        print_variable(vars->name, vars->name_length, vars);

      /* 情報の表示処理 */
      for(vars = response->variables; vars; vars = vars->next_variable) {
        if (vars->type == ASN_OCTET_STR) {
            char *sp = (char *)malloc(1 + vars->val_len);
            memcpy(sp, vars->val.string, vars->val_len);
             sp[vars->val_len] = '\0';
          printf("値 #%d は文字列: %s\n", count++, sp);
            free(sp);
          } else
          printf("値はe #%d 文字列ではない! \n", count++);
       }
     } else {
      /*
       * 失敗: エラー表示
       */

      if (status == STAT_SUCCESS)
        fprintf(stderr, "エラーコード: %s\n",
                snmp_errstring(response->errstat));
      else
        snmp_sess_perror("snmpget", ss);
    }
    /*
     * 終了処理:
     *  1) 応答の開放.
     *  2) セッションのクローズ.
     */
    if (response)
      snmp_free_pdu(response);
    snmp_close(ss);

    SOCK_CLEANUP;
    return (0);
} /* main() */

非同期アクセス方式

非同期アクセス方式は、SNMPマネージャからのリクエスト送信と、SNMPエージェントからの応答受信を非同期で(独立して)実行可能な方式です。次の図に非同期アクセス方式を示します。

「実践SNMP教科書 原稿」
非同期アクセス方式

SNMPマネージャの処理は、応答が返るまで待つ必要がなく、複数のリクエストを送信することができます。SNMPマネージャでは、非同期アクセス方式を使用しているのが一般的です。非同期アクセス方式を実現するためには、以下のようなプログラミング上の方法があります。
①マルチプロセス
同期アクセス方式によるSNMPアクセスコマンドを別プロセスとして複数起動して、終了したプロセスから応答を取得する方法です。別プロセスの起動には時間がかかるため効率も悪く、メモリ使用量、通信ソケットの使用量も増えるため、管理対象の数が多い場合には、お勧めできません。
②マルチスレッド
同期アクセス方式によるSNMPアクセス処理を別スレッドとして複数起動して、終了したスレッドから応答を取得する方法です。同一プロセスの中で動作するため効率も悪くなりませんが、スレッド間で共用するメモリなどの排他制御が必要になり実装が難しくなります。
③コールバック
リクエスト実行時に、応答受信及び、タイムアウトイベントのためのコールバック関数を登録します。定期的にイベントチェックルーチンをコールすれば、イベント発生時に登録したコールバック関数が呼ばれる仕組みです。同一プロセス、スレッドで動作するため安定性がありますが、イベントチェックルーチンの呼び出し間隔に応答受信時間が左右されます。マルチスレッドと併用することで、この問題に対応することもできます。NET-SNMPのAPIでは、コールバック方式に対応しています。
同期/非同期の比較プログラムは、
http://www.net-snmp.org/tutorial-5/toolkit/asyncapp/index.html
にあります。

「実践SNMP教科書 原稿」

TWSNMPのSNMPアクセスクラスライブラリ

TWSNMPマネージャのSNMPアクセスは、NET-SNMPのAPIを直接使わずに、独自に開発したC++のSNMPアクセスクラスライブラリを通して行います。

SNMPアクセスクラスは、

①基本的なSNMPアクセス(get、getnext、trap受信、walk)を提供する。
②非同期アクセス方式をサポートする。
(安定性を高めるためのコールバック方式で実現しています。)
③SNMPライブラリの初期化、MIB DBの読込を可能にする。
④読み込んだMIB DBの検索を可能にする。
⑤MIBのオブジェクト名、値は、文字列で扱えるようにする。
(これは、文字列ならば、すべてのデータタイプを表現できるためです。)

のような方針で設計しています。SNMPアクセスクラスの定義とメソッドの実装例を次のソースコードに示します。

「実践SNMP教科書 原稿」
class CSnmpApi : public CObject  
{
public:
// タイマーコールバックルーチン
    static void CALLBACK TimerCallBack(HWND hWnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime);
// ユーティリティ関数、
    static CString GetVBStr(char *p,BOOL bOid);
    static CString GetSubTree(struct tree *tree);
    static CString GetTrapDescr(int wTrap);
    static CString ConvShortNameVarBind(CString szIn);
    static CString  GetTypeName(int wType );
    static int GetIndexList(CString& szIn,CString szMib,CStringList& slIndex);
    static CString GetIPAddr(CString &szIPList, CString szIndex);
    static double GetDoubleMibVal(CString & szIn,CString szMib);
    static double GetDoubleMibVal(CString & szVal);
    static CString GetShortName( CString &sIn);
    static CString GetOid(char *pLabel);
    static int GetIntMibVal(CString &sIn,CString  szMib);
    static int GetIntMibVal(CString & szVal);
    static CString GetMibVal( CString &sIn,CString szMib);

// SNMPライブラリの初期化/開放
    static void CloseSnmpApi(CString szType);
    static void InitSnmpApi(CString szType,CString szConfDir,CString szMibDir,CString szLogFile ="");

// TRAP送受信
    void StopTrapRcv(void);
    BOOL SendTrap(CString szIP,CString szCom,CString szEID,CString szVarBindList,int wGen,int wSpe);
    CString GetTrap(void);
    BOOL StartTrapRcv(int wPort=162);
    CStringList m_TrapList;
    UINT    m_wTrapPort;
    static int CSnmpApi::TrapRcv(int op,struct snmp_session *session,int reqid,struct snmp_pdu *pdu,void *magic);

// SNMPアクセスメッソド
    CString GetRetStr(void);
    int m_wReqType;
    void SetVarList(CString& szVarList,CString szSep);
    void ClearReq(void);
// リクエストコールバックルーチン
    static int  CSnmpApi::ReqCallback(int op,struct snmp_session *session,int reqid,struct snmp_pdu *pdu,void *magic);
    BOOL Walk(int nMode,CString szIP,CString szComOrUser,CString szPasswd,CString szVarList,int wTimeOut = 50,int wRetry =2);
    BOOL SetReq(int nMode,CString szIP,CString szComOrUser,CString szPasswd,CString szVarList,int wTimeOut = 50,int wRetry =2);
    BOOL GetReq(int nMode,CString szIP,CString szComOrUser,CString szPasswd,CString szVarList,int wTimeOut = 50,int wRetry =2);
    BOOL GetNextReq(int nMode,CString szIP,CString szComOrUser,CString szPasswd,CString szVarList,int wTimeOut = 50,int wRetry =2);
    BOOL SendSetReq(void);
    BOOL SendReq(void);

// セッション変数とパラメータ
    struct snmp_session *m_pSnmpSession; 
    unsigned int m_wTimeOut;
    unsigned int m_wRetry;
    CString   m_szIP;
    unsigned int m_wTime;
    int        m_wStatus;
    int        m_wRetCode;
    oid m_RootOID[MAX_OID_LEN];
    size_t m_nRootOIDLen;

    CStringList m_slVarList;
    CStringList  m_slRet;
    BOOL    m_bMIBOver;
    CSnmpApi();
    virtual ~CSnmpApi();

    int m_nSnmpMode;

    CString m_szPasswd;
    CString m_szComOrUser;
};
①初期化
void CSnmpApi::InitSnmpApi(CString szType, CString szConfDir, CString szMibDir, CString szLogFile)
{
    CString s;
//SNMPライブラリに必要な環境変数の登録
    s.Format("MIBDIRS=%s",szMibDir);
    _putenv(s);
    s.Format("SNMPCONFPATH=%s",szConfDir);
    _putenv(s);
    s.Format("SNMP_PERSISTENT_FILE=%s\\%s.tmp",szConfDir,szType);
    _putenv(s);
    char  szType2[256];
    strcpy(szType2,szType);
//SNMPライブラリの初期化
  init_snmp(szType2);
//受信確認のためのタイマーコールバックルーチンの登録
    nIDTimer = ::SetTimer(NULL,123,5,TimerCallBack);

}



②リクエスト送信
BOOL CSnmpApi::SendReq()
{
    if( m_slVarList.IsEmpty() ) {
        m_wRetCode = SNMP_INTERROR;
        m_wStatus = SNMP_DONE;
        return FALSE;
    }
    m_wStatus = SNMP_DOING;
    struct snmp_session session;
    struct snmp_pdu *pdu;
    oid name[MAX_OID_LEN];
    size_t name_length;
    int status;
    char szIP[64];
    char szComOrUser[256];
    char szPasswd[256];

    m_slRet.RemoveAll();

//セッションの初期化
    snmp_sess_init(&session);
    session.local_port = SNMP_DEFAULT_REMPORT;
    session.callback = ReqCallback; 
    session.callback_magic = this; 
    session.authenticator = NULL;
    session.retries = m_wRetry;    /* リトライ. */
    session.timeout = m_wTimeOut*1000;    /* タイムアウト(uSec単位) */
    strcpy(szIP,m_szIP);
    session.peername = szIP;    //宛先

// SNMPのバージョンによって、Community又は、ユーザ名、パスワードを設定
    strcpy(szComOrUser,m_szComOrUser);
    strcpy(szPasswd,m_szPasswd);
    switch (m_nSnmpMode ) {
    case SNMP_MODE_V1:
        session.version = SNMP_VERSION_1;
        session.community = (unsigned char*)szComOrUser;
        session.community_len = strlen(szComOrUser);
        break;
    case SNMP_MODE_V2C:
        session.version = SNMP_VERSION_2c;
        session.community = (unsigned char*)szComOrUser;
        session.community_len = strlen(szComOrUser);
        break;
    case SNMP_MODE_V3_ANP:
        session.version = SNMP_VERSION_3;
        session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;
        session.securityAuthProto = usmHMACMD5AuthProtocol;
        session.securityAuthProtoLen = USM_AUTH_PROTO_MD5_LEN;
        session.securityName = szComOrUser;
        session.securityNameLen = strlen(szComOrUser);
        session.securityAuthKeyLen = USM_AUTH_KU_LEN;
        if (generate_Ku(session.securityAuthProto,
                        session.securityAuthProtoLen,
                        (u_char *) szPasswd, strlen(szPasswd),
                        session.securityAuthKey,
                        &session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
            m_wRetCode = SNMP_INTERROR;
            m_wStatus = SNMP_DONE;
            return FALSE;
        }
        break;
    default:
        m_wRetCode = SNMP_INTERROR;
        m_wStatus = SNMP_DONE;
        return FALSE;
    }

    /* 
     * SNMPセッションのオープン
     */
    m_pSnmpSession = snmp_open(&session);
    if (m_pSnmpSession == NULL){
            m_wRetCode = SNMP_INTERROR;
            m_wStatus = SNMP_DONE;
            return FALSE;
    }
    /* 
     * PDUの作成
     */
    pdu = snmp_pdu_create(m_wReqType == SNMP_GET ? SNMP_MSG_GET: SNMP_MSG_GETNEXT);
    while( !m_slVarList.IsEmpty() ) {
        CString s = m_slVarList.RemoveHead();
        char szName[1024];
        strcpy(szName,s);
          name_length = MAX_OID_LEN;
          if (!snmp_parse_oid(szName, name, &name_length)) {
            m_wRetCode = SNMP_INTERROR;
            m_wStatus = SNMP_DONE;
            snmp_free_pdu(pdu);
            snmp_close(m_pSnmpSession);
            m_pSnmpSession = NULL;
            return (FALSE);
          } else {
            snmp_add_null_var(pdu, name, name_length);
          }
    }

    /* 
     * リクエスト送信
     *
     */
    status = snmp_async_send(m_pSnmpSession,pdu,ReqCallback,this);
    if(status == 0 &&  pdu ) snmp_free_pdu(pdu);
    return(status > 1);
}

③コールバックルーチン/応答受信または、タイムアウト時に呼ばれる。
 応答からの動作モードに応じた管理情報の切り出しなどを行う。
int CSnmpApi::ReqCallback(int op,struct snmp_session *session,int reqid,struct snmp_pdu *pdu,void *magic)
{
    CSnmpApi* p = (CSnmpApi*)magic;
    if( p == 0 ) return(1);
    struct variable_list *vars;
    char szTmp[2048];
//opには、コールバックの理由が設定されている。
    switch (op ) {
// SNMPメッセージ受信
        case  NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE:
            {
// エラーなし
                if (pdu->errstat == SNMP_ERR_NOERROR){
//WALKモードの場合、終了を判定し、必要ならば、次のリクエストを送信する。
                    if( p->m_wReqType == SNMP_WALK ) {
                        vars = pdu->variables;
                        if( vars &&
                            ( vars->type != SNMP_ENDOFMIBVIEW) &&
                        (vars->type != SNMP_NOSUCHOBJECT) &&
                        (vars->type != SNMP_NOSUCHINSTANCE)) {
                            snprint_variable(szTmp,sizeof(szTmp),vars->name, vars->name_length, vars);
                            if( netsnmp_oid_is_subtree(p->m_RootOID,p->m_nRootOIDLen,vars->name,vars->name_length) ==0  ) {
                                p->m_slRet.AddTail(p->GetVBStr(szTmp,vars->type == ASN_OBJECT_ID));
                                struct snmp_pdu *npdu = snmp_pdu_create(SNMP_MSG_GETNEXT);
                                if( npdu ) {
                                    snmp_add_null_var(npdu, vars->name, vars->name_length);
                                    if( snmp_async_send(session,npdu,ReqCallback,p) > 1) {
                                        return(1);
                                    }
                                }
                            }
                        }
                        if( !p->m_slRet.IsEmpty() ) {
                            p->m_wRetCode = SNMP_NOERROR;
                        } else {
                            p->m_wRetCode = SNMP_NOSUCHNAME;
                        }
                        p->m_wStatus = SNMP_DONE;
                    } else {
// WALKモード以外は、取得した値を保存して終了
                        for(vars = pdu->variables; vars; vars = vars->next_variable){
                            snprint_variable(szTmp,sizeof(szTmp),vars->name, vars->name_length, vars);
                            p->m_slRet.AddTail(p->GetVBStr(szTmp,vars->type == ASN_OBJECT_ID));
                        }
                        p->m_wRetCode = SNMP_NOERROR;
                        p->m_wStatus = SNMP_DONE;
                    }
                } else {
                    if (pdu->errstat < 5){
                        p->m_wRetCode = pdu->errstat;
                        p->m_wStatus = SNMP_DONE;
                    } else {
                        p->m_wRetCode = SNMP_GENERROR;
                        p->m_wStatus = SNMP_DONE;
                    }
                }  /* endif -- SNMP_ERR_NOERROR */
            }
            break;
        case NETSNMP_CALLBACK_OP_TIMED_OUT:
            p->m_wRetCode = SNMP_TIMEOUT;
            p->m_wStatus = SNMP_DONE;
            break;
        case NETSNMP_CALLBACK_OP_SEND_FAILED:
            p->m_wRetCode = SNMP_TIMEOUT;
            p->m_wStatus = SNMP_DONE;
            break;
        default:
            p->m_wRetCode = SNMP_INTERROR;
            p->m_wStatus = SNMP_DONE;
            break;
    }
    return 1;
}

SNMPアクセスクラスの使用は、ライブラリの初期化を行った後、
CSnmpApi snmpapi;
という変数を定義し、
snmpapi.GetReq()
などのメッソドをコールすれば、リクエストを送信します。
snmpapi.m_wStatusが、SNMP_DONE(完了)になった場合、エラーコードをチェックして、エラーがなければ、snmpapi.m_slRetに取得したMIB値が文字列のリストで格納されています。

「実践SNMP教科書 原稿」

コラム:TWSNMPにおけるSNMPアクセスクラスライブラリの変遷
TWSNMPにおけるSNMPアクセスクラスライブラリは、最初に同期アクセスのNET-SNMP APIを使用して開発しました。しかし、マネージャを長時間アイコン化して放置しておくと、マネージャ自体が、無応答になるという現象が発生しました。頻繁ではなかったのですが、マネージャソフトとしては、最悪の問題なので、懸命に原因究明を行いましたが解決できませんでした。そこで、マルチスレッドをあきらめ、コールバック方式の非同期アクセスに変更しました。結果、無応答になる問題は、発生しなくなりました。NET-SNMPのAPIは、いちおうスレッドセーフになっていると思いますが、安全ではない可能性があります。最新バージョンのNET-SNMPライブラリでは改善されているかもしれませんが、未確認です。もし、マルチスレッドの実装を行う場合には、注意が必要です。

「実践SNMP教科書 原稿」

ここまでは、古いTWSNMPやNET-SNMPの話です。2024年時点で開発しているTWSNMP FC/FKでは、GO言語のSNMPパッケージ

を使っています。

MIBアクセス方式

SNMPマネージャがエージェントからMIBを取得する場合、管理アプリケーションの目的によって、違った取得方法を使用します。

ポーリング

機器監視のためのポーリングでは、予め取得したいMIBのインスタンスが分かっているので、通常は、Getリクエストでアクセスします。

検索アクセス

ポーリングするMIBのインスタンスを登録する時に、ネットワークI/Fなどのようにノードによってインスタンスの数が変わる場合、インスタンスを検索する必要があります。インスタンスの検索は、GetNextを取得したインスタンスを元に繰り返し実施するsnmpwalkのような方法で行います。リスト9-5にネットワークI/Fの検索例を示します。リストの例では、対象ノードがソフトウェアループバックと、イーサネットの2つのI/Fを持っていることが分かります。この情報を元に、インデックス2番のイーサネットのI/Fに関するポーリングを登録すればよいと判断できます。

ネットワークI/Fの検索例

# snmpwalk -v 2c -c public localhost ifType
RFC1213-MIB::ifType.1 = INTEGER: softwareLoopback(24)
RFC1213-MIB::ifType.2 = INTEGER: ethernet-csmacd(6)

テーブルアクセス

テーブル形式のMIBオブジェクトは、個々のオブジェクトを別々に取得するよりも表(テーブル)の状態で取得し表示した方が理解しやすくなります。例えば、ifTableの場合、

「実践SNMP教科書 原稿」
ifTableの取得例

ような表現よりは、

「実践SNMP教科書 原稿」
ifTableの取得例(テーブル形式)

のような表現のほうが情報が整理されていて分かりやすいと思います。しかし、SNMPでは、第3章で説明したように、テーブル全体やテーブルの行単位で一括して取得することはできません。このため、SNMPマネージャでは、テーブル内の個々のインスタンスをすべて取得後に、テーブルの形式に組み立て直す必要があります。

「実践SNMP教科書 原稿」

テーブル内の全インスタンスの取得方法には、以下の2つがあります。

①テーブルを構成するオブジェクト名を全て列挙する方法
テーブルを構成するオブジェクト名を全て列挙したGetNextリクエストから開始して、取得したインスタンスを次のGetNextリクエストに使用します。これを、テーブル内のオブジェクト以外が返るか、MIBの終端に達するまで繰り返すして、テーブル内の全インスタンスを取得する方法です。NET-SNMPのコマンドツールにあるsnmptableがこの方法を使用しています。この方法は、行数分の繰り返しで済むので、非常に効率がよいのですが、MIB DB内から対象のテーブル内のオブジェクト名を予め抽出する必要があるのと、エージェントの実装によって、正しく取得できない場合があるためあまりお勧めできません。(コラム参照)


②snmpwalkによる方法
テーブル名のオブジェクトを指定して、snmpwalkを実施して、テーブル内の全インスタンスを取得します。テーブル内の全インスタンス分のリクエストが必要なので非効率ですが、マネージャ側の処理も簡単で、エージェントの実装による問題も起こりにくいため比較的確実に取得できます。

どちらの方法でも、GetNextではなく、SNMPv2のGetBulkを使用すればアクセス効率は上がります。①又は②の方法で取得したテーブル内の全インスタンスは次の図に示すようにオブジェクト名から列をインデックスから行を特定してテーブル形式に変換して表示します。

「実践SNMP教科書 原稿」
ifTableのテーブル化例

コラム:エージェントの問題はマネージャが対処する?
世の中には、様々なSNMPエージェントが存在します。全てが正しくSNMPを実装しているとは限りません。例えば、本文中のテーブル形式のMIBアクセスにおいて、①の方法によってアクセスすると問題がおこるエージェントが存在します。ifTableには、22個のオブジェクトがありますが、これらを一度に取得しようとすると、tooBigエラーを返すエージェントがありました。tooBigエラーは、メッセージのSNMPメッセージのサイズが超過していることを示しのですが、一つ一つのインスタンスを取得して、全部返した場合のメッセージサイズを計算してもサイズ超過にはなりませんでした。取得するオブジェクトの個数を変えて実行してみると、問題のエージェントは、なんと10個以上のVarBindを指定したリクエストには、すべてtooBigを返すように実装されていたのです。当然SNMPのプロトコルには違反しています。しかし、エージェント側の修正はほとんど不可能なため(ファームウェアの入れ替えはほとんどの場合大変なので)、マネージャ側で対処するしかなかったのです。このように、どんなにおかしなエージェントだとしても、管理するためには、マネージャ側で対処するのが一般的です。筆者が開発した数々のマネージャにおいても、問題エージェントに対応するため、非効率な方法による実装や、場合によっては、対象のエージェントによってアクセス方法を変えるなどの対応が含まれています。

「実践SNMP教科書 原稿」

数値形式のMIBの取得方式

ポーリングやグラフ表示のために数値形式のMIBを取得する場合には、以下のような取得方法があります。
①絶対値
絶対値は、取得したMIB値をそのまま使用する方法です。MIBオブジェクトが状態値などを示している場合は、値そのものに意味があるので、この方法で取得します。例えば、ifTableのifOperStatusは、ネットワークI/Fの状態を示しているので、この値のみで、状態の判断ができます。
②差分
Counter型のMIB値などでは、1回の取得値では、あまり意味を持たない場合があります。例えば、ifTableのifInOctetsは、受信バイト数を示しますが、これは、対象ノードが起動してからの累計値なので、取得時点での通信量の状況は判断できません。このような場合、ある程度の間隔を空けて2回取得して、取得値の差分を計算することにより、取得時点の傾向をつかむことができます。
③単位時間値
差分では、取得間隔に依存した値になるため、通信量の比較などへの利用には不適切です。差分を取得間隔で割った単位時間値に変換することにより、より状況を表す値になります。ifInOctetsの単位時間値は、単位時間あたりの受信バイト数(バイト/秒)になり、トラフィックの傾向をつかみやすくなります。単位時間値を計算する場合、重要なのが時間軸の決定方法です。SNMPマネージャの時計により、応答受信時刻の差を基準に計算する方法もありますが、これだと、ネットワークの負荷や、SNMPマネージャ、エージェントの内部処理の影響を受けて正確な値になりません。一般的には、対象のMIB値と同時に、エージェントのsysUpTime.0の値を取得して、時間軸の基準とする方法を利用します。
次の図に絶対値、差分、単位時間値の関係を示します。

「実践SNMP教科書 原稿」
絶対値、差分、単位時間値の関係

差分及び、単位時間値を取得する場合に、注意が必要なことは、Conter型やTimeTicks型(sysUpTime)は、最大値に達するか再起動されると0クリアーされることです。最大値による0クリア、及び再起動が発生したことを検出して、データを破棄するか補正する処理が必要になります。

「実践SNMP教科書 原稿」

コラム:sysUpTime.0が常に0のエージェント
昔遭遇した問題エージェントに、sysUpTime.0が常に0というものがありました。このエージェントだと本文中の単位時間値の計算において、sysUpTime.0の差分が常に0になり、プログラム上は、0での割り算エラーが発生しました。理由は、エージェントに時計機能がないためとのことでしたが、最適限、sysUpTimeのカウントぐらいは実装しないと、TRAPの送信時刻などでも必要なので、エージェントとしては、役にたたないような気がします。

「実践SNMP教科書 原稿」

④計算値
更に、単独のMIBオブジェクトの値だけでは、判断できない状況があります。例えば、ifTableから受信パケットの総数を取得する場合、ifInUcastPkts、ifInNUcastPkts、ifInDiscards、 ifInErrors、ifInUnknownProtosなどから計算しなければなりません。また、ifInErrorsは、受信エラーパケット数を示しますが、単位時間に変換したエラーパケット/秒でもエラーの傾向を表す値としては、ふさわしくありません。この値を同じ期間の総受信パケット数に対する割合で表したほうがよりエラーの傾向を表せます。エラー発生率(%)になります。このように、いくつかのMIB値を組み合わせて計算する方式も必要になります。理想的には、以下のように計算式で登録できればよいと思います。(しかし、実装は複雑になります。)

「実践SNMP教科書 原稿」
ifInTotalPkts=ifInUcastPkts+ifInNUcastPkts+ifInDiscards+ifInErrors.....
ifInErrorRate = (ifInErrors/ifInTotalPkts) * 100

⑤少数
SNMPでは、前の記事で説明したように少数を転送することはできません。しかし、管理情報には少数で表したいものもあります。整数値から少数値への変換もマネージャ側の処理として必要です。簡単な実装としては、SNMPマネージャ内部では、少数値(倍精度浮動小数点など)で数値を扱い、エージェントから取得した整数値に登録した倍率を掛けて少数値に変換します。

「実践SNMP教科書 原稿」

コラム:状態の判断は、どっちで?
管理対象の何らかの状態は、取得したMIBの値からSNMPマネージャ側でできます。例えば、ifInOctetsの単位時間値を取得して、この値が、指定された閾値を越えたら過負荷状態とマネージャで判断することができます。一方、SNMPエージェントに閾値を登録すれば、エージェント側で、同じ判断ができ、負荷状態を示すMIBオブジェクトを定義すれば、エージェント側で判断した負荷状態をマネージャに伝えることができます。前者は、マネージャ側の負担が大きい反面、エージェントの負担が少なく、閾値の設定をエージェントに伝える必要がないので、一般的です。後者は、エージェント、マネージャの負担が逆になります。どちらが良いかは、難しい問題なのですが、最近は、SNMP以外にWEB画面などで、状態を表示する必要があるため、閾値や状態判断がエージェント側に実装されるケースが増えています。

「実践SNMP教科書 原稿」

管理アプリケーション

ここでは、管理アプリケーションの種類と内部処理のポイントについて解説します。

「実践SNMP教科書 原稿」

管理MAPとポーリング

一般的なSNMPマネージャでは、図に示す管理MAPによってネットワークの構成と管理対象ノードの状態を表現します。管理対象ノードの状態は、定期的に行われるポーリングによって判定し、ノードや接続線の色を変えることによって表現します。ポーリングには、SNMP以外で行うものと、SNMPによって行うものがあります。SNMP以外のポーリングとしては、PINGによる応答確認が一般的で、TCPによる接続確認でサーバ(WEBサーバ、FTPサーバなど)の動作状態を確認する機能もあります。SNMPによるポーリングには、I/Fの状態確認を行いネットワークの接続線の状態色を変えるものなどが一般的です。(SNMPによるポーリングに関しては、第10章で説明します。)ポーリングによって判定した状態は、障害のレベルによって段階分けされます。一般的な段階分けとしては、
①致命的
②重度
③軽度
④注意
⑤正常
の5段階になります。それぞれ色を変えて表示します。TWSNMPでは、①重度(赤)
②軽度(マゼンダ)
③正常(緑)
の3段階で表現しています。更に、
④確認済み障害(黄色)
管理者が障害を確認したノード
⑤復帰(青)
管理者が知らないうちに、障害が発生して復帰したノード
の状態も表示可能です。
また、ポーリングによって検出された障害は、ログに記録するとともに、設定によっては、何らかのアクション(外部プログラムの起動、メールやTRAPによる通知)を行うことができます。高機能なマネージャでは、管理MAPをサブネットワーク毎に、階層化して表現する機能もあります。

「実践SNMP教科書 原稿」
SNMPマネージャ管理MAPの例

シン・TWSNMP(TWSNMP FK)では、

のように進化しました。

自動発見

管理MAPの作成(ノードの登録)は、手動で行うことができます。しかし、SNMPマネージャは、多くのノードを管理するため手動での作成は大変です。一般的なSNMPマネージャでは、管理MAPの自動作成機能を持っています。自動作成機能は、ネットワーク上を検索してノードを発見し、ノードの情報をSNMPなどにより取得してノードを登録するものです。図9-7にTWSNMPマネージャの自動発見画面を示します。TWSNMPマネージャでは、自動発見を次のような手順で行います。
①検索範囲のIPアドレスに対して順番にPINGを実施します。
②応答のあるノードに対してSNMPのsystemグループの情報を取得します。
③SNMPの応答がないノードは、ホスト名をDNSなどから取得して、IPノードとして登録する。
④SNMPの応答があるノードは、sysName.0からノード名を決定し、sysObjectID.0からノードの種類を特定します。(ノードの種類によって、表示するアイコンや実施するポーリングを変えることができます。)
⑤SNMP対応ノードに関しては、I/Fの情報とIPアドレスの情報を取得して、ノードのI/F情報を登録します。

「実践SNMP教科書 原稿」
TWSNMPマネージャの自動発見画面

TWSNMPによる検索は、比較的簡単な方法です。高機能なSNMPマネージャでは、次のような機能もあります。
①ルータ(3層SWも含む)を検出した場合、ルートテーブルを取得して、自動的に検索範囲を広げる。
②I/Fテーブルなどの情報からノードの接続関係を自動判定し、接続線を自動作成する。

「実践SNMP教科書 原稿」

TRAP受信

TRAPを受信した場合は、ログに記録したり、設定によって何らかのアクションを行うことができます。

「実践SNMP教科書 原稿」

ログ機能

ポーリングによる障害検出やTRAPの受信などは、ログとして保存されます。ログの保存については、TWSNMPマネージャのようにテキストファイルで保存するものや、データベースに保存するものもあります。また、TWSNMPの場合ログは管理MAPで右下段のように表示されます。

「実践SNMP教科書 原稿」

グラフ機能

トラフィックなどの管理には、取得したMIB情報をグラフ化することが有効な場合があります。一般的には、単位時間値をグラフ化します。次の図にTWSNMPによるグラフ表示の例を示します。

「実践SNMP教科書 原稿」
TWSNMPによるグラフ表示の例

パネル表示

SNMPマネージャには、実機のLED表示などのそのままのイメージで表示する機能を実装している場合があります。内部的には、単にSNMPでMIB値を取得しているだけで実現可能ですが、よりビジュアルな表現を行うことにより、管理を分かり易くできます。TWSNMPのパネル表示の例を次の図に示します。

「実践SNMP教科書 原稿」
TWSNMPのパネル表示の例

シン・TWSNMPでのパネルは3Dに表示になりました。

MIBブラウザ

MIBブラウザは、管理対象のノードに対して、SNMP MIBの取得や設定を行う機能で、ほとんどのSNMPマネージャで実装されています。TWSNMPのMIBブラウザは、MIBツリー選択で取得したいMIBを選択しMIBブラウザで取得を実施します。TWSNMPマネージャのMIBブラウザでは、NET-SNMPのコマンドツールにあるsnmpget,snmpwalk,snmptableのようなアクセスが可能です。アクセス方法については、MIBツリーで選択したMIBオブジェクト名から自動的に最適な方法を選択するようになっています。SET機能については、MIBブラウザから行える場合もありますが、TWSNMPでは、別の画面で実装しています。

「実践SNMP教科書 原稿」
MIBツリーの選択
MIBブラウザ

MIBに特化した管理アプリケーション

MIBモジュールの中には、特定の装置やアプリケーションの管理に特化したものがあります。これらのMIBに対応した専用の管理アプリケーションを開発する場合もあります。図9-12にTWSNMPのRMON管理の画面の例を示します。

「実践SNMP教科書 原稿」
TWSNMPのRMON管理の画面の例

シン・TWSNMPでは、RMON、ホストリソースなどにも対応しています。

ここから先は

0字
SNMPの仕様について解説した本やサイトは、沢山あると思います。 独自の拡張MIBを自分で設計してMIBファイルやエージェントを作る方法を解説した教科書はないと思います。

20年近く前に書いた「実践SNMP教科書」を現在でも通用する部分だけ書き直して復刻するマガジンです。最近MIBの設計で困っている人に遭遇し…

開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。