Go言語のSNMPパッケージgosnmpでsnmpwalkを作る方法

# はじめに

Go言語のSNMPパッケージgosnmpを紹介するシリーズの6回目です。

gosnmpパッケージのサンプルプログラムは、NET-SNMPのsnmpgetコマンドのような動作でした。今回はsnmpwalkコマンドの動作の作り方です。

https://qiita.com/twsnmp/items/2986b696cced09820291

の記事で紹介したOIDと名前の変換も使っています。

# snmpwalkコマンドについて

snmpwalkは、NET-SNMPのコマンドです。グループ単位でMIBを取得できます。SNMPエージェントが対応しているMIBを全て取得する場合にも利用します。

```terminal
$snmpwalk -v 2c -c public 192.168.1.210 system
SNMPv2-MIB::sysDescr.0 = STRING: Linux minipc 4.19.0-26-amd64 1 SMP Debian 4.19.304-1 (2024-01-09) x86_64
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (17244672) 1 day, 23:54:06.72
SNMPv2-MIB::sysContact.0 = STRING: twsnmp@gmail.com
SNMPv2-MIB::sysName.0 = STRING: minipc
SNMPv2-MIB::sysLocation.0 = STRING: YMI2F
SNMPv2-MIB::sysServices.0 = INTEGER: 72
SNMPv2-MIB::sysORLastChange.0 = Timeticks: (1) 0:00:00.01
```
のようにsystemグループのMIBを１つのコマンドで取得できます。

:::note info
SNMPのGetNextコマンドを繰り返してMIBツリーのsystem配下にあるMIB取得しています。
systemと指定している最初のMIBをisoとすれば、対象のSNMPエージェントが対応している全MIBを取得することができます。
:::

# snmpwalkをgosnmpを使って作る

snmpwalkをgosnmpで作ると

```go
package main

import (
	"fmt"
	"log"

	g "github.com/gosnmp/gosnmp"
	mibdb "github.com/twsnmp/go-mibdb"
)

func main() {
	m, err := mibdb.NewMIBDB("./mib.txt")
	if err != nil {
		fmt.Printf("NewMIBDB failed err=%v", err)
		return
	}
	g.Default.Target = "192.168.1.210"
	err = g.Default.Connect()
	if err != nil {
		log.Fatalf("Connect() err: %v", err)
	}
	defer g.Default.Conn.Close()
	err = g.Default.Walk(m.NameToOID("system"), func(variable g.SnmpPDU) error {
		fmt.Printf("oid: %s name:%s = ", variable.Name, m.OIDToName(variable.Name))
		switch variable.Type {
		case g.ObjectIdentifier:
			fmt.Printf("oid: %s(%s)\n", m.OIDToName(variable.Value.(string)), variable.Value)
		case g.OctetString:
			bytes := variable.Value.([]byte)
			fmt.Printf("string: %s\n", string(bytes))
		default:
			fmt.Printf("number: %d\n", g.ToBigInt(variable.Value))
		}
		return nil
	})
	if err != nil {
		log.Fatalf("Walk() err: %v", err)
	}
}

```

gosnmpには、Walkという関数があります。Walkは、GetNextで取得したMIBを１つづつCallバック関数に渡してくれます。

### Callバック関数

```go
err = g.Default.Walk(m.NameToOID("system"), func(variable g.SnmpPDU) error {
		fmt.Printf("oid: %s name:%s = ", variable.Name, m.OIDToName(variable.Name))
		switch variable.Type {
		case g.ObjectIdentifier:
			fmt.Printf("oid: %s(%s)\n", m.OIDToName(variable.Value.(string)), variable.Value)
		case g.OctetString:
			bytes := variable.Value.([]byte)
			fmt.Printf("string: %s\n", string(bytes))
		default:
			fmt.Printf("number: %d\n", g.ToBigInt(variable.Value))
		}
		return nil
	})
```
がCallバック関数です。この中で取得したMIBを表示すれば、snnmpwalkコマンドと同じような動作を実現できます。

### 実行結果

