diff --git a/README.md b/README.md index 8d609f9..0efbf50 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ mackerel: # (オプション)Mackerel に送信する時のパラメータ name: "" # (オプション)Mackerel に登録するホスト名 x-api-key: xxxxx # (必須) Mackerel の APIキー host-id: xxxxx # (オプション) Mackerel でのホストID、無指定時の場合、プログラム内で自動的に取得し、設定ファイルを更新します。 + ignore-network-info: false # (オプション) true時、mackerel へインターフェイスに紐づくIPアドレス、MACアドレスの情報を送信しません。 ``` ## v0.0.1 からの移行 diff --git a/collector/collector.go b/collector/collector.go index 06e4dab..f54af5c 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -12,6 +12,8 @@ type snmpClientImpl interface { BulkWalk(oid string, length uint64) (map[uint64]uint64, error) BulkWalkGetInterfaceName(length uint64) (map[uint64]string, error) BulkWalkGetInterfaceState(length uint64) (map[uint64]bool, error) + BulkWalkGetInterfaceIPAddress() (map[uint64][]string, error) + BulkWalkGetInterfacePhysAddress(length uint64) (map[uint64]string, error) Close() error GetInterfaceNumber() (uint64, error) } @@ -74,3 +76,47 @@ func do(ctx context.Context, snmpClient snmpClientImpl, c *config.Config) ([]Met } return metrics, nil } + +func DoInterfaceIPAddress(ctx context.Context, c *config.Config) ([]Interface, error) { + snmpClient, err := snmp.Init(ctx, c.Target, c.Community) + if err != nil { + return nil, err + } + defer snmpClient.Close() + return doInterfaceIPAddress(ctx, snmpClient, c) +} + +func doInterfaceIPAddress(ctx context.Context, snmpClient snmpClientImpl, c *config.Config) ([]Interface, error) { + ifNumber, err := snmpClient.GetInterfaceNumber() + if err != nil { + return nil, err + } + ifDescr, err := snmpClient.BulkWalkGetInterfaceName(ifNumber) + if err != nil { + return nil, err + } + + ifIndexIP, err := snmpClient.BulkWalkGetInterfaceIPAddress() + if err != nil { + return nil, err + } + + ifPhysAddress, err := snmpClient.BulkWalkGetInterfacePhysAddress(ifNumber) + if err != nil { + return nil, err + } + + var interfaces []Interface + for ifIndex, ip := range ifIndexIP { + if name, ok := ifDescr[ifIndex]; ok { + phy := ifPhysAddress[ifIndex] + interfaces = append(interfaces, Interface{ + IfName: name, + IpAddress: ip, + MacAddress: phy, + }) + } + } + + return interfaces, nil +} diff --git a/collector/collector_test.go b/collector/collector_test.go index 34bd1a7..4aae18c 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -60,6 +60,23 @@ func (m *mockSnmpClient) GetInterfaceNumber() (uint64, error) { return 4, nil } +func (m *mockSnmpClient) BulkWalkGetInterfaceIPAddress() (map[uint64][]string, error) { + return map[uint64][]string{ + 1: {"127.0.0.1"}, + 2: {"192.0.2.1"}, + 3: {"192.0.2.2", "192.0.2.3"}, + 4: {"198.51.100.1"}, + 5: {"198.51.100.2"}, + }, nil +} +func (m *mockSnmpClient) BulkWalkGetInterfacePhysAddress(length uint64) (map[uint64]string, error) { + return map[uint64]string{ + 2: "00:00:87:12:34:56", + 3: "00:00:4C:23:45:67", + 4: "00:00:0E:34:56:78", + }, nil +} + func TestDo(t *testing.T) { ctx := context.Background() @@ -135,7 +152,7 @@ func TestDo(t *testing.T) { } }) - t.Run("non skip", func(t *testing.T) { + t.Run("skip down-linkstate", func(t *testing.T) { c := &config.Config{ MIBs: []string{"ifHCInOctets", "ifHCOutOctets"}, SkipDownLinkState: true, @@ -162,3 +179,40 @@ func TestDo(t *testing.T) { }) } + +func TestDoInterfaceIPAddress(t *testing.T) { + ctx := context.Background() + c := &config.Config{} + actual, err := doInterfaceIPAddress(ctx, &mockSnmpClient{}, c) + if err != nil { + t.Error("invalid raised error") + } + expected := []Interface{ + { + IfName: "eth0", + IpAddress: []string{"192.0.2.1"}, + MacAddress: "00:00:87:12:34:56", + }, + { + IfName: "eth1", + IpAddress: []string{"192.0.2.2", "192.0.2.3"}, + MacAddress: "00:00:4C:23:45:67", + }, + { + IfName: "eth2", + IpAddress: []string{"198.51.100.1"}, + MacAddress: "00:00:0E:34:56:78", + }, + { + IfName: "lo0", + IpAddress: []string{"127.0.0.1"}, + }, + } + if d := cmp.Diff( + actual, + expected, + cmpopts.SortSlices(func(i, j Interface) bool { return i.IfName < j.IfName }), + ); d != "" { + t.Errorf("invalid result %s", d) + } +} diff --git a/collector/types.go b/collector/types.go index 0f3bb8f..f96ce0c 100644 --- a/collector/types.go +++ b/collector/types.go @@ -12,3 +12,9 @@ type MetricsDutum struct { func (m *MetricsDutum) String() string { return fmt.Sprintf("%d\t%s\t%s\t%d", m.IfIndex, m.IfName, m.Mib, m.Value) } + +type Interface struct { + IfName string + IpAddress []string + MacAddress string +} diff --git a/config.yaml.sample b/config.yaml.sample index 37603ee..72fd433 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -11,3 +11,4 @@ mackerel: x-api-key: xxxxx host-id: xxxxx name: "" # display Name on Mackerel + ignore-network-info: false diff --git a/config/config.go b/config/config.go index da0711c..579c448 100644 --- a/config/config.go +++ b/config/config.go @@ -29,9 +29,10 @@ type Interface struct { } type Mackerel struct { - HostID string `yaml:"host-id"` - ApiKey string `yaml:"x-api-key"` - Name string `yaml:"name,omitempty"` + HostID string `yaml:"host-id"` + ApiKey string `yaml:"x-api-key"` + Name string `yaml:"name,omitempty"` + IgnoreNetworkInfo bool `yaml:"ignore-network-info,omitempty"` } type Config struct { diff --git a/mackerel/mackerel.go b/mackerel/mackerel.go index 9904349..fbe6257 100644 --- a/mackerel/mackerel.go +++ b/mackerel/mackerel.go @@ -53,14 +53,26 @@ func NewQueue(qa *QueueArg) *Queue { } // return host ID when create. -func (q *Queue) Init() (*string, error) { +func (q *Queue) Init(ifs []collector.Interface) (*string, error) { log.Println("init queue") - interfaces := []mackerel.Interface{ - { - Name: "main", - IPv4Addresses: []string{q.targetAddr}, - }, + var interfaces []mackerel.Interface + + if len(ifs) == 0 { + interfaces = []mackerel.Interface{ + { + Name: "main", + IPv4Addresses: []string{q.targetAddr}, + }, + } + } else { + for i := range ifs { + interfaces = append(interfaces, mackerel.Interface{ + Name: ifs[i].IfName, + IPv4Addresses: ifs[i].IpAddress, + MacAddress: ifs[i].MacAddress, + }) + } } var newHostID *string diff --git a/mackerel/mackerel_test.go b/mackerel/mackerel_test.go index 466de55..6248cf6 100644 --- a/mackerel/mackerel_test.go +++ b/mackerel/mackerel_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/mackerelio/mackerel-client-go" + + "github.com/yseto/switch-traffic-to-mackerel/collector" ) type mackerelClientMock struct { @@ -71,6 +73,7 @@ func TestInit(t *testing.T) { returnHostID *string queue *Queue mock *mackerelClientMock + interfaces []collector.Interface }{ { name: "create host when hostID is empty", @@ -142,12 +145,48 @@ func TestInit(t *testing.T) { }, expectedGraphDef: graphDefs, }, + { + name: "[]collector.interface is exist", + expectedCreateParam: mackerel.CreateHostParam{ + Name: "hostname", + Interfaces: []mackerel.Interface{ + { + Name: "eth0", + IPv4Addresses: []string{"192.0.2.1", "192.0.2.2"}, + }, + { + Name: "eth1", + IPv4Addresses: []string{"192.0.2.3"}, + }, + }, + }, + queue: &Queue{ + buffers: list.New(), + name: "hostname", + targetAddr: "192.0.2.1", + }, + returnHostID: &id, + mock: &mackerelClientMock{ + returnHostID: "1234567890", + }, + expectedGraphDef: graphDefs, + interfaces: []collector.Interface{ + { + IfName: "eth0", + IpAddress: []string{"192.0.2.1", "192.0.2.2"}, + }, + { + IfName: "eth1", + IpAddress: []string{"192.0.2.3"}, + }, + }, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { tc.queue.client = tc.mock - newHostID, err := tc.queue.Init() + newHostID, err := tc.queue.Init(tc.interfaces) if !errors.Is(err, tc.expectedError) { t.Error("invalid error") } diff --git a/main.go b/main.go index c6d3bee..c36cf1f 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,16 @@ func main() { return } - newHostID, err := queue.Init() + var interfaces []collector.Interface + if !c.Mackerel.IgnoreNetworkInfo { + interfaces, err = collector.DoInterfaceIPAddress(ctx, c) + if err != nil { + log.Println("HINT: try mackerel > ignore-network-info: true") + log.Fatal(err) + } + } + + newHostID, err := queue.Init(interfaces) if err != nil { log.Fatal(err) } diff --git a/snmp/snmp.go b/snmp/snmp.go index 9d2fcaa..bdc24a2 100644 --- a/snmp/snmp.go +++ b/snmp/snmp.go @@ -3,6 +3,8 @@ package snmp import ( "context" "errors" + "fmt" + "net" "strconv" "strings" @@ -10,9 +12,11 @@ import ( ) const ( - MIBifNumber = "1.3.6.1.2.1.2.1.0" - MIBifDescr = "1.3.6.1.2.1.2.2.1.2" - MIBifOperStatus = "1.3.6.1.2.1.2.2.1.8" + MIBifNumber = "1.3.6.1.2.1.2.1.0" + MIBifDescr = "1.3.6.1.2.1.2.2.1.2" + MIBifPhysAddress = "1.3.6.1.2.1.2.2.1.6" + MIBifOperStatus = "1.3.6.1.2.1.2.2.1.8" + MIBipAdEntIfIndex = "1.3.6.1.2.1.4.20.1.2" ) type SNMP struct { @@ -33,9 +37,10 @@ func (s *SNMP) Close() error { } var ( - errGetInterfaceNumber = errors.New("cant get interface number") - errParseInterfaceName = errors.New("cant parse interface name") - errParseError = errors.New("cant parse value.") + errGetInterfaceNumber = errors.New("cant get interface number") + errParseInterfaceName = errors.New("cant parse interface name") + errParseInterfacePhyAddress = errors.New("cant parse phy address") + errParseError = errors.New("cant parse value.") ) func (s *SNMP) GetInterfaceNumber() (uint64, error) { @@ -129,3 +134,61 @@ func captureIfIndex(name string) (uint64, error) { sl := strings.Split(name, ".") return strconv.ParseUint(sl[len(sl)-1], 10, 64) } + +func (s *SNMP) BulkWalkGetInterfaceIPAddress() (map[uint64][]string, error) { + kv := make(map[uint64][]string) + err := s.handler.BulkWalk(MIBipAdEntIfIndex, func(pdu gosnmp.SnmpPDU) error { + ipAddress := strings.Replace(pdu.Name, MIBipAdEntIfIndex, "", 1) + ipAddress = strings.TrimLeft(ipAddress, ".") + + ip := net.ParseIP(ipAddress) + if ip == nil { + return nil + } + if ip.IsLoopback() { + return nil + } + + switch pdu.Type { + case gosnmp.OctetString: + return errParseError + default: + ifIndex := gosnmp.ToBigInt(pdu.Value).Uint64() + kv[ifIndex] = append(kv[ifIndex], ipAddress) + } + return nil + }) + if err != nil { + return nil, err + } + return kv, nil +} + +func (s *SNMP) BulkWalkGetInterfacePhysAddress(length uint64) (map[uint64]string, error) { + kv := make(map[uint64]string, length) + err := s.handler.BulkWalk(MIBifPhysAddress, func(pdu gosnmp.SnmpPDU) error { + index, err := captureIfIndex(pdu.Name) + if err != nil { + return err + } + switch pdu.Type { + case gosnmp.OctetString: + value, ok := pdu.Value.([]byte) + if !ok { + return errParseInterfacePhyAddress + } + var parts []string + for _, i := range value { + parts = append(parts, fmt.Sprintf("%02x", i)) + } + kv[index] = strings.Join(parts, ":") + default: + return errParseInterfacePhyAddress + } + return nil + }) + if err != nil { + return nil, err + } + return kv, nil +} diff --git a/snmp/snmp_test.go b/snmp/snmp_test.go index 008ad86..e1d3b53 100644 --- a/snmp/snmp_test.go +++ b/snmp/snmp_test.go @@ -156,3 +156,77 @@ func TestBulkWalk(t *testing.T) { t.Error("invalid argument") } } + +func TestBulkWalkGetInterfaceIPAddress(t *testing.T) { + m := mockHandler{ + pdus: []gosnmp.SnmpPDU{ + { + Name: "1.3.6.1.2.1.4.20.1.2.192.0.2.1", + Value: 1, + }, + { + Name: "1.3.6.1.2.1.4.20.1.2.192.0.2.2", + Value: 1, + }, + // invalid ip + { + Name: "1.3.6.1.2.1.4.20.1.2.1024.1.2.3", + Value: 2, + }, + { + Name: "1.3.6.1.2.1.4.20.1.2.198.51.100.1", + Value: 3, + }, + { + Name: "1.3.6.1.2.1.4.20.1.2.127.0.0.1", + Value: 4, + }, + }, + } + s := &SNMP{handler: &m} + + actual, err := s.BulkWalkGetInterfaceIPAddress() + expected := map[uint64][]string{ + 1: {"192.0.2.1", "192.0.2.2"}, + 3: {"198.51.100.1"}, + } + if err != nil { + t.Error("failed raised error") + } + if d := cmp.Diff(actual, expected); d != "" { + t.Errorf("invalid result %s", d) + } +} + +func TestBulkWalkGetInterfacePhysAddress(t *testing.T) { + m := mockHandler{ + pdus: []gosnmp.SnmpPDU{ + { + Name: "1.3.6.1.2.1.2.2.1.6.1", + Value: []byte{0x00, 0x00, 0x87, 0x12, 0x34, 0x56}, + Type: gosnmp.OctetString, + }, + { + Name: "1.3.6.1.2.1.2.2.1.6.2", + Value: []byte{0x00, 0x00, 0x4C, 0x23, 0x45, 0x67}, + Type: gosnmp.OctetString, + }, + }, + } + s := &SNMP{handler: &m} + + actual, err := s.BulkWalkGetInterfacePhysAddress(2) + expected := map[uint64]string{ + 1: "00:00:87:12:34:56", + 2: "00:00:4c:23:45:67", + } + if err != nil { + t.Error("failed raised error") + } + if d := cmp.Diff(actual, expected); d != "" { + t.Errorf("invalid result %s", d) + } + if !reflect.DeepEqual(m.rootOid, MIBifPhysAddress) { + t.Error("invalid argument") + } +}