見出し画像

GoSNMPにSNMP agent機能を追加する(後編)

GoSNMPにSNMP agent機能を追加した話の後編です。前編ではSNMPのリクエストを解析する部分とSNMPの応答データを作る処理について書きました。今回はリクエストに応答するために必要なMIBデータを検索する処理について書きます。30年近く前にSNNP agentを作った時の苦労を思い出しました。その頃はC言語で作っていました。

MIBはデータベース

SNMPのMIBについては、

のようなに一種のデータベースです。
MIBのツリー構造に関しては、

も参考にしてください。

SNMPマネージャから聞かれた情報をMIBから検索して応答します。マネージャからの問い合わせはMIBデータの名前(OID)で必要な情報を指定します。例えば、システムの説明を知りたければ

	sysDescr.0(.1.3.6.1.2.1.1.1.0)

を指定します。sysDescr.0は人間のための名前、括弧内の数値はコンピュータが処理するためのものです。
他に、システムが起動してからの時間ならば、

sysUpTime.0(.1.3.6.1.2.1.1.3.0)

という感じです。
実際に取得してみると

% snmpget -v 2c -c public 192.168.1.203 sysUpTime.0
SNMPv2-MIB::sysUpTime.0 = Timeticks: (92101484) 10 days, 15:50:14.84
% snmpget -v 2c -c public 192.168.1.203 sysDescr.0
SNMPv2-MIB::sysDescr.0 = STRING: Linux DiskStationYMI 2.6.32.12 #24922 Tue Apr 23 17:32:06 CST 2019 armv5tel

のようになります。

これなら簡単にMIBの検索機能を作れるように思います。C言語だと検索するための処理をかなり書く必要がありますがGO言語を初めモダンなプログラミング言語だとマップとかハッシュと呼ばれる機能を使えば楽勝です。

たぶん、

var mib = make(map[string]interface{})

initMIB() {
    mib["sysDescr.0"] = "test"
    |
}

getSysDescr(oid string) interface{}{
  return mib["sysDescr.0"]
}

ぐらいで実現できそうです。でも、そう簡単には行きません。

厄介なSNMP GetNextリクエスト

先の例ではsnmpgetコマンドを使っていました。この場合はSNMPのGetリクエストで取得しているので指定した名前のMIBデータを応答すればOKです。でも、SNMPにはSNMP GetNextリクエストもあります。これはMIBツリーの中で指定した名前の次に存在するMIBデータを応答する決まりになっています。
実際のsnmpgetnextコマンドで試してみると

% snmpgetnext -v 2c -c public 192.168.1.203 sysDescr.0
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10

のようになります。さっきと同じsysDescr.0を指定したのに、応答は、sysObjectID.0になっています。MIBツリーで見ると

画像1

の位置関係です。ちなみにsysDescr.0ではなくsysDescrを指定すると

 % snmpgetnext -v 2c -c public 192.168.1.203 sysDescr
SNMPv2-MIB::sysDescr.0 = STRING: Linux DiskStationYMI 2.6.32.12 #24922 Tue Apr 23 17:32:06 CST 2019 armv5tel

のようになります。sysDescrの次に存在するMIBデータはsysDescr.0なのです。
SNMP GetNextリクエストは、SNMPが開発された時代には画期的なアイデアでした。SNMPマネージャがエージェントから取得できるMIBについて何も知識がなくてもGetNextを繰り返せばすべてのMIBを取得できるというものです。さっきの例だと、sysDescr.0の次を指定してsysObjectID.0が取得できます。次のリクエストでsysObjectID.0の次を指定してsysUpTime.0を取得します。これを繰り返していけば、最後にnoSuchName(そのような名前はありません。)というエラーで終わります。
回線速度が遅くて1回の転送量を少なくしたい時代には良い方法でしたが、現在の環境ではSNMP以外のもっとよい方法が使われています。

話を戻すとSNMP GetNextリクエストに対応するにはGO言語のマップを使ってMIBデータの検索する方法では対応できません。名前が一致するデータしか検索できないからです。順番も意識して検索する仕組みが必要です。

GO言語のsortパッケージのSearch関数を使う

SNMP GetNextリクエストに対応するMIBの検索方法のためにGO言語で検索するパッケージを探していると標準のsortパッケージのSearch関数に行き着きました。

特に、このサンプル

func main() {
	a := []int{1, 3, 6, 10, 15, 21, 28, 36, 45, 55}
	x := 6

	i := sort.Search(len(a), func(i int) bool { return a[i] >= x })
	if i < len(a) && a[i] == x {
		fmt.Printf("found %d at index %d in %v\n", x, i, a)
	} else {
		fmt.Printf("%d not found in %v\n", x, a)
	}
}

が探していたものです。順番に検索して行って一致するデータか、値が超えた場合の直後のデータの位置を返してくれます。まさに、GetNextの動きです。この関数を使ってMIBデータの検索を作ることにしました。よい方法が見つかってかなり喜びました。

実際に作ったプログラムのポイント

データの構造は、

type mibEnt struct {
	strOid  string
	oid     []uint16
	objType Asn1BER
	getFunc func(string) interface{}
}

のように、高速に比較するため数値のOIDも保存しています。

MIBと追加する処理は、

func (a *GoSNMPAgent) AddMibList(oid string, vbType Asn1BER, get func(string) interface{}) {
	mib := &mibEnt{
		strOid:  oid,
		getFunc: get,
		objType: vbType,
		oid:     toNumOid(oid),
	}
	pos := sort.Search(len(a.mibList), func(i int) bool {
		return cmpOid(mib.oid, a.mibList[i].oid) <= 0
	})
	if pos >= len(a.mibList) {
		a.mibList = append(a.mibList, mib)
		return
	}
	if cmpOid(mib.oid, a.mibList[pos].oid) == 0 {
		a.Logger.Printf("AddMibList replace OID=%s", oid)
		a.mibList[pos] = mib
		return
	}
	a.mibList = append(a.mibList[:pos+1], a.mibList[pos:]...)
	a.mibList[pos] = mib
}

のようにしました。追加する順番を気にしなくても正しい位置に追加できるようにしています。同じOIDはリプレースするようになっています。

MIBを検索する処理は、

func (a *GoSNMPAgent) findMib(oid string, bNext bool) (string, Asn1BER, interface{}, error) {
	noid := toNumOid(oid)
	i := sort.Search(len(a.mibList), func(i int) bool {
		return cmpOid(noid, a.mibList[i].oid) <= 0
	})
	if i >= len(a.mibList) {
		return "", Integer, nil, fmt.Errorf("Not found")
	}
	if cmpOid(noid, a.mibList[i].oid) == 0 {
		if !bNext {
			return oid, a.mibList[i].objType, a.mibList[i].getFunc(oid), nil
		}
		i++
		if i >= len(a.mibList) {
			return "", Integer, nil, fmt.Errorf("Not found")
		}
	}
	oid = a.mibList[i].strOid
	return oid, a.mibList[i].objType, a.mibList[i].getFunc(oid), nil
}

のようにしました。Getの場合はOIDが一致するMIB、GetNextの場合は次に位置するMIBを返すようになっています。

全体のソースコードは、

をみてください。

30年近く前に作った時よりうまくできているように思います。
言語の進化(C言語ー>GO言語)を実感しました。


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