```terminal
$go run main.go
oid: .1.3.6.1.2.1.1.1.0 name:sysDescr.0 = string: Linux minipc 4.19.0-26-amd64 1 SMP Debian 4.19.304-1 (2024-01-09) x86_64
oid: .1.3.6.1.2.1.1.2.0 name:sysObjectID.0 = oid: netSnmpAgentOIDs.10(.1.3.6.1.4.1.8072.3.2.10)
oid: .1.3.6.1.2.1.1.3.0 name:sysUpTimeInstance = number: 16366034
oid: .1.3.6.1.2.1.1.4.0 name:sysContact.0 = string: twsnmp@gmail.com
oid: .1.3.6.1.2.1.1.5.0 name:sysName.0 = string: minipc
oid: .1.3.6.1.2.1.1.6.0 name:sysLocation.0 = string: YMI2F
oid: .1.3.6.1.2.1.1.7.0 name:sysServices.0 = number: 72
oid: .1.3.6.1.2.1.1.8.0 name:sysORLastChange.0 = number: 1
```

# snmptableコマンドについて

NET-SNMPにはテーブル形式のMIBを取得するsnmptableというコマンドがあります。
インターフェイスの情報ifTableをsnmpwalkで取得すると

```terminal
$snmpwalk -v 2c -c public 192.168.1.210 ifTable
IF-MIB::ifIndex.1 = INTEGER: 1
IF-MIB::ifIndex.2 = INTEGER: 2
IF-MIB::ifIndex.3 = INTEGER: 3
IF-MIB::ifIndex.4 = INTEGER: 4
IF-MIB::ifIndex.5 = INTEGER: 5
IF-MIB::ifDescr.1 = STRING: lo
IF-MIB::ifDescr.2 = STRING: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
IF-MIB::ifDescr.3 = STRING: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
IF-MIB::ifDescr.4 = STRING: Qualcomm Atheros QCA9377 802.11ac Wireless Network Adapter
IF-MIB::ifDescr.5 = STRING: docker0
IF-MIB::ifType.1 = INTEGER: softwareLoopback(24)
IF-MIB::ifType.2 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifType.3 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifType.4 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifType.5 = INTEGER: ethernetCsmacd(6)
IF-MIB::ifMtu.1 = INTEGER: 65536
IF-MIB::ifMtu.2 = INTEGER: 1500
IF-MIB::ifMtu.3 = INTEGER: 1500
IF-MIB::ifMtu.4 = INTEGER: 1500
IF-MIB::ifMtu.5 = INTEGER: 1500
IF-MIB::ifSpeed.1 = Gauge32: 10000000
IF-MIB::ifSpeed.2 = Gauge32: 1000000000

```

のように見やすくありません。snmptableで取得すると


```terminal
$snmptable -v 2c -c public 192.168.1.210 ifTable
SNMP table: IF-MIB::ifTable

 ifIndex                                                                                   ifDescr           ifType ifMtu    ifSpeed     ifPhysAddress ifAdminStatus ifOperStatus ifLastChange ifInOctets ifInUcastPkts ifInNUcastPkts ifInDiscards ifInErrors ifInUnknownProtos ifOutOctets ifOutUcastPkts ifOutNUcastPkts ifOutDiscards ifOutErrors ifOutQLen              ifSpecific
       1                                                                                        lo softwareLoopback 65536   10000000                              up           up 0:0:00:00.00  614573050       3237520              0            0          0                 0   614573050        3237520               0             0           0         0 SNMPv2-SMI::zeroDotZero
       2 Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller   ethernetCsmacd  1500 1000000000 5c:85:7e:4a:9a:58            up           up 0:0:00:00.00   30076806      64586983          75750      9840636          0                 0  2996661367      121366896               0             0           0         0 SNMPv2-SMI::zeroDotZero
       3 Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller   ethernetCsmacd  1500 1000000000 5c:85:7e:4a:9a:59            up           up 0:0:00:00.00  317343533    2127902930       22268130        78808          0                 0    93935144        1114655               0             0           0         0 SNMPv2-SMI::zeroDotZero
       4                                Qualcomm Atheros QCA9377 802.11ac Wireless Network Adapter   ethernetCsmacd  1500          0  0:e9:3a:89:8d:ff          down         down 0:0:00:00.00          0             0              0            0          0                 0           0              0               0             0           0         0 SNMPv2-SMI::zeroDotZero
       5                                                                                   docker0   ethernetCsmacd  1500          0   2:42:89:3:c4:e2            up         down 0:0:00:00.00          0             0              0            0          0                 0           0              0               0             0           0         0 SNMPv2-SMI::zeroDotZero

```

ようにテーブル形式で表示できます。

### TWSNMP FKの場合

TWSNMP FKで取得すると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/d71fe5c2-ed4f-90eb-9ec4-beaec8fd7dd3.png)

のように表示できます。


# gosnmpでsnmptableを作る

gosnmpでsnmptableを作ると

```go
package main

import (
	"fmt"
	"log"
	"strings"

	g "github.com/gosnmp/gosnmp"
	mibdb "github.com/twsnmp/go-mibdb"
)

func main() {
	m, err := mibdb.NewMIBDB("./mib.txt")
	if err != nil {
		fmt.Printf("NewMIBDB failed err=%v", err)
		return
	}
	g.Default.Target = "192.168.1.210"
	err = g.Default.Connect()
	if err != nil {
		log.Fatalf("Connect() err: %v", err)
	}
	defer g.Default.Conn.Close()
	tableMap := make(map[string]map[string]string)
	nameMap := make(map[string]bool)
	nameList := []string{}
	err = g.Default.Walk(m.NameToOID("ifTable"), func(variable g.SnmpPDU) error {
		name := m.OIDToName(variable.Name)
		a := strings.SplitN(name, ".", 2)
		if len(a) != 2 {
			log.Fatalln("no index")
		}
		name = a[0]
		index := a[1]
		val := ""
		switch variable.Type {
		case g.ObjectIdentifier:
			val = fmt.Sprintf("%s(%s)", m.OIDToName(variable.Value.(string)), variable.Value)
		case g.OctetString:
			if name == "ifPhysAddress" {
				bytes := variable.Value.([]byte)
				for i, b := range bytes {
					if i > 0 {
						val += ":"
					}
					val += fmt.Sprintf("%02X", b)
				}
			} else {
				bytes := variable.Value.([]byte)
				val = string(bytes)
			}
		default:
			val = fmt.Sprintf("%d", g.ToBigInt(variable.Value))
		}
		if _, ok := nameMap[name]; !ok {
			nameMap[name] = true
			nameList = append(nameList, name)
		}
		if _, ok := tableMap[index]; ok {
			tableMap[index][name] = val
		} else {
			tableMap[index] = make(map[string]string)
			tableMap[index][name] = val
		}
		return nil
	})
	if err != nil {
		log.Fatalf("Walk() err: %v", err)
	}
	fmt.Printf("index")
	for _, n := range nameList {
		fmt.Printf("\t%s", n)
	}
	fmt.Println("")
	for i, m := range tableMap {
		fmt.Printf("%s", i)
		for _, n := range nameList {
			if v, ok := m[n]; ok {
				fmt.Printf("\t%s", v)
			} else {
				fmt.Printf("\t")
			}
		}
		fmt.Println("")
	}
}
```

Walk関数で取得したMIBをGo言語のMAPを使って並べ替えています。

```terminal
$go run main.go
index	ifIndex	ifDescr	ifType	ifMtu	ifSpeed	ifPhysAddress	ifAdminStatus	ifOperStatus	ifLastChange	ifInOctets	ifInUcastPkts	ifInNUcastPkts	ifInDiscards	ifInErrors	ifInUnknownProtos	ifOutOctets	ifOutUcastPkts	ifOutNUcastPkts	ifOutDiscards	ifOutErrors	ifOutQLen	ifSpecific
5	5	docker0	6	1500	0	02:42:89:03:C4:E2	1	2	0	0	0	0	0	0	0	0	0	00	0	0	zeroDotZero(.0.0)
1	1	lo	24	65536	10000000		1	1	0	614573050	3237520	0	0	0	0	614573050	3237520	0	0	0	0	zeroDotZero(.0.0)
2	2	Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller	6	1500	1000000000	5C:85:7E:4A:9A:58	1	1	0	30524249	64592573	75761	9841794	0	0	3001538382	121384914	0	0	0	0	zeroDotZero(.0.0)
3	3	Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller	6	1500	1000000000	5C:85:7E:4A:9A:59	1	1	0	430705774	2128102302	22271244	78808	0	0	93945512	1114781	0	0	0	0	zeroDotZero(.0.0)
4	4	Qualcomm Atheros QCA9377 802.11ac Wireless Network Adapter	6	1500	0	00:E9:3A:89:8D:FF	2	2	0	0	00	0	0	0	0	0	0	0	0	0	zeroDotZero(.0.0)

```

ifPhysAddressは、そのまま文字列にすると文字化けするので

```go
if name == "ifPhysAddress" {
	bytes := variable.Value.([]byte)
	for i, b := range bytes {
    	if i > 0 {
			val += ":"
		}
		val += fmt.Sprintf("%02X", b)
	}
} else {
	bytes := variable.Value.([]byte)
	val = string(bytes)
}
```

のようにMACアドレスのように表示する工夫をしています。