feat(go.d/snmp): add YAML overrides for sysobjectids mapping (#20828)
Ilya Mashchenko committed
Aug 17, 2025 at 17:32 UTC
188ae084f5979727efa51a741cccbb98e6f9e7d6
26 files changed
+2370
-2031
CMakeLists.txt
+4
@@ -3767,6 +3767,10 @@ if(ENABLE_PLUGIN_GO)
3767
COMPONENT plugin-go
3768
DESTINATION usr/lib/netdata/conf.d/go.d/snmp.profiles/default)
3769
3770
+ install(FILES src/go/plugin/go.d/config/go.d/snmp.profiles/meta_overrides.yaml
3771
+ COMPONENT plugin-go
3772
+ DESTINATION usr/lib/netdata/conf.d/go.d/snmp.profiles)
3773
+
3774
if(BUILD_FOR_PACKAGING)
3775
install(FILES
3776
${PKG_FILES_PATH}/copyright
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/discoverer.go
+3
-2
@@ -17,6 +17,7 @@ import (
17
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/model"
18
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/filepersister"
19
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/iprange"
20
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
21
)
22
23
const (
@@ -249,7 +250,7 @@ func (d *Discoverer) probeIPAddress(ctx context.Context, sub subnet, ip string,
250
tgg.addTarget(tg)
251
}
252
252
-func (d *Discoverer) getSnmpSysInfo(sub subnet, ip string) (*SysInfo, error) {
253
+func (d *Discoverer) getSnmpSysInfo(sub subnet, ip string) (*snmputils.SysInfo, error) {
254
client, cleanup := d.newSnmpClient()
255
defer cleanup()
256
@@ -264,7 +265,7 @@ func (d *Discoverer) getSnmpSysInfo(sub subnet, ip string) (*SysInfo, error) {
265
266
defer func() { _ = client.Close() }()
267
267
- return GetSysInfo(client)
268
+ return snmputils.GetSysInfo(client)
269
}
270
271
func send(ctx context.Context, in chan<- []model.TargetGroup, tgg model.TargetGroup) {
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/discoverer_test.go
+2
-1
@@ -11,6 +11,7 @@ import (
11
"github.com/stretchr/testify/require"
12
13
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/model"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
15
)
16
17
func TestNewDiscoverer(t *testing.T) {
@@ -269,7 +270,7 @@ func prepareNewTargetGroup(sub subnet, ips ...string) *targetGroup {
270
}
271
272
func prepareNewTarget(sub subnet, ip string) *target {
272
- return newTarget(ip, sub.credential, SysInfo{
273
+ return newTarget(ip, sub.credential, snmputils.SysInfo{
274
Descr: mockSysDescr,
275
Contact: mockSysContact,
276
Name: mockSysName,
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/sim_test.go
+7
-6
@@ -17,6 +17,7 @@ import (
17
"github.com/stretchr/testify/require"
18
19
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/model"
20
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
21
)
22
23
type discoverySim struct {
@@ -172,11 +173,11 @@ const (
173
)
174
175
func (m *mockSnmpHandler) setExpectSysInfo() {
175
- m.EXPECT().WalkAll(RootOidMibSystem).Return([]gosnmp.SnmpPDU{
176
- {Name: OidSysDescr, Value: []uint8(mockSysDescr), Type: gosnmp.OctetString},
177
- {Name: OidSysObject, Value: mockSysObject, Type: gosnmp.ObjectIdentifier},
178
- {Name: OidSysContact, Value: []uint8(mockSysContact), Type: gosnmp.OctetString},
179
- {Name: OidSysName, Value: []uint8(mockSysName), Type: gosnmp.OctetString},
180
- {Name: OidSysLocation, Value: []uint8(mockSysLocation), Type: gosnmp.OctetString},
176
+ m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return([]gosnmp.SnmpPDU{
177
+ {Name: snmputils.OidSysDescr, Value: []uint8(mockSysDescr), Type: gosnmp.OctetString},
178
+ {Name: snmputils.OidSysObject, Value: mockSysObject, Type: gosnmp.ObjectIdentifier},
179
+ {Name: snmputils.OidSysContact, Value: []uint8(mockSysContact), Type: gosnmp.OctetString},
180
+ {Name: snmputils.OidSysName, Value: []uint8(mockSysName), Type: gosnmp.OctetString},
181
+ {Name: snmputils.OidSysLocation, Value: []uint8(mockSysLocation), Type: gosnmp.OctetString},
182
}, nil).AnyTimes()
183
}
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/status.go
+4
-2
@@ -10,6 +10,8 @@ import (
10
"sync"
11
"sync/atomic"
12
"time"
13
+
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
15
)
16
17
func (d *Discoverer) loadFileStatus() {
@@ -58,8 +60,8 @@ type (
60
ConfigHash uint64 `json:"config_hash"`
61
}
62
discoveredDevice struct {
61
- DiscoverTime time.Time `json:"discover_time"`
62
- SysInfo SysInfo `json:"sysinfo"`
63
+ DiscoverTime time.Time `json:"discover_time"`
64
+ SysInfo snmputils.SysInfo `json:"sysinfo"`
65
}
66
)
67
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/sysinfo.go
deleted
-348
@@ -1,348 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package snmpsd
4
-
5
-import (
6
- "bufio"
7
- "bytes"
8
- _ "embed"
9
- "encoding/json"
10
- "fmt"
11
- "strconv"
12
- "strings"
13
-
14
- "github.com/gosnmp/gosnmp"
15
-)
16
-
17
-const (
18
- RootOidMibSystem = "1.3.6.1.2.1.1"
19
- OidSysDescr = "1.3.6.1.2.1.1.1.0"
20
- OidSysObject = "1.3.6.1.2.1.1.2.0"
21
- OidSysUptime = "1.3.6.1.2.1.1.3.0"
22
- OidSysContact = "1.3.6.1.2.1.1.4.0"
23
- OidSysName = "1.3.6.1.2.1.1.5.0"
24
- OidSysLocation = "1.3.6.1.2.1.1.6.0"
25
-)
26
-
27
-var (
28
- // https://www.iana.org/assignments/enterprise-numbers.txt
29
- //go:embed "enterprise-numbers.txt"
30
- enterpriseNumberTxt []byte
31
- // https://github.com/parthiganesh/snmp-sysObjectID
32
- //go:embed "sysObjectIDs.json"
33
- sysObjectIDsJson []byte
34
-)
35
-
36
-type SysInfo struct {
37
- Descr string `json:"description"`
38
- Contact string `json:"contact"`
39
- Name string `json:"name"`
40
- Location string `json:"location"`
41
- SysObjectID string `json:"-"`
42
-
43
- Organization string `json:"organization"`
44
- Vendor string `json:"vendor"`
45
- Category string `json:"category"`
46
- Model string `json:"model"`
47
-}
48
-
49
-func GetSysInfo(client gosnmp.Handler) (*SysInfo, error) {
50
- pdus, err := client.WalkAll(RootOidMibSystem)
51
- if err != nil {
52
- return nil, err
53
- }
54
-
55
- si := &SysInfo{
56
- Name: "unknown",
57
- Organization: "Unknown",
58
- }
59
-
60
- r := strings.NewReplacer(
61
- "'", "",
62
- "\n", " ",
63
- "\r", " ",
64
- "\x00", "",
65
- "\"", "",
66
- "`", "",
67
- "\\", "",
68
- )
69
-
70
- for _, pdu := range pdus {
71
- oid := strings.TrimPrefix(pdu.Name, ".")
72
-
73
- switch oid {
74
- case OidSysDescr:
75
- si.Descr, err = PduToString(pdu)
76
- si.Descr = r.Replace(si.Descr)
77
- case OidSysObject:
78
- var sysObj string
79
- if sysObj, err = PduToString(pdu); err == nil {
80
- si.SysObjectID = sysObj
81
- }
82
- case OidSysContact:
83
- si.Contact, err = PduToString(pdu)
84
- si.Contact = r.Replace(si.Contact)
85
- case OidSysName:
86
- si.Name, err = PduToString(pdu)
87
- si.Name = r.Replace(si.Name)
88
- case OidSysLocation:
89
- si.Location, err = PduToString(pdu)
90
- si.Location = r.Replace(si.Location)
91
- }
92
- if err != nil {
93
- return nil, fmt.Errorf("OID '%s': %v", pdu.Name, err)
94
- }
95
- }
96
-
97
- if si.SysObjectID != "" {
98
- si.Organization = lookupEnterpriseNumber(si.SysObjectID)
99
- if v, ok := entNumbersOrgToVendorMap[si.Organization]; ok {
100
- si.Vendor = v
101
- }
102
- si.Organization = r.Replace(si.Organization)
103
-
104
- if meta, ok := lookupDeviceMeta(si.SysObjectID); ok {
105
- si.Category = meta.Category
106
- si.Model = meta.Model
107
- }
108
- }
109
-
110
- return si, nil
111
-}
112
-
113
-type sysObjectIDInfo struct {
114
- Category string
115
- Model string
116
-}
117
-
118
-func lookupDeviceMeta(sysObject string) (sysObjectIDInfo, bool) {
119
- v, ok := sysObjectIDs[sysObject]
120
- return v, ok
121
-}
122
-
123
-var sysObjectIDs = func() map[string]sysObjectIDInfo {
124
- if len(sysObjectIDsJson) == 0 {
125
- panic("snmp: sysObjectIDs.json is empty")
126
- }
127
-
128
- var ids = map[string]sysObjectIDInfo{}
129
- if err := json.Unmarshal(sysObjectIDsJson, &ids); err != nil {
130
- panic(fmt.Sprintf("snmp: invalid sysObjectIDs.json: %v", err))
131
- }
132
- return ids
133
-}()
134
-
135
-func lookupEnterpriseNumber(sysObject string) string {
136
- return entNumbers[extractEntNumber(sysObject)]
137
-}
138
-
139
-var entNumbers = func() map[string]string {
140
- if len(enterpriseNumberTxt) == 0 {
141
- panic("snmp: enterprise-numbers.txt is empty")
142
- }
143
-
144
- mapping := make(map[string]string, 65000)
145
-
146
- vr := strings.NewReplacer("\"", "", "`", "", "\\", "")
147
- var id string
148
-
149
- sc := bufio.NewScanner(bytes.NewReader(enterpriseNumberTxt))
150
-
151
- for sc.Scan() {
152
- line := strings.TrimSpace(sc.Text())
153
- if line == "" {
154
- continue
155
- }
156
-
157
- if _, err := strconv.Atoi(line); err == nil {
158
- if id == "" {
159
- id = line
160
- if _, ok := mapping[id]; ok {
161
- panic("snmp: duplicate entry number: " + line)
162
- }
163
- }
164
- continue
165
- }
166
- if id != "" {
167
- line = vr.Replace(line)
168
- if line == "---none---" || line == "Reserved" {
169
- id = ""
170
- continue
171
- }
172
- mapping[id] = line
173
- id = ""
174
- }
175
- }
176
-
177
- if len(mapping) == 0 {
178
- panic("snmp: enterprise-numbers mapping is empty after reading enterprise-numbers.txt")
179
- }
180
-
181
- return mapping
182
-}()
183
-
184
-func extractEntNumber(sysObject string) string {
185
- const rootOidIanaPEN = "1.3.6.1.4.1"
186
-
187
- // .1.3.6.1.4.1.14988.1 => 14988
188
-
189
- sysObject = strings.TrimPrefix(sysObject, ".")
190
-
191
- s := strings.TrimPrefix(sysObject, rootOidIanaPEN+".")
192
-
193
- num, _, ok := strings.Cut(s, ".")
194
- if !ok {
195
- return ""
196
- }
197
-
198
- return num
199
-}
200
-
201
-func PduToString(pdu gosnmp.SnmpPDU) (string, error) {
202
- switch pdu.Type {
203
- case gosnmp.OctetString:
204
- // TODO: this isn't reliable (e.g. physAddress we need hex.EncodeToString())
205
- bs, ok := pdu.Value.([]byte)
206
- if !ok {
207
- return "", fmt.Errorf("OctetString is not a []byte but %T", pdu.Value)
208
- }
209
- return strings.ToValidUTF8(string(bs), "�"), nil
210
- case gosnmp.Counter32, gosnmp.Counter64, gosnmp.Integer, gosnmp.Gauge32:
211
- return gosnmp.ToBigInt(pdu.Value).String(), nil
212
- case gosnmp.ObjectIdentifier:
213
- v, ok := pdu.Value.(string)
214
- if !ok {
215
- return "", fmt.Errorf("ObjectIdentifier is not a string but %T", pdu.Value)
216
- }
217
- return strings.TrimPrefix(v, "."), nil
218
- default:
219
- return "", fmt.Errorf("unsupported type: '%v'", pdu.Type)
220
- }
221
-}
222
-
223
-var entNumbersOrgToVendorMap = map[string]string{
224
- "Alcatel-Lucent TMC (formerly 'Alcatel SOC')": "Alcatel-Lucent",
225
- "allied networks GmbH": "Allied",
226
- "Allied Data Technologies": "Allied",
227
- "Allied Telesis, Inc.": "Allied",
228
- "American Power Conversion Corp.": "APC",
229
- "Arista Networks, Inc. (formerly 'Arastra, Inc.')": "Arista",
230
- "Aruba PEC S.p.A.": "Aruba",
231
- "Aruba S.r.l.": "Aruba",
232
- "Aruba, a Hewlett Packard Enterprise company": "Aruba",
233
- "AVAYA": "Avaya",
234
- "Avaya Atlanta Lab": "Avaya",
235
- "Avaya Communication": "Avaya",
236
- "Barracuda Networks AG (previous was 'phion Information Technologies')": "Barracuda",
237
- "Barracuda Networks, Inc.": "Barracuda",
238
- "barracuda digitale agentur GmbH": "Barracuda",
239
- "Blade Network Technologies, Inc.": "IBM",
240
- "Brocade Communication Systems, Inc. (formerly 'Foundry Networks, Inc.')": "Brocade",
241
- "Brocade Communication Systems, Inc. (formerly 'Rhapsody Networks Inc.')": "Brocade",
242
- "Brocade Communications Systems, Inc.": "Brocade",
243
- "Brocade Communications Systems, Inc. (formerly 'McDATA Corp.')": "Brocade",
244
- "Brocade Communications Systems, Inc. (formerly 'McDATA Corporation')": "Brocade",
245
- "Brocade Communications Systems, Inc. (formerly 'McDATA,Inc')": "Brocade",
246
- "Brocade Communications Systems, Inc. (formerly 'NuView Inc.')": "Brocade",
247
- "CIENA Corporation (formerly 'ONI Systems Corp.')": "Ciena",
248
- "Ciena (formerly 'Akara Inc.')": "Ciena",
249
- "Ciena Corporation": "Ciena",
250
- "Ciena Corporation (formerly 'Catena Networks')": "Ciena",
251
- "Cisco Flex Platform": "Cisco",
252
- "Cisco Sera": "Cisco",
253
- "Cisco SolutionsLab": "Cisco",
254
- "Cisco Sytems, Inc.": "Cisco",
255
- "Cisco Systems": "Cisco",
256
- "Cisco Systems Inc": "Cisco",
257
- "Cisco Systems India Private Limited": "Cisco",
258
- "Cisco Systems, Inc.": "Cisco",
259
- "Cisco Systems, Inc. (formerly 'Arch Rock Corporation')": "Cisco",
260
- "ciscoSystems": "Cisco",
261
- "Citrix Systems Inc.": "Citrix",
262
- "Dialogic Corporation": "Dialogic",
263
- "D-Link Systems, Inc.": "D-Link",
264
- "Dell Inc.": "Dell",
265
- "Eaton Energy Automation Solutions (EAS) Division": "Eaton",
266
- "EATON Wireless": "Eaton",
267
- "Ericsson AB": "Ericsson",
268
- "Ericsson AB - 4G5G (formerly 'Ellemtel Telecommunication Systems Laboratories')": "Ericsson",
269
- "Ericsson AB - Packet Core Networks": "Ericsson",
270
- "Ericsson Ahead Communications Systems GmbH": "Ericsson",
271
- "Ericsson Communications Ltd.": "Ericsson",
272
- "Ericsson Denmark A/S, Telebit Division": "Ericsson",
273
- "Ericsson Inc. (formerly 'BelAir Networks')": "Ericsson",
274
- "Ericsson Mobile Platforms AB": "Ericsson",
275
- "Ericsson Nikola Tesla d.d.": "Ericsson",
276
- "Ericsson Research Montreal (LMC)": "Ericsson",
277
- "Ericsson Wireless LAN Systems": "Ericsson",
278
- "ERICSSON FIBER ACCESS": "Ericsson",
279
- "Extreme Networks": "Extreme",
280
- "Extreme Networks (formerly 'Ipanema Technologies')": "Extreme",
281
- "F5 Labs, Inc.": "F5",
282
- "F5 Networks Inc": "F5",
283
- "Fortinet, Inc.": "Fortinet",
284
- "Fortinet. Inc.": "Fortinet",
285
- "Gigamon Systems LLC": "Gigamon",
286
- "Hewlett-Packard": "HP",
287
- "Hewlett-Packard (Schweiz) GmbH": "HP",
288
- "Hewlett-Packard Slovakia": "HP",
289
- "Hewlett Packard Enterprise": "HPE",
290
- "HUAWEI Technology Co.,Ltd": "Huawei",
291
- "Huawei Symantec Technologies Co.,Ltd": "Huawei",
292
- "Infinera Corp.": "Infinera",
293
- "InfoBlox Inc.": "Infoblox",
294
- "Infoblox, WinConnect (formerly 'Ipanto')": "Infoblox",
295
- "Juniper Financial Corp.": "Juniper",
296
- "Juniper Networks, Inc.": "Juniper",
297
- "Juniper Networks/Funk Software": "Juniper",
298
- "Juniper Networks/Unisphere": "Juniper",
299
- "KYOCERA Corporation": "Kyocera",
300
- "Kyocera Communication Systems Co.Ltd": "Kyocera",
301
- "McAFee Associates Inc.": "McAfee",
302
- "McAfee (formerly 'Secure Computing Corporation')": "McAfee",
303
- "McAfee Inc. (formerly 'Network Associates, Inc.')": "McAfee",
304
- "McAfee, Inc. (formerly 'Securify, Inc.')": "McAfee",
305
- "McAfee Inc. (formerly 'Reconnex Corporation')": "McAfee",
306
- "McAfee, LLC": "McAfee",
307
- "Meraki Networks, Inc.": "Meraki",
308
- "Nasuni Corporation": "Nasuni",
309
- "NEC Corporation": "NEC",
310
- "NEC Eluminant Technologies, Inc.": "NEC",
311
- "NEC Platforms, Ltd.": "NEC",
312
- "NEC Telenetworx,Ltd": "NEC",
313
- "NEC COMPUTERS INTERNATIONAL B.V.": "NEC",
314
- "NEC informatec systems,ltd.": "NEC",
315
- "NEC Electronics Corporation": "NEC",
316
- "NEC Unified Solutions": "NEC",
317
- "NEC Enterprise Communication Technologies": "NEC",
318
- "Network Appliance Corporation": "NetApp",
319
- "Nokia (formerly 'Alcatel-Lucent')": "Nokia",
320
- "Nokia (formerly 'Novarra, Inc.')": "Nokia",
321
- "Nokia Distributed Access": "Nokia",
322
- "Nokia Networks (formerly 'Nokia Siemens Networks')": "Nokia",
323
- "Nokia Shanghai Bell": "Nokia",
324
- "NVIDIA Corporation": "NVIDIA",
325
- "PALO ALTO NETWORKS": "Palo Alto",
326
- "Palo Alto Research Center, Inc.": "Palo Alto",
327
- "Palo Alto Software, Inc.": "Palo Alto",
328
- "Ruckus Wireless, Inc.": "Ruckus",
329
- "SINETICA": "Panduit",
330
- "Sophos Plc": "Sophos",
331
- "SVTO Hewlett-Packard": "HP",
332
- "Synology Inc.": "Synology",
333
- "Tejas Networks": "Tejas",
334
- "TP-Link Systems Inc.": "TP-Link",
335
- "Ubiquiti Networks, Inc.": "Ubiquiti",
336
- "Velocloud Networks, Inc.": "VeloCloud",
337
- "Vertiv (formerly 'Emerson Computer Power')": "Vertiv",
338
- "Vertiv (formerly 'Emerson Energy Systems')": "Vertiv",
339
- "Vertiv Tech Co.,Ltd. (formerly 'Emerson Network Power Co.,Ltd.')": "Vertiv",
340
- "Vertiv (formerly 'Geist Manufacturing, Inc')": "Vertiv",
341
- "Vertiv Co": "Vertiv",
342
- "VMware Inc.": "VMware",
343
- "WatchGuard Technologies Inc.": "WatchGuard",
344
- "Western Digital Corporation": "Western Digital",
345
- "Yokogawa-Hewlett-Packard": "HP",
346
- "Zebra Technologies Corporation": "Zebra",
347
- "ZyXEL Communications Corp.": "Zyxel",
348
-}
src/go/plugin/go.d/agent/discovery/sd/discoverer/snmpsd/target.go
+4
-3
@@ -9,6 +9,7 @@ import (
9
"github.com/gohugoio/hashstructure"
10
11
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/model"
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
13
)
14
15
func targetSource(sub subnet) string { return fmt.Sprintf("discoverer=snmp,network=%s", subKey(sub)) }
@@ -37,7 +38,7 @@ func (g *targetGroup) addTarget(tg model.Target) {
38
g.targets = append(g.targets, tg)
39
}
40
40
-func newTarget(ip string, cred CredentialConfig, si SysInfo) *target {
41
+func newTarget(ip string, cred CredentialConfig, si snmputils.SysInfo) *target {
42
tg := &target{
43
IPAddress: ip,
44
Credential: cred,
@@ -55,8 +56,8 @@ type (
56
hash uint64
57
58
IPAddress string
58
- Credential CredentialConfig `hash:"ignore"`
59
- SysInfo SysInfo `hash:"ignore"`
59
+ Credential CredentialConfig `hash:"ignore"`
60
+ SysInfo snmputils.SysInfo `hash:"ignore"`
61
}
62
)
63
src/go/plugin/go.d/collector/snmp/collect.go
+26
-31
@@ -16,15 +16,17 @@ import (
16
"github.com/gosnmp/gosnmp"
17
18
"github.com/netdata/netdata/go/plugins/logger"
19
- "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/discoverer/snmpsd"
19
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
20
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
21
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
22
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
23
)
24
25
+const oidSysUptime = "1.3.6.1.2.1.1.3.0"
26
+
27
func (c *Collector) collect() (map[string]int64, error) {
28
if c.sysInfo == nil {
27
- si, err := snmpsd.GetSysInfo(c.snmpClient)
29
+ si, err := snmputils.GetSysInfo(c.snmpClient)
30
if err != nil {
31
return nil, err
32
}
@@ -80,7 +82,7 @@ func (c *Collector) collect() (map[string]int64, error) {
82
}
83
84
func (c *Collector) collectSysUptime(mx map[string]int64) error {
83
- resp, err := c.snmpClient.Get([]string{snmpsd.OidSysUptime})
85
+ resp, err := c.snmpClient.Get([]string{oidSysUptime})
86
if err != nil {
87
return err
88
}
@@ -104,7 +106,7 @@ func (c *Collector) walkAll(rootOid string) ([]gosnmp.SnmpPDU, error) {
106
return c.snmpClient.BulkWalkAll(rootOid)
107
}
108
107
-func (c *Collector) setupVnode(si *snmpsd.SysInfo, deviceMeta map[string]string) *vnodes.VirtualNode {
109
+func (c *Collector) setupVnode(si *snmputils.SysInfo, deviceMeta map[string]ddsnmp.MetaTag) *vnodes.VirtualNode {
110
if c.Vnode.GUID == "" {
111
c.Vnode.GUID = uuid.NewSHA1(uuid.NameSpaceDNS, []byte(c.Hostname)).String()
112
}
@@ -129,38 +131,31 @@ func (c *Collector) setupVnode(si *snmpsd.SysInfo, deviceMeta map[string]string)
131
labels["_node_stale_after_seconds"] = strconv.Itoa(v)
132
}
133
132
- maps.Copy(labels, c.Vnode.Labels)
133
- maps.Copy(labels, deviceMeta)
134
-
135
- if _, ok := labels["sys_object_id"]; !ok {
136
- labels["sys_object_id"] = si.SysObjectID
137
- }
138
- if _, ok := labels["name"]; !ok {
139
- labels["name"] = si.Name
140
- }
141
- if _, ok := labels["description"]; !ok && si.Descr != "" {
142
- labels["description"] = si.Descr
143
- }
144
- if _, ok := labels["contact"]; !ok && si.Contact != "" {
145
- labels["contact"] = si.Contact
146
- }
147
- if _, ok := labels["location"]; !ok && si.Location != "" {
148
- labels["location"] = si.Location
134
+ labels["sys_object_id"] = si.SysObjectID
135
+ labels["name"] = si.Name
136
+ labels["description"] = si.Descr
137
+ labels["contact"] = si.Contact
138
+ labels["location"] = si.Location
139
+ if si.Vendor != "" {
140
+ labels["vendor"] = si.Vendor
141
+ } else if si.Organization != "" {
142
+ labels["vendor"] = si.Organization
143
}
150
- if _, ok := labels["vendor"]; !ok {
151
- if si.Vendor != "" {
152
- labels["vendor"] = si.Vendor
153
- } else if si.Organization != "" {
154
- labels["vendor"] = si.Organization
155
- }
156
- }
157
- if _, ok := labels["type"]; !ok && si.Category != "" {
144
+ if si.Category != "" {
145
labels["type"] = si.Category
146
}
160
- if _, ok := labels["model"]; !ok && si.Model != "" {
147
+ if si.Model != "" {
148
labels["model"] = si.Model
149
}
150
151
+ for k, val := range deviceMeta {
152
+ if v, ok := labels[k]; !ok || v == "" || val.IsExactMatch {
153
+ labels[k] = val.Value
154
+ }
155
+ }
156
+
157
+ maps.Copy(labels, c.Vnode.Labels)
158
+
159
return &vnodes.VirtualNode{
160
GUID: c.Vnode.GUID,
161
Hostname: c.Vnode.Hostname,
@@ -195,7 +190,7 @@ func (c *Collector) adjustMaxRepetitions() (bool, error) {
190
for maxReps > 0 && attempts < maxAttempts {
191
attempts++
192
198
- v, err := c.walkAll(snmpsd.RootOidMibSystem)
193
+ v, err := c.walkAll(snmputils.RootOidMibSystem)
194
if err != nil {
195
return false, err
196
}
src/go/plugin/go.d/collector/snmp/collect_if_mib.go
+4
-4
@@ -9,7 +9,7 @@ import (
9
"strings"
10
11
"github.com/netdata/netdata/go/plugins/logger"
12
- "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/discoverer/snmpsd"
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
13
14
"github.com/gosnmp/gosnmp"
15
)
@@ -62,7 +62,7 @@ func (c *Collector) collectNetworkInterfaces(mx map[string]int64) error {
62
case oidIfIndex:
63
iface.ifIndex, err = pduToInt(pdu)
64
case oidIfDescr:
65
- iface.ifDescr, err = snmpsd.PduToString(pdu)
65
+ iface.ifDescr, err = snmputils.PduToString(pdu)
66
case oidIfType:
67
iface.ifType, err = pduToInt(pdu)
68
case oidIfMtu:
@@ -96,7 +96,7 @@ func (c *Collector) collectNetworkInterfaces(mx map[string]int64) error {
96
case oidIfOutErrors:
97
iface.ifOutErrors, err = pduToInt(pdu)
98
case oidIfName:
99
- iface.ifName, err = snmpsd.PduToString(pdu)
99
+ iface.ifName, err = snmputils.PduToString(pdu)
100
case oidIfInMulticastPkts:
101
iface.ifInMulticastPkts, err = pduToInt(pdu)
102
case oidIfInBroadcastPkts:
@@ -124,7 +124,7 @@ func (c *Collector) collectNetworkInterfaces(mx map[string]int64) error {
124
case oidIfHighSpeed:
125
iface.ifHighSpeed, err = pduToInt(pdu)
126
case oidIfAlias:
127
- iface.ifAlias, err = snmpsd.PduToString(pdu)
127
+ iface.ifAlias, err = snmputils.PduToString(pdu)
128
default:
129
continue
130
}
src/go/plugin/go.d/collector/snmp/collector.go
+3
-3
@@ -10,11 +10,11 @@ import (
10
"github.com/gosnmp/gosnmp"
11
12
"github.com/netdata/netdata/go/plugins/pkg/matcher"
13
- "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/discoverer/snmpsd"
13
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
16
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
18
)
19
20
//go:embed "config_schema.json"
@@ -90,7 +90,7 @@ type Collector struct {
90
91
netInterfaces map[string]*netInterface
92
93
- sysInfo *snmpsd.SysInfo
93
+ sysInfo *snmputils.SysInfo
94
95
customOids []string
96
@@ -140,7 +140,7 @@ func (c *Collector) Init(context.Context) error {
140
}
141
142
func (c *Collector) Check(context.Context) error {
143
- if _, err := snmpsd.GetSysInfo(c.snmpClient); err != nil {
143
+ if _, err := snmputils.GetSysInfo(c.snmpClient); err != nil {
144
return err
145
}
146
ok, err := c.adjustMaxRepetitions()
src/go/plugin/go.d/collector/snmp/collector_test.go
+10
-10
@@ -11,8 +11,8 @@ import (
11
"strings"
12
"testing"
13
14
- "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/discovery/sd/discoverer/snmpsd"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
16
17
"github.com/golang/mock/gomock"
18
"github.com/gosnmp/gosnmp"
@@ -221,7 +221,7 @@ func TestCollector_Check(t *testing.T) {
221
collr := New()
222
collr.Config = prepareConfigWithUserCharts(prepareV2Config(), 0, 3)
223
collr.collectIfMib = false
224
- m.EXPECT().WalkAll(snmpsd.RootOidMibSystem).Return(nil, errors.New("mock Get() error")).Times(1)
224
+ m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return(nil, errors.New("mock Get() error")).Times(1)
225
226
return collr
227
},
@@ -586,7 +586,7 @@ func setMockClientInitExpect(m *snmpmock.MockHandler) {
586
}
587
588
func setMockClientSysObjectidExpect(m *snmpmock.MockHandler) {
589
- m.EXPECT().Get([]string{snmpsd.OidSysObject}).Return(&gosnmp.SnmpPacket{
589
+ m.EXPECT().Get([]string{snmputils.OidSysObject}).Return(&gosnmp.SnmpPacket{
590
Variables: []gosnmp.SnmpPDU{
591
{Value: ".1.1.1",
592
Name: ".1.3.6.1.2.1.1.2.0",
@@ -602,19 +602,19 @@ func setMockClientSysObjectidExpect(m *snmpmock.MockHandler) {
602
}
603
604
func setMockClientSysInfoExpect(m *snmpmock.MockHandler) {
605
- m.EXPECT().WalkAll(snmpsd.RootOidMibSystem).Return([]gosnmp.SnmpPDU{
606
- {Name: snmpsd.OidSysDescr, Value: []uint8("mock sysDescr"), Type: gosnmp.OctetString},
607
- {Name: snmpsd.OidSysObject, Value: ".1.3.6.1.4.1.14988.1", Type: gosnmp.ObjectIdentifier},
608
- {Name: snmpsd.OidSysContact, Value: []uint8("mock sysContact"), Type: gosnmp.OctetString},
609
- {Name: snmpsd.OidSysName, Value: []uint8("mock sysName"), Type: gosnmp.OctetString},
610
- {Name: snmpsd.OidSysLocation, Value: []uint8("mock sysLocation"), Type: gosnmp.OctetString},
605
+ m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return([]gosnmp.SnmpPDU{
606
+ {Name: snmputils.OidSysDescr, Value: []uint8("mock sysDescr"), Type: gosnmp.OctetString},
607
+ {Name: snmputils.OidSysObject, Value: ".1.3.6.1.4.1.14988.1", Type: gosnmp.ObjectIdentifier},
608
+ {Name: snmputils.OidSysContact, Value: []uint8("mock sysContact"), Type: gosnmp.OctetString},
609
+ {Name: snmputils.OidSysName, Value: []uint8("mock sysName"), Type: gosnmp.OctetString},
610
+ {Name: snmputils.OidSysLocation, Value: []uint8("mock sysLocation"), Type: gosnmp.OctetString},
611
}, nil).MinTimes(1)
612
}
613
614
func setMockClientSysinfoAndUptimeExpect(m *snmpmock.MockHandler) {
615
setMockClientSysInfoExpect(m)
616
617
- m.EXPECT().Get([]string{snmpsd.OidSysUptime}).Return(&gosnmp.SnmpPacket{
617
+ m.EXPECT().Get([]string{oidSysUptime}).Return(&gosnmp.SnmpPacket{
618
Variables: []gosnmp.SnmpPDU{
619
{Value: uint32(6048), Type: gosnmp.TimeTicks},
620
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+6
-4
@@ -62,12 +62,12 @@ type (
62
profile *ddsnmp.Profile
63
initialized bool
64
globalTags map[string]string
65
- deviceMetadata map[string]string
65
+ deviceMetadata map[string]ddsnmp.MetaTag
66
}
67
)
68
69
-func (c *Collector) CollectDeviceMetadata() (map[string]string, error) {
70
- meta := make(map[string]string)
69
+func (c *Collector) CollectDeviceMetadata() (map[string]ddsnmp.MetaTag, error) {
70
+ meta := make(map[string]ddsnmp.MetaTag)
71
72
for _, prof := range c.profiles {
73
profDeviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
@@ -75,7 +75,9 @@ func (c *Collector) CollectDeviceMetadata() (map[string]string, error) {
75
return nil, err
76
}
77
78
- mergeTagsIfAbsent(meta, profDeviceMeta)
78
+ for k, v := range profDeviceMeta {
79
+ mergeMetaTagIfAbsent(meta, k, v)
80
+ }
81
}
82
83
return meta, nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta.go
renamed
+13
-139
@@ -14,132 +14,6 @@ import (
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
)
16
17
-// globalTagsCollector handles collection of profile-wide tags
18
-type globalTagsCollector struct {
19
- snmpClient gosnmp.Handler
20
- missingOIDs map[string]bool
21
- log *logger.Logger
22
- tagProc *globalTagProcessor
23
-}
24
-
25
-func newGlobalTagsCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *globalTagsCollector {
26
- return &globalTagsCollector{
27
- snmpClient: snmpClient,
28
- missingOIDs: missingOIDs,
29
- log: log,
30
- tagProc: newGlobalTagProcessor(),
31
- }
32
-}
33
-
34
-// Collect gathers all global tags from the profile
35
-func (gc *globalTagsCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
36
- if len(prof.Definition.MetricTags) == 0 && len(prof.Definition.StaticTags) == 0 {
37
- return nil, nil
38
- }
39
-
40
- tags := make(map[string]string)
41
-
42
- gc.processStaticTags(prof.Definition.StaticTags, tags)
43
-
44
- if err := gc.processDynamicTags(prof.Definition.MetricTags, tags); err != nil {
45
- return ternary(len(tags) > 0, tags, nil), err
46
- }
47
-
48
- return tags, nil
49
-}
50
-
51
-func (gc *globalTagsCollector) processStaticTags(staticTags []string, globalTags map[string]string) {
52
- mergeTagsIfAbsent(globalTags, parseStaticTags(staticTags))
53
-}
54
-
55
-// processDynamicTags processes tags that require SNMP fetching
56
-func (gc *globalTagsCollector) processDynamicTags(metricTags []ddprofiledefinition.MetricTagConfig, globalTags map[string]string) error {
57
- // Identify OIDs to collect
58
- oids, missingOIDs := gc.identifyTagOIDs(metricTags)
59
-
60
- if len(missingOIDs) > 0 {
61
- gc.log.Debugf("global tags missing OIDs: %v", missingOIDs)
62
- }
63
-
64
- if len(oids) == 0 {
65
- return nil
66
- }
67
-
68
- pdus, err := gc.fetchTagValues(oids)
69
- if err != nil {
70
- return fmt.Errorf("failed to fetch global tag values: %w", err)
71
- }
72
-
73
- // Collect each tag configuration
74
- var errs []error
75
- for _, tagCfg := range metricTags {
76
- if tagCfg.Symbol.OID == "" {
77
- continue
78
- }
79
-
80
- ta := tagAdder{tags: globalTags}
81
-
82
- if err := gc.tagProc.processTag(tagCfg, pdus, ta); err != nil {
83
- errs = append(errs, fmt.Errorf("failed to process tag value for '%s/%s': %w",
84
- tagCfg.Tag, tagCfg.Symbol.Name, err))
85
- continue
86
- }
87
- }
88
-
89
- if len(errs) > 0 && len(globalTags) == 0 {
90
- return fmt.Errorf("failed to process any global tags: %w", errors.Join(errs...))
91
- }
92
-
93
- return nil
94
-}
95
-
96
-func (gc *globalTagsCollector) identifyTagOIDs(metricTags []ddprofiledefinition.MetricTagConfig) ([]string, []string) {
97
- var oids []string
98
- var missingOIDs []string
99
-
100
- for _, tagCfg := range metricTags {
101
- if tagCfg.Symbol.OID == "" {
102
- continue
103
- }
104
-
105
- oid := trimOID(tagCfg.Symbol.OID)
106
- if gc.missingOIDs[oid] {
107
- missingOIDs = append(missingOIDs, tagCfg.Symbol.OID)
108
- continue
109
- }
110
-
111
- oids = append(oids, tagCfg.Symbol.OID)
112
- }
113
-
114
- // Sort and deduplicate
115
- slices.Sort(oids)
116
- oids = slices.Compact(oids)
117
-
118
- return oids, missingOIDs
119
-}
120
-
121
-func (gc *globalTagsCollector) fetchTagValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
122
- pdus := make(map[string]gosnmp.SnmpPDU)
123
- maxOids := gc.snmpClient.MaxOids()
124
-
125
- for chunk := range slices.Chunk(oids, maxOids) {
126
- result, err := gc.snmpClient.Get(chunk)
127
- if err != nil {
128
- return nil, err
129
- }
130
-
131
- for _, pdu := range result.Variables {
132
- if !isPduWithData(pdu) {
133
- gc.missingOIDs[trimOID(pdu.Name)] = true
134
- continue
135
- }
136
- pdus[trimOID(pdu.Name)] = pdu
137
- }
138
- }
139
-
140
- return pdus, nil
141
-}
142
-
17
// deviceMetadataCollector handles collection of device metadata
18
type deviceMetadataCollector struct {
19
snmpClient gosnmp.Handler
@@ -157,8 +31,8 @@ func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[strin
31
}
32
}
33
160
-func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
161
- if len(prof.Definition.Metadata) == 0 {
34
+func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]ddsnmp.MetaTag, error) {
35
+ if len(prof.Definition.Metadata) == 0 && len(prof.Definition.SysobjectIDMetadata) == 0 {
36
return nil, nil
37
}
38
@@ -168,12 +42,12 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
42
return nil, nil
43
}
44
171
- meta := make(map[string]string)
45
+ meta := make(map[string]ddsnmp.MetaTag)
46
47
if dc.sysobjectid != "" {
48
for i, entry := range prof.Definition.SysobjectIDMetadata {
49
if ddsnmp.OidMatches(dc.sysobjectid, entry.SysobjectID) {
176
- if err := dc.processMetadataFields(entry.Metadata, meta); err != nil {
50
+ if err := dc.processMetadataFields(entry.Metadata, meta, dc.sysobjectid == entry.SysobjectID); err != nil {
51
dc.log.Warningf("sysobjectid_metadata[%d]: failed to process metadata fields for sysobjectid '%s': %v",
52
i, entry.SysobjectID, err)
53
continue
@@ -184,7 +58,7 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
58
}
59
}
60
187
- if err := dc.processMetadataFields(cfg.Fields, meta); err != nil {
61
+ if err := dc.processMetadataFields(cfg.Fields, meta, slices.Contains(prof.Definition.Extends, dc.sysobjectid)); err != nil {
62
return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
63
}
64
@@ -192,8 +66,8 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
66
}
67
68
// processMetadataFields processes a single metadata resource
195
-func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) error {
196
- oids := dc.collectStaticAndIdentifyOIDs(fields, metadata)
69
+func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]ddsnmp.MetaTag, isExactMatch bool) error {
70
+ oids := dc.collectStaticAndIdentifyOIDs(fields, metadata, isExactMatch)
71
72
if len(oids) == 0 {
73
return nil
@@ -204,17 +78,17 @@ func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddpro
78
return fmt.Errorf("failed to fetch metadata values: %w", err)
79
}
80
207
- return dc.processDynamicFields(fields, pdus, metadata)
81
+ return dc.processDynamicFields(fields, pdus, metadata, isExactMatch)
82
}
83
84
// collectStaticAndIdentifyOIDs collects static values and returns OIDs to fetch
211
-func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) []string {
85
+func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]ddsnmp.MetaTag, isExactMatch bool) []string {
86
var oids []string
87
88
for name, field := range fields {
89
switch {
90
case field.Value != "":
217
- mergeTagsIfAbsent(metadata, map[string]string{name: field.Value})
91
+ mergeMetaTagIfAbsent(metadata, name, ddsnmp.MetaTag{Value: field.Value, IsExactMatch: isExactMatch})
92
case field.Symbol.OID != "":
93
if !dc.missingOIDs[trimOID(field.Symbol.OID)] {
94
oids = append(oids, field.Symbol.OID)
@@ -257,7 +131,7 @@ func (dc *deviceMetadataCollector) fetchMetadataValues(oids []string) (map[strin
131
return pdus, nil
132
}
133
260
-func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprofiledefinition.MetadataField, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
134
+func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprofiledefinition.MetadataField, pdus map[string]gosnmp.SnmpPDU, metadata map[string]ddsnmp.MetaTag, isExactMatch bool) error {
135
var errs []error
136
137
for name, field := range fields {
@@ -270,7 +144,7 @@ func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprof
144
continue
145
}
146
if v != "" {
273
- mergeTagsIfAbsent(metadata, map[string]string{name: v})
147
+ mergeMetaTagIfAbsent(metadata, name, ddsnmp.MetaTag{Value: v, IsExactMatch: isExactMatch})
148
}
149
case len(field.Symbols) > 0:
150
// Multiple symbols - try each until one succeeds
@@ -282,7 +156,7 @@ func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprof
156
continue
157
}
158
if v != "" {
285
- mergeTagsIfAbsent(metadata, map[string]string{name: v})
159
+ mergeMetaTagIfAbsent(metadata, name, ddsnmp.MetaTag{Value: v, IsExactMatch: isExactMatch})
160
break // Use first successful value
161
}
162
}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_device_meta_test.go
new
+955
@@ -0,0 +1,955 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "testing"
8
+
9
+ "github.com/golang/mock/gomock"
10
+ "github.com/gosnmp/gosnmp"
11
+ "github.com/stretchr/testify/assert"
12
+
13
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
14
+
15
+ "github.com/netdata/netdata/go/plugins/logger"
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
+)
19
+
20
+func TestDeviceMetadataCollector_Collect(t *testing.T) {
21
+ tests := map[string]struct {
22
+ profile *ddsnmp.Profile
23
+ setupMock func(m *snmpmock.MockHandler)
24
+ sysobjectid string
25
+ expectedResult map[string]ddsnmp.MetaTag
26
+ expectedError bool
27
+ errorContains string
28
+ }{
29
+ "no metadata configured": {
30
+ profile: &ddsnmp.Profile{
31
+ Definition: &ddprofiledefinition.ProfileDefinition{
32
+ Metadata: ddprofiledefinition.MetadataConfig{},
33
+ },
34
+ },
35
+ setupMock: func(m *snmpmock.MockHandler) {},
36
+ expectedResult: nil,
37
+ expectedError: false,
38
+ },
39
+ "static values only": {
40
+ profile: &ddsnmp.Profile{
41
+ Definition: &ddprofiledefinition.ProfileDefinition{
42
+ Metadata: ddprofiledefinition.MetadataConfig{
43
+ "device": ddprofiledefinition.MetadataResourceConfig{
44
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
45
+ "vendor": {Value: "dell"},
46
+ "type": {Value: "router"},
47
+ "model": {Value: "PowerEdge R740"},
48
+ },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ setupMock: func(m *snmpmock.MockHandler) {},
54
+ expectedResult: map[string]ddsnmp.MetaTag{
55
+ "vendor": {Value: "dell", IsExactMatch: false},
56
+ "type": {Value: "router", IsExactMatch: false},
57
+ "model": {Value: "PowerEdge R740", IsExactMatch: false},
58
+ },
59
+ expectedError: false,
60
+ },
61
+ "dynamic values with single symbol": {
62
+ profile: &ddsnmp.Profile{
63
+ Definition: &ddprofiledefinition.ProfileDefinition{
64
+ Metadata: ddprofiledefinition.MetadataConfig{
65
+ "device": ddprofiledefinition.MetadataResourceConfig{
66
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
67
+ "vendor": {Value: "dell"},
68
+ "serial_number": {
69
+ Symbol: ddprofiledefinition.SymbolConfig{
70
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
71
+ Name: "chassisSerialNumber",
72
+ },
73
+ },
74
+ "version": {
75
+ Symbol: ddprofiledefinition.SymbolConfig{
76
+ OID: "1.3.6.1.2.1.1.1.0",
77
+ Name: "sysDescr",
78
+ },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ setupMock: func(m *snmpmock.MockHandler) {
86
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
87
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
88
+ "1.3.6.1.2.1.1.1.0",
89
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
90
+ })).Return(
91
+ &gosnmp.SnmpPacket{
92
+ Variables: []gosnmp.SnmpPDU{
93
+ {Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0", Type: gosnmp.OctetString, Value: []byte("ABC123XYZ")},
94
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Dell EMC Networking OS10")},
95
+ },
96
+ }, nil,
97
+ )
98
+ },
99
+ expectedResult: map[string]ddsnmp.MetaTag{
100
+ "vendor": {Value: "dell", IsExactMatch: false},
101
+ "serial_number": {Value: "ABC123XYZ", IsExactMatch: false},
102
+ "version": {Value: "Dell EMC Networking OS10", IsExactMatch: false},
103
+ },
104
+ expectedError: false,
105
+ },
106
+ "multiple symbols fallback": {
107
+ profile: &ddsnmp.Profile{
108
+ Definition: &ddprofiledefinition.ProfileDefinition{
109
+ Metadata: ddprofiledefinition.MetadataConfig{
110
+ "device": ddprofiledefinition.MetadataResourceConfig{
111
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
112
+ "serial_number": {
113
+ Symbols: []ddprofiledefinition.SymbolConfig{
114
+ {OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0", Name: "chassisSerialNumber"},
115
+ {OID: "1.3.6.1.4.1.674.10892.5.1.3.3.0", Name: "backupSerialNumber"},
116
+ },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ setupMock: func(m *snmpmock.MockHandler) {
124
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
125
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
126
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
127
+ "1.3.6.1.4.1.674.10892.5.1.3.3.0",
128
+ })).Return(
129
+ &gosnmp.SnmpPacket{
130
+ Variables: []gosnmp.SnmpPDU{
131
+ {Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0", Type: gosnmp.NoSuchObject, Value: nil},
132
+ {Name: "1.3.6.1.4.1.674.10892.5.1.3.3.0", Type: gosnmp.OctetString, Value: []byte("BACKUP123")},
133
+ },
134
+ }, nil,
135
+ )
136
+ },
137
+ expectedResult: map[string]ddsnmp.MetaTag{
138
+ "serial_number": {Value: "BACKUP123", IsExactMatch: false},
139
+ },
140
+ expectedError: false,
141
+ },
142
+ "value with match_pattern": {
143
+ profile: &ddsnmp.Profile{
144
+ Definition: &ddprofiledefinition.ProfileDefinition{
145
+ Metadata: ddprofiledefinition.MetadataConfig{
146
+ "device": ddprofiledefinition.MetadataResourceConfig{
147
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
148
+ "version": {
149
+ Symbol: ddprofiledefinition.SymbolConfig{
150
+ OID: "1.3.6.1.2.1.1.1.0",
151
+ Name: "sysDescr",
152
+ MatchPatternCompiled: mustCompileRegex(`Isilon OneFS v(\S+)`),
153
+ MatchValue: "$1",
154
+ },
155
+ },
156
+ },
157
+ },
158
+ },
159
+ },
160
+ },
161
+ setupMock: func(m *snmpmock.MockHandler) {
162
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
163
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
164
+ &gosnmp.SnmpPacket{
165
+ Variables: []gosnmp.SnmpPDU{
166
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("device-name-3 263829375 Isilon OneFS v8.2.0.0")},
167
+ },
168
+ }, nil,
169
+ )
170
+ },
171
+ expectedResult: map[string]ddsnmp.MetaTag{
172
+ "version": {Value: "8.2.0.0", IsExactMatch: false},
173
+ },
174
+ expectedError: false,
175
+ },
176
+ "value with extract_value": {
177
+ profile: &ddsnmp.Profile{
178
+ Definition: &ddprofiledefinition.ProfileDefinition{
179
+ Metadata: ddprofiledefinition.MetadataConfig{
180
+ "device": ddprofiledefinition.MetadataResourceConfig{
181
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
182
+ "temperature": {
183
+ Symbol: ddprofiledefinition.SymbolConfig{
184
+ OID: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1",
185
+ Name: "temperatureProbeReading",
186
+ ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
187
+ },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ setupMock: func(m *snmpmock.MockHandler) {
195
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
196
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1"}).Return(
197
+ &gosnmp.SnmpPacket{
198
+ Variables: []gosnmp.SnmpPDU{
199
+ {Name: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1", Type: gosnmp.OctetString, Value: []byte("25C")},
200
+ },
201
+ }, nil,
202
+ )
203
+ },
204
+ expectedResult: map[string]ddsnmp.MetaTag{
205
+ "temperature": {Value: "25", IsExactMatch: false},
206
+ },
207
+ expectedError: false,
208
+ },
209
+ "value with mapping": {
210
+ profile: &ddsnmp.Profile{
211
+ Definition: &ddprofiledefinition.ProfileDefinition{
212
+ Metadata: ddprofiledefinition.MetadataConfig{
213
+ "device": ddprofiledefinition.MetadataResourceConfig{
214
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
215
+ "status": {
216
+ Symbol: ddprofiledefinition.SymbolConfig{
217
+ OID: "1.3.6.1.4.1.674.10892.5.2.1.0",
218
+ Name: "globalSystemStatus",
219
+ Mapping: map[string]string{
220
+ "1": "other",
221
+ "2": "unknown",
222
+ "3": "ok",
223
+ "4": "nonCritical",
224
+ "5": "critical",
225
+ "6": "nonRecoverable",
226
+ },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ },
232
+ },
233
+ },
234
+ setupMock: func(m *snmpmock.MockHandler) {
235
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
236
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.2.1.0"}).Return(
237
+ &gosnmp.SnmpPacket{
238
+ Variables: []gosnmp.SnmpPDU{
239
+ {Name: "1.3.6.1.4.1.674.10892.5.2.1.0", Type: gosnmp.Integer, Value: 3},
240
+ },
241
+ }, nil,
242
+ )
243
+ },
244
+ expectedResult: map[string]ddsnmp.MetaTag{
245
+ "status": {Value: "ok", IsExactMatch: false},
246
+ },
247
+ expectedError: false,
248
+ },
249
+ "format mac_address": {
250
+ profile: &ddsnmp.Profile{
251
+ Definition: &ddprofiledefinition.ProfileDefinition{
252
+ Metadata: ddprofiledefinition.MetadataConfig{
253
+ "device": ddprofiledefinition.MetadataResourceConfig{
254
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
255
+ "mac_address": {
256
+ Symbol: ddprofiledefinition.SymbolConfig{
257
+ OID: "1.3.6.1.2.1.2.2.1.6.1",
258
+ Name: "ifPhysAddress",
259
+ Format: "mac_address",
260
+ },
261
+ },
262
+ },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ setupMock: func(m *snmpmock.MockHandler) {
268
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
269
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.6.1"}).Return(
270
+ &gosnmp.SnmpPacket{
271
+ Variables: []gosnmp.SnmpPDU{
272
+ {Name: "1.3.6.1.2.1.2.2.1.6.1", Type: gosnmp.OctetString, Value: []byte{0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF}},
273
+ },
274
+ }, nil,
275
+ )
276
+ },
277
+ expectedResult: map[string]ddsnmp.MetaTag{
278
+ "mac_address": {Value: "00:50:56:AB:CD:EF", IsExactMatch: false},
279
+ },
280
+ expectedError: false,
281
+ },
282
+ "non-device resource ignored": {
283
+ profile: &ddsnmp.Profile{
284
+ Definition: &ddprofiledefinition.ProfileDefinition{
285
+ Metadata: ddprofiledefinition.MetadataConfig{
286
+ "interface": ddprofiledefinition.MetadataResourceConfig{
287
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
288
+ "name": {Value: "eth0"},
289
+ },
290
+ },
291
+ "device": ddprofiledefinition.MetadataResourceConfig{
292
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
293
+ "vendor": {Value: "cisco"},
294
+ },
295
+ },
296
+ },
297
+ },
298
+ },
299
+ setupMock: func(m *snmpmock.MockHandler) {},
300
+ expectedResult: map[string]ddsnmp.MetaTag{
301
+ "vendor": {Value: "cisco", IsExactMatch: false},
302
+ },
303
+ expectedError: false,
304
+ },
305
+ "SNMP error": {
306
+ profile: &ddsnmp.Profile{
307
+ Definition: &ddprofiledefinition.ProfileDefinition{
308
+ Metadata: ddprofiledefinition.MetadataConfig{
309
+ "device": ddprofiledefinition.MetadataResourceConfig{
310
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
311
+ "serial_number": {
312
+ Symbol: ddprofiledefinition.SymbolConfig{
313
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
314
+ Name: "chassisSerialNumber",
315
+ },
316
+ },
317
+ },
318
+ },
319
+ },
320
+ },
321
+ },
322
+ setupMock: func(m *snmpmock.MockHandler) {
323
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
324
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.1.3.2.0"}).Return(nil, errors.New("SNMP timeout"))
325
+ },
326
+ expectedResult: nil,
327
+ expectedError: true,
328
+ errorContains: "failed to fetch metadata values",
329
+ },
330
+ "missing OID continues with other fields": {
331
+ profile: &ddsnmp.Profile{
332
+ Definition: &ddprofiledefinition.ProfileDefinition{
333
+ Metadata: ddprofiledefinition.MetadataConfig{
334
+ "device": ddprofiledefinition.MetadataResourceConfig{
335
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
336
+ "vendor": {Value: "dell"},
337
+ "serial_number": {
338
+ Symbol: ddprofiledefinition.SymbolConfig{
339
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
340
+ Name: "chassisSerialNumber",
341
+ },
342
+ },
343
+ "model": {
344
+ Symbol: ddprofiledefinition.SymbolConfig{
345
+ OID: "1.3.6.1.4.1.674.10892.5.1.3.1.0",
346
+ Name: "chassisModelName",
347
+ },
348
+ },
349
+ },
350
+ },
351
+ },
352
+ },
353
+ },
354
+ setupMock: func(m *snmpmock.MockHandler) {
355
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
356
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
357
+ "1.3.6.1.4.1.674.10892.5.1.3.1.0",
358
+ "1.3.6.1.4.1.674.10892.5.1.3.2.0",
359
+ })).Return(
360
+ &gosnmp.SnmpPacket{
361
+ Variables: []gosnmp.SnmpPDU{
362
+ {Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0", Type: gosnmp.NoSuchObject, Value: nil},
363
+ {Name: "1.3.6.1.4.1.674.10892.5.1.3.1.0", Type: gosnmp.OctetString, Value: []byte("PowerEdge R740")},
364
+ },
365
+ }, nil,
366
+ )
367
+ },
368
+ expectedResult: map[string]ddsnmp.MetaTag{
369
+ "vendor": {Value: "dell", IsExactMatch: false},
370
+ "model": {Value: "PowerEdge R740", IsExactMatch: false},
371
+ },
372
+ expectedError: false,
373
+ },
374
+ "chunked requests": {
375
+ profile: &ddsnmp.Profile{
376
+ Definition: &ddprofiledefinition.ProfileDefinition{
377
+ Metadata: ddprofiledefinition.MetadataConfig{
378
+ "device": ddprofiledefinition.MetadataResourceConfig{
379
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
380
+ "field1": {Symbol: ddprofiledefinition.SymbolConfig{OID: "1.3.6.1.2.1.1.1.0", Name: "oid1"}},
381
+ "field2": {Symbol: ddprofiledefinition.SymbolConfig{OID: "1.3.6.1.2.1.1.2.0", Name: "oid2"}},
382
+ "field3": {Symbol: ddprofiledefinition.SymbolConfig{OID: "1.3.6.1.2.1.1.3.0", Name: "oid3"}},
383
+ },
384
+ },
385
+ },
386
+ },
387
+ },
388
+ setupMock: func(m *snmpmock.MockHandler) {
389
+ m.EXPECT().MaxOids().Return(2).AnyTimes()
390
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
391
+ &gosnmp.SnmpPacket{
392
+ Variables: []gosnmp.SnmpPDU{
393
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("value1")},
394
+ {Name: "1.3.6.1.2.1.1.2.0", Type: gosnmp.OctetString, Value: []byte("value2")},
395
+ },
396
+ }, nil,
397
+ )
398
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
399
+ &gosnmp.SnmpPacket{
400
+ Variables: []gosnmp.SnmpPDU{
401
+ {Name: "1.3.6.1.2.1.1.3.0", Type: gosnmp.OctetString, Value: []byte("value3")},
402
+ },
403
+ }, nil,
404
+ )
405
+ },
406
+ expectedResult: map[string]ddsnmp.MetaTag{
407
+ "field1": {Value: "value1", IsExactMatch: false},
408
+ "field2": {Value: "value2", IsExactMatch: false},
409
+ "field3": {Value: "value3", IsExactMatch: false},
410
+ },
411
+ expectedError: false,
412
+ },
413
+ "sysobjectid metadata override - single match": {
414
+ profile: &ddsnmp.Profile{
415
+ Definition: &ddprofiledefinition.ProfileDefinition{
416
+ Metadata: ddprofiledefinition.MetadataConfig{
417
+ "device": {
418
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
419
+ "vendor": {Value: "Cisco"},
420
+ "type": {Value: "Firewall"},
421
+ },
422
+ },
423
+ },
424
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
425
+ {
426
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
427
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
428
+ "model": {Value: "ASA5510"},
429
+ "series": {Value: "ASA5500"},
430
+ },
431
+ },
432
+ {
433
+ SysobjectID: "1.3.6.1.4.1.9.1.670",
434
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
435
+ "model": {Value: "ASA5520"},
436
+ },
437
+ },
438
+ },
439
+ },
440
+ },
441
+ setupMock: func(m *snmpmock.MockHandler) {},
442
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
443
+ expectedResult: map[string]ddsnmp.MetaTag{
444
+ "vendor": {Value: "Cisco", IsExactMatch: false},
445
+ "type": {Value: "Firewall", IsExactMatch: false},
446
+ "model": {Value: "ASA5510", IsExactMatch: true},
447
+ "series": {Value: "ASA5500", IsExactMatch: true},
448
+ },
449
+ expectedError: false,
450
+ },
451
+ "sysobjectid metadata override - multiple matches cascade": {
452
+ profile: &ddsnmp.Profile{
453
+ Definition: &ddprofiledefinition.ProfileDefinition{
454
+ Metadata: ddprofiledefinition.MetadataConfig{
455
+ "device": {
456
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
457
+ "vendor": {Value: "Cisco"},
458
+ "platform": {Value: "Default Platform"},
459
+ },
460
+ },
461
+ },
462
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
463
+ {
464
+ SysobjectID: "1.3.6.1.4.1.9.*",
465
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
466
+ "platform": {Value: "Enterprise"},
467
+ "support": {Value: "Premium"},
468
+ },
469
+ },
470
+ {
471
+ SysobjectID: "1.3.6.1.4.1.9.1.*",
472
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
473
+ "type": {Value: "Firewall"},
474
+ "series": {Value: "ASA"},
475
+ "support": {Value: "Standard"},
476
+ },
477
+ },
478
+ {
479
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
480
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
481
+ "model": {Value: "ASA5510"},
482
+ "series": {Value: "ASA5500"},
483
+ },
484
+ },
485
+ },
486
+ },
487
+ },
488
+ setupMock: func(m *snmpmock.MockHandler) {},
489
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
490
+ expectedResult: map[string]ddsnmp.MetaTag{
491
+ "vendor": {Value: "Cisco", IsExactMatch: false}, // base
492
+ "platform": {Value: "Enterprise", IsExactMatch: false}, // wildcard match
493
+ "support": {Value: "Premium", IsExactMatch: false}, // first wildcard wins
494
+ "type": {Value: "Firewall", IsExactMatch: false}, // second wildcard
495
+ "series": {Value: "ASA5500", IsExactMatch: true}, // exact match entry
496
+ "model": {Value: "ASA5510", IsExactMatch: true}, // exact match entry
497
+ },
498
+ expectedError: false,
499
+ },
500
+ "sysobjectid metadata with dynamic SNMP values": {
501
+ profile: &ddsnmp.Profile{
502
+ Definition: &ddprofiledefinition.ProfileDefinition{
503
+ Metadata: ddprofiledefinition.MetadataConfig{
504
+ "device": {Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"vendor": {Value: "Cisco"}}},
505
+ },
506
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
507
+ {
508
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
509
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
510
+ "model": {Value: "ASA5510"},
511
+ "firmware": {Symbol: ddprofiledefinition.SymbolConfig{
512
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
513
+ Name: "ciscoImageVersion",
514
+ }},
515
+ },
516
+ },
517
+ },
518
+ },
519
+ },
520
+ setupMock: func(m *snmpmock.MockHandler) {
521
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
522
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
523
+ &gosnmp.SnmpPacket{Variables: []gosnmp.SnmpPDU{
524
+ {Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1", Type: gosnmp.OctetString, Value: []byte("9.2(4)")},
525
+ }}, nil,
526
+ )
527
+ },
528
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
529
+ expectedResult: map[string]ddsnmp.MetaTag{
530
+ "vendor": {Value: "Cisco", IsExactMatch: false},
531
+ "model": {Value: "ASA5510", IsExactMatch: true},
532
+ "firmware": {Value: "9.2(4)", IsExactMatch: true},
533
+ },
534
+ expectedError: false,
535
+ },
536
+ "sysobjectid metadata no match": {
537
+ profile: &ddsnmp.Profile{
538
+ Definition: &ddprofiledefinition.ProfileDefinition{
539
+ Metadata: ddprofiledefinition.MetadataConfig{
540
+ "device": {
541
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
542
+ "vendor": {Value: "Cisco"},
543
+ "type": {Value: "Firewall"},
544
+ },
545
+ },
546
+ },
547
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
548
+ {
549
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
550
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"model": {Value: "ASA5510"}},
551
+ },
552
+ },
553
+ },
554
+ },
555
+ setupMock: func(m *snmpmock.MockHandler) {},
556
+ sysobjectid: "1.3.6.1.4.1.9.1.700",
557
+ expectedResult: map[string]ddsnmp.MetaTag{
558
+ "vendor": {Value: "Cisco", IsExactMatch: false},
559
+ "type": {Value: "Firewall", IsExactMatch: false},
560
+ },
561
+ expectedError: false,
562
+ },
563
+ "sysobjectid metadata with invalid regex pattern": {
564
+ profile: &ddsnmp.Profile{
565
+ Definition: &ddprofiledefinition.ProfileDefinition{
566
+ Metadata: ddprofiledefinition.MetadataConfig{
567
+ "device": {Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"vendor": {Value: "Cisco"}}},
568
+ },
569
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
570
+ {
571
+ SysobjectID: "1.3.6.1.4.1.9.1.[invalid",
572
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"model": {Value: "ASA5510"}},
573
+ },
574
+ {
575
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
576
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"series": {Value: "ASA5500"}},
577
+ },
578
+ },
579
+ },
580
+ },
581
+ setupMock: func(m *snmpmock.MockHandler) {},
582
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
583
+ expectedResult: map[string]ddsnmp.MetaTag{
584
+ "vendor": {Value: "Cisco", IsExactMatch: false},
585
+ "series": {Value: "ASA5500", IsExactMatch: true},
586
+ },
587
+ expectedError: false,
588
+ },
589
+ "sysobjectid metadata overrides base metadata field": {
590
+ profile: &ddsnmp.Profile{
591
+ Definition: &ddprofiledefinition.ProfileDefinition{
592
+ Metadata: ddprofiledefinition.MetadataConfig{
593
+ "device": {
594
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
595
+ "vendor": {Value: "Generic"},
596
+ "type": {Value: "Unknown"},
597
+ "model": {Value: "Generic Model"},
598
+ },
599
+ },
600
+ },
601
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
602
+ {
603
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
604
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
605
+ "vendor": {Value: "Cisco Systems"},
606
+ "model": {Value: "ASA5510"},
607
+ },
608
+ },
609
+ },
610
+ },
611
+ },
612
+ setupMock: func(m *snmpmock.MockHandler) {},
613
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
614
+ expectedResult: map[string]ddsnmp.MetaTag{
615
+ "vendor": {Value: "Cisco Systems", IsExactMatch: true},
616
+ "type": {Value: "Unknown", IsExactMatch: false},
617
+ "model": {Value: "ASA5510", IsExactMatch: true},
618
+ },
619
+ expectedError: false,
620
+ },
621
+ "sysobjectid metadata with SNMP fetch error continues": {
622
+ profile: &ddsnmp.Profile{
623
+ Definition: &ddprofiledefinition.ProfileDefinition{
624
+ Metadata: ddprofiledefinition.MetadataConfig{
625
+ "device": {Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{"vendor": {Value: "Cisco"}}},
626
+ },
627
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
628
+ {
629
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
630
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
631
+ "model": {Value: "ASA5510"},
632
+ "firmware": {Symbol: ddprofiledefinition.SymbolConfig{
633
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
634
+ Name: "ciscoImageVersion",
635
+ }},
636
+ },
637
+ },
638
+ },
639
+ },
640
+ },
641
+ setupMock: func(m *snmpmock.MockHandler) {
642
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
643
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(nil, errors.New("SNMP timeout"))
644
+ },
645
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
646
+ expectedResult: map[string]ddsnmp.MetaTag{
647
+ "vendor": {Value: "Cisco", IsExactMatch: false},
648
+ "model": {Value: "ASA5510", IsExactMatch: true},
649
+ },
650
+ expectedError: false,
651
+ },
652
+ "os_name with multiple symbols and match_pattern fallback": {
653
+ profile: &ddsnmp.Profile{
654
+ Definition: &ddprofiledefinition.ProfileDefinition{
655
+ Metadata: ddprofiledefinition.MetadataConfig{
656
+ "device": {
657
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
658
+ "vendor": {Value: "Cisco"},
659
+ "os_name": {
660
+ Symbols: []ddprofiledefinition.SymbolConfig{
661
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco Internetwork Operating System Software`), MatchValue: "IOS"},
662
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`), MatchValue: "IOS"},
663
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco NX-OS`), MatchValue: "NXOS"},
664
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco IOS XR`), MatchValue: "IOSXR"},
665
+ },
666
+ },
667
+ },
668
+ },
669
+ },
670
+ },
671
+ },
672
+ setupMock: func(m *snmpmock.MockHandler) {
673
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
674
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
675
+ &gosnmp.SnmpPacket{
676
+ Variables: []gosnmp.SnmpPDU{
677
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Cisco NX-OS(tm) m9100, Software (m9100-s2ek9-mz), Version 4.1(1c), RELEASE SOFTWARE Copyright (c) 2002-2008 by Cisco Systems, Inc. Compiled 11/24/2008 18:00:00")},
678
+ },
679
+ }, nil,
680
+ )
681
+ },
682
+ expectedResult: map[string]ddsnmp.MetaTag{
683
+ "vendor": {Value: "Cisco", IsExactMatch: false},
684
+ "os_name": {Value: "NXOS", IsExactMatch: false},
685
+ },
686
+ expectedError: false,
687
+ },
688
+ "os_name with multiple symbols - no match fallback": {
689
+ profile: &ddsnmp.Profile{
690
+ Definition: &ddprofiledefinition.ProfileDefinition{
691
+ Metadata: ddprofiledefinition.MetadataConfig{
692
+ "device": {
693
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
694
+ "vendor": {Value: "Juniper"},
695
+ "os_name": {
696
+ Symbols: []ddprofiledefinition.SymbolConfig{
697
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`), MatchValue: "IOS"},
698
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco NX-OS`), MatchValue: "NXOS"},
699
+ },
700
+ },
701
+ },
702
+ },
703
+ },
704
+ },
705
+ },
706
+ setupMock: func(m *snmpmock.MockHandler) {
707
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
708
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
709
+ &gosnmp.SnmpPacket{
710
+ Variables: []gosnmp.SnmpPDU{
711
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Juniper Networks, Inc. mx960 internet router, kernel JUNOS 12.3R3.4")},
712
+ },
713
+ }, nil,
714
+ )
715
+ },
716
+ expectedResult: map[string]ddsnmp.MetaTag{
717
+ "vendor": {Value: "Juniper", IsExactMatch: false},
718
+ },
719
+ expectedError: false,
720
+ },
721
+ "os_name with single symbol and match_pattern - no match": {
722
+ profile: &ddsnmp.Profile{
723
+ Definition: &ddprofiledefinition.ProfileDefinition{
724
+ Metadata: ddprofiledefinition.MetadataConfig{
725
+ "device": {
726
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
727
+ "vendor": {Value: "Generic"},
728
+ "os_name": {
729
+ Symbol: ddprofiledefinition.SymbolConfig{
730
+ OID: "1.3.6.1.2.1.1.1.0",
731
+ Name: "sysDescr",
732
+ MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`),
733
+ MatchValue: "IOS",
734
+ },
735
+ },
736
+ },
737
+ },
738
+ },
739
+ },
740
+ },
741
+ setupMock: func(m *snmpmock.MockHandler) {
742
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
743
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
744
+ &gosnmp.SnmpPacket{
745
+ Variables: []gosnmp.SnmpPDU{
746
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Some other device description")},
747
+ },
748
+ }, nil,
749
+ )
750
+ },
751
+ expectedResult: map[string]ddsnmp.MetaTag{
752
+ "vendor": {Value: "Generic", IsExactMatch: false},
753
+ },
754
+ expectedError: false,
755
+ },
756
+ "mixed fields with match_pattern and extract_value": {
757
+ profile: &ddsnmp.Profile{
758
+ Definition: &ddprofiledefinition.ProfileDefinition{
759
+ Metadata: ddprofiledefinition.MetadataConfig{
760
+ "device": {
761
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
762
+ "vendor": {Value: "Cisco"},
763
+ "version": {Symbol: ddprofiledefinition.SymbolConfig{
764
+ OID: "1.3.6.1.2.1.1.1.0",
765
+ Name: "sysDescr",
766
+ ExtractValueCompiled: mustCompileRegex(`Version\s+([a-zA-Z0-9.()\[\]]+)`),
767
+ }},
768
+ "model": {Symbol: ddprofiledefinition.SymbolConfig{
769
+ OID: "1.3.6.1.2.1.1.1.0",
770
+ Name: "sysDescr",
771
+ ExtractValueCompiled: mustCompileRegex(`Software\s+\(([-a-zA-Z0-9_ ]+)\)`),
772
+ }},
773
+ "os_name": {
774
+ Symbols: []ddprofiledefinition.SymbolConfig{
775
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`), MatchValue: "IOS"},
776
+ {OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr", MatchPatternCompiled: mustCompileRegex(`Cisco IOS XR Software`), MatchValue: "IOSXR"},
777
+ },
778
+ },
779
+ },
780
+ },
781
+ },
782
+ },
783
+ },
784
+ setupMock: func(m *snmpmock.MockHandler) {
785
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
786
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
787
+ &gosnmp.SnmpPacket{
788
+ Variables: []gosnmp.SnmpPDU{
789
+ {Name: "1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Cisco IOS XR Software (Cisco ASR9K Series), Version 4.2.3[Default] Copyright (c) 2013 by Cisco Systems, Inc.")},
790
+ },
791
+ }, nil,
792
+ )
793
+ },
794
+ expectedResult: map[string]ddsnmp.MetaTag{
795
+ "vendor": {Value: "Cisco", IsExactMatch: false},
796
+ "version": {Value: "4.2.3[Default]", IsExactMatch: false},
797
+ "model": {Value: "Cisco ASR9K Series", IsExactMatch: false},
798
+ "os_name": {Value: "IOSXR", IsExactMatch: false},
799
+ },
800
+ expectedError: false,
801
+ },
802
+ "base metadata exact via Extends": {
803
+ profile: &ddsnmp.Profile{
804
+ Definition: &ddprofiledefinition.ProfileDefinition{
805
+ Extends: []string{"1.3.6.1.4.1.9.1.669"},
806
+ Metadata: ddprofiledefinition.MetadataConfig{
807
+ "device": {
808
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
809
+ "vendor": {Value: "Cisco"},
810
+ "type": {Value: "Firewall"},
811
+ "model": {Value: "Generic"},
812
+ },
813
+ },
814
+ },
815
+ },
816
+ },
817
+ setupMock: func(m *snmpmock.MockHandler) {},
818
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
819
+ expectedResult: map[string]ddsnmp.MetaTag{
820
+ "vendor": {Value: "Cisco", IsExactMatch: true},
821
+ "type": {Value: "Firewall", IsExactMatch: true},
822
+ "model": {Value: "Generic", IsExactMatch: true},
823
+ },
824
+ expectedError: false,
825
+ },
826
+ "extends exact + sysobjectid wildcard → base wins overlap": {
827
+ profile: &ddsnmp.Profile{
828
+ Definition: &ddprofiledefinition.ProfileDefinition{
829
+ Extends: []string{"1.3.6.1.4.1.9.1.669"},
830
+ Metadata: ddprofiledefinition.MetadataConfig{
831
+ "device": {
832
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
833
+ "platform": {Value: "Enterprise-Exact"},
834
+ "series": {Value: "ASA-Exact"},
835
+ },
836
+ },
837
+ },
838
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
839
+ {
840
+ SysobjectID: "1.3.6.1.4.1.9.1.*", // wildcard match (not exact)
841
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
842
+ "platform": {Value: "Enterprise-Wildcard"},
843
+ "series": {Value: "ASA-Wildcard"},
844
+ "model": {Value: "ASA5510-Wildcard"},
845
+ },
846
+ },
847
+ },
848
+ },
849
+ },
850
+ setupMock: func(m *snmpmock.MockHandler) {},
851
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
852
+ expectedResult: map[string]ddsnmp.MetaTag{
853
+ // base exact overwrites wildcard for overlapping keys
854
+ "platform": {Value: "Enterprise-Exact", IsExactMatch: true},
855
+ "series": {Value: "ASA-Exact", IsExactMatch: true},
856
+ // non-overlapping key from wildcard remains
857
+ "model": {Value: "ASA5510-Wildcard", IsExactMatch: false},
858
+ },
859
+ expectedError: false,
860
+ },
861
+ "no extends (non-exact) + sysobjectid wildcard → wildcard wins": {
862
+ profile: &ddsnmp.Profile{
863
+ Definition: &ddprofiledefinition.ProfileDefinition{
864
+ Metadata: ddprofiledefinition.MetadataConfig{
865
+ "device": {
866
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
867
+ "platform": {Value: "Enterprise-Base"},
868
+ "series": {Value: "ASA-Base"},
869
+ },
870
+ },
871
+ },
872
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
873
+ {
874
+ SysobjectID: "1.3.6.1.4.1.9.1.*", // wildcard
875
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
876
+ "platform": {Value: "Enterprise-Wildcard"},
877
+ "series": {Value: "ASA-Wildcard"},
878
+ "model": {Value: "ASA5510"},
879
+ },
880
+ },
881
+ },
882
+ },
883
+ },
884
+ setupMock: func(m *snmpmock.MockHandler) {},
885
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
886
+ expectedResult: map[string]ddsnmp.MetaTag{
887
+ // wildcard (not exact) is applied first and base (non-exact) cannot overwrite
888
+ "platform": {Value: "Enterprise-Wildcard", IsExactMatch: false},
889
+ "series": {Value: "ASA-Wildcard", IsExactMatch: false},
890
+ "model": {Value: "ASA5510", IsExactMatch: false},
891
+ },
892
+ expectedError: false,
893
+ },
894
+ "extends exact + sysobjectid exact → sysobjectid wins overlap": {
895
+ profile: &ddsnmp.Profile{
896
+ Definition: &ddprofiledefinition.ProfileDefinition{
897
+ Extends: []string{"1.3.6.1.4.1.9.1.669"},
898
+ Metadata: ddprofiledefinition.MetadataConfig{
899
+ "device": {
900
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
901
+ "vendor": {Value: "Cisco-Base"},
902
+ "model": {Value: "BaseModel"},
903
+ },
904
+ },
905
+ },
906
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
907
+ {
908
+ SysobjectID: "1.3.6.1.4.1.9.1.669", // exact
909
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
910
+ "vendor": {Value: "Cisco-Exact"},
911
+ "model": {Value: "ASA5510"},
912
+ "series": {Value: "ASA5500"},
913
+ },
914
+ },
915
+ },
916
+ },
917
+ },
918
+ setupMock: func(m *snmpmock.MockHandler) {},
919
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
920
+ expectedResult: map[string]ddsnmp.MetaTag{
921
+ // sysobjectid exact (written first) holds; base exact does not overwrite exact
922
+ "vendor": {Value: "Cisco-Exact", IsExactMatch: true},
923
+ "model": {Value: "ASA5510", IsExactMatch: true},
924
+ "series": {Value: "ASA5500", IsExactMatch: true},
925
+ },
926
+ expectedError: false,
927
+ },
928
+ }
929
+
930
+ for name, tc := range tests {
931
+ t.Run(name, func(t *testing.T) {
932
+ ctrl := gomock.NewController(t)
933
+ defer ctrl.Finish()
934
+
935
+ mockHandler := snmpmock.NewMockHandler(ctrl)
936
+ tc.setupMock(mockHandler)
937
+
938
+ missingOIDs := make(map[string]bool)
939
+ collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New(), tc.sysobjectid)
940
+
941
+ result, err := collector.Collect(tc.profile)
942
+
943
+ if tc.expectedError {
944
+ assert.Error(t, err)
945
+ if tc.errorContains != "" {
946
+ assert.Contains(t, err.Error(), tc.errorContains)
947
+ }
948
+ } else {
949
+ assert.NoError(t, err)
950
+ }
951
+
952
+ assert.Equal(t, tc.expectedResult, result)
953
+ })
954
+ }
955
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_global_tags.go
new
+142
@@ -0,0 +1,142 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "fmt"
8
+ "slices"
9
+
10
+ "github.com/gosnmp/gosnmp"
11
+
12
+ "github.com/netdata/netdata/go/plugins/logger"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
15
+)
16
+
17
+// globalTagsCollector handles collection of profile-wide tags
18
+type globalTagsCollector struct {
19
+ snmpClient gosnmp.Handler
20
+ missingOIDs map[string]bool
21
+ log *logger.Logger
22
+ tagProc *globalTagProcessor
23
+}
24
+
25
+func newGlobalTagsCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *globalTagsCollector {
26
+ return &globalTagsCollector{
27
+ snmpClient: snmpClient,
28
+ missingOIDs: missingOIDs,
29
+ log: log,
30
+ tagProc: newGlobalTagProcessor(),
31
+ }
32
+}
33
+
34
+// Collect gathers all global tags from the profile
35
+func (gc *globalTagsCollector) Collect(prof *ddsnmp.Profile) (map[string]string, error) {
36
+ if len(prof.Definition.MetricTags) == 0 && len(prof.Definition.StaticTags) == 0 {
37
+ return nil, nil
38
+ }
39
+
40
+ tags := make(map[string]string)
41
+
42
+ gc.processStaticTags(prof.Definition.StaticTags, tags)
43
+
44
+ if err := gc.processDynamicTags(prof.Definition.MetricTags, tags); err != nil {
45
+ return ternary(len(tags) > 0, tags, nil), err
46
+ }
47
+
48
+ return tags, nil
49
+}
50
+
51
+func (gc *globalTagsCollector) processStaticTags(staticTags []string, globalTags map[string]string) {
52
+ ta := tagAdder{tags: globalTags}
53
+ ta.addTags(parseStaticTags(staticTags))
54
+}
55
+
56
+// processDynamicTags processes tags that require SNMP fetching
57
+func (gc *globalTagsCollector) processDynamicTags(metricTags []ddprofiledefinition.MetricTagConfig, globalTags map[string]string) error {
58
+ // Identify OIDs to collect
59
+ oids, missingOIDs := gc.identifyTagOIDs(metricTags)
60
+
61
+ if len(missingOIDs) > 0 {
62
+ gc.log.Debugf("global tags missing OIDs: %v", missingOIDs)
63
+ }
64
+
65
+ if len(oids) == 0 {
66
+ return nil
67
+ }
68
+
69
+ pdus, err := gc.fetchTagValues(oids)
70
+ if err != nil {
71
+ return fmt.Errorf("failed to fetch global tag values: %w", err)
72
+ }
73
+
74
+ // Collect each tag configuration
75
+ var errs []error
76
+ for _, tagCfg := range metricTags {
77
+ if tagCfg.Symbol.OID == "" {
78
+ continue
79
+ }
80
+
81
+ ta := tagAdder{tags: globalTags}
82
+
83
+ if err := gc.tagProc.processTag(tagCfg, pdus, ta); err != nil {
84
+ errs = append(errs, fmt.Errorf("failed to process tag value for '%s/%s': %w",
85
+ tagCfg.Tag, tagCfg.Symbol.Name, err))
86
+ continue
87
+ }
88
+ }
89
+
90
+ if len(errs) > 0 && len(globalTags) == 0 {
91
+ return fmt.Errorf("failed to process any global tags: %w", errors.Join(errs...))
92
+ }
93
+
94
+ return nil
95
+}
96
+
97
+func (gc *globalTagsCollector) identifyTagOIDs(metricTags []ddprofiledefinition.MetricTagConfig) ([]string, []string) {
98
+ var oids []string
99
+ var missingOIDs []string
100
+
101
+ for _, tagCfg := range metricTags {
102
+ if tagCfg.Symbol.OID == "" {
103
+ continue
104
+ }
105
+
106
+ oid := trimOID(tagCfg.Symbol.OID)
107
+ if gc.missingOIDs[oid] {
108
+ missingOIDs = append(missingOIDs, tagCfg.Symbol.OID)
109
+ continue
110
+ }
111
+
112
+ oids = append(oids, tagCfg.Symbol.OID)
113
+ }
114
+
115
+ // Sort and deduplicate
116
+ slices.Sort(oids)
117
+ oids = slices.Compact(oids)
118
+
119
+ return oids, missingOIDs
120
+}
121
+
122
+func (gc *globalTagsCollector) fetchTagValues(oids []string) (map[string]gosnmp.SnmpPDU, error) {
123
+ pdus := make(map[string]gosnmp.SnmpPDU)
124
+ maxOids := gc.snmpClient.MaxOids()
125
+
126
+ for chunk := range slices.Chunk(oids, maxOids) {
127
+ result, err := gc.snmpClient.Get(chunk)
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+
132
+ for _, pdu := range result.Variables {
133
+ if !isPduWithData(pdu) {
134
+ gc.missingOIDs[trimOID(pdu.Name)] = true
135
+ continue
136
+ }
137
+ pdus[trimOID(pdu.Name)] = pdu
138
+ }
139
+ }
140
+
141
+ return pdus, nil
142
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_global_tags_test.go
new
+406
@@ -0,0 +1,406 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ddsnmpcollector
4
+
5
+import (
6
+ "errors"
7
+ "testing"
8
+
9
+ "github.com/golang/mock/gomock"
10
+ "github.com/gosnmp/gosnmp"
11
+ "github.com/stretchr/testify/assert"
12
+
13
+ snmpmock "github.com/gosnmp/gosnmp/mocks"
14
+
15
+ "github.com/netdata/netdata/go/plugins/logger"
16
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
+)
19
+
20
+func TestGlobalTagsCollector_Collect(t *testing.T) {
21
+ tests := map[string]struct {
22
+ profile *ddsnmp.Profile
23
+ setupMock func(m *snmpmock.MockHandler)
24
+ expectedResult map[string]string
25
+ expectedError bool
26
+ errorContains string
27
+ }{
28
+ "no tags configured": {
29
+ profile: &ddsnmp.Profile{
30
+ Definition: &ddprofiledefinition.ProfileDefinition{
31
+ MetricTags: []ddprofiledefinition.MetricTagConfig{},
32
+ StaticTags: []string{},
33
+ },
34
+ },
35
+ setupMock: func(m *snmpmock.MockHandler) {},
36
+ expectedResult: nil,
37
+ expectedError: false,
38
+ },
39
+ "static tags only": {
40
+ profile: &ddsnmp.Profile{
41
+ Definition: &ddprofiledefinition.ProfileDefinition{
42
+ StaticTags: []string{
43
+ "environment:production",
44
+ "region:us-east-1",
45
+ "service:network",
46
+ },
47
+ },
48
+ },
49
+ setupMock: func(m *snmpmock.MockHandler) {},
50
+ expectedResult: map[string]string{
51
+ "environment": "production",
52
+ "region": "us-east-1",
53
+ "service": "network",
54
+ },
55
+ expectedError: false,
56
+ },
57
+ "dynamic tags only": {
58
+ profile: &ddsnmp.Profile{
59
+ Definition: &ddprofiledefinition.ProfileDefinition{
60
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
61
+ {
62
+ Tag: "device_vendor",
63
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
64
+ OID: "1.3.6.1.2.1.1.1.0",
65
+ Name: "sysDescr",
66
+ },
67
+ },
68
+ {
69
+ Tag: "location",
70
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
71
+ OID: "1.3.6.1.2.1.1.6.0",
72
+ Name: "sysLocation",
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ setupMock: func(m *snmpmock.MockHandler) {
79
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
80
+ m.EXPECT().Get(gomock.InAnyOrder([]string{
81
+ "1.3.6.1.2.1.1.1.0",
82
+ "1.3.6.1.2.1.1.6.0",
83
+ })).Return(
84
+ &gosnmp.SnmpPacket{
85
+ Variables: []gosnmp.SnmpPDU{
86
+ {
87
+ Name: "1.3.6.1.2.1.1.1.0",
88
+ Type: gosnmp.OctetString,
89
+ Value: []byte("Cisco IOS Software"),
90
+ },
91
+ {
92
+ Name: "1.3.6.1.2.1.1.6.0",
93
+ Type: gosnmp.OctetString,
94
+ Value: []byte("DataCenter-1"),
95
+ },
96
+ },
97
+ }, nil,
98
+ )
99
+ },
100
+ expectedResult: map[string]string{
101
+ "device_vendor": "Cisco IOS Software",
102
+ "location": "DataCenter-1",
103
+ },
104
+ expectedError: false,
105
+ },
106
+ "mixed static and dynamic tags": {
107
+ profile: &ddsnmp.Profile{
108
+ Definition: &ddprofiledefinition.ProfileDefinition{
109
+ StaticTags: []string{
110
+ "environment:production",
111
+ "managed:true",
112
+ },
113
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
114
+ {
115
+ Tag: "hostname",
116
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
117
+ OID: "1.3.6.1.2.1.1.5.0",
118
+ Name: "sysName",
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ setupMock: func(m *snmpmock.MockHandler) {
125
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
126
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
127
+ &gosnmp.SnmpPacket{
128
+ Variables: []gosnmp.SnmpPDU{
129
+ {
130
+ Name: "1.3.6.1.2.1.1.5.0",
131
+ Type: gosnmp.OctetString,
132
+ Value: []byte("router-01"),
133
+ },
134
+ },
135
+ }, nil,
136
+ )
137
+ },
138
+ expectedResult: map[string]string{
139
+ "environment": "production",
140
+ "managed": "true",
141
+ "hostname": "router-01",
142
+ },
143
+ expectedError: false,
144
+ },
145
+ "tag with mapping": {
146
+ profile: &ddsnmp.Profile{
147
+ Definition: &ddprofiledefinition.ProfileDefinition{
148
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
149
+ {
150
+ Tag: "device_type",
151
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
152
+ OID: "1.3.6.1.2.1.1.2.0",
153
+ Name: "sysObjectID",
154
+ },
155
+ Mapping: map[string]string{
156
+ "1.3.6.1.4.1.9.1.1": "router",
157
+ "1.3.6.1.4.1.9.1.2": "switch",
158
+ "1.3.6.1.4.1.9.1.3": "firewall",
159
+ },
160
+ },
161
+ },
162
+ },
163
+ },
164
+ setupMock: func(m *snmpmock.MockHandler) {
165
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
166
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
167
+ &gosnmp.SnmpPacket{
168
+ Variables: []gosnmp.SnmpPDU{
169
+ {
170
+ Name: "1.3.6.1.2.1.1.2.0",
171
+ Type: gosnmp.ObjectIdentifier,
172
+ Value: "1.3.6.1.4.1.9.1.2",
173
+ },
174
+ },
175
+ }, nil,
176
+ )
177
+ },
178
+ expectedResult: map[string]string{
179
+ "device_type": "switch",
180
+ },
181
+ expectedError: false,
182
+ },
183
+ "tag with pattern matching": {
184
+ profile: &ddsnmp.Profile{
185
+ Definition: &ddprofiledefinition.ProfileDefinition{
186
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
187
+ {
188
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
189
+ OID: "1.3.6.1.2.1.1.5.0",
190
+ Name: "sysName",
191
+ },
192
+ Pattern: mustCompileRegex(`(.*)-(.*)-(.*)`),
193
+ Tags: map[string]string{
194
+ "site": "$1",
195
+ "role": "$2",
196
+ "unit_num": "$3",
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ setupMock: func(m *snmpmock.MockHandler) {
203
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
204
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
205
+ &gosnmp.SnmpPacket{
206
+ Variables: []gosnmp.SnmpPDU{
207
+ {
208
+ Name: "1.3.6.1.2.1.1.5.0",
209
+ Type: gosnmp.OctetString,
210
+ Value: []byte("NYC-CORE-01"),
211
+ },
212
+ },
213
+ }, nil,
214
+ )
215
+ },
216
+ expectedResult: map[string]string{
217
+ "site": "NYC",
218
+ "role": "CORE",
219
+ "unit_num": "01",
220
+ },
221
+ expectedError: false,
222
+ },
223
+ "missing OID": {
224
+ profile: &ddsnmp.Profile{
225
+ Definition: &ddprofiledefinition.ProfileDefinition{
226
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
227
+ {
228
+ Tag: "hostname",
229
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
230
+ OID: "1.3.6.1.2.1.1.5.0",
231
+ Name: "sysName",
232
+ },
233
+ },
234
+ },
235
+ },
236
+ },
237
+ setupMock: func(m *snmpmock.MockHandler) {
238
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
239
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
240
+ &gosnmp.SnmpPacket{
241
+ Variables: []gosnmp.SnmpPDU{
242
+ {
243
+ Name: "1.3.6.1.2.1.1.5.0",
244
+ Type: gosnmp.NoSuchObject,
245
+ Value: nil,
246
+ },
247
+ },
248
+ }, nil,
249
+ )
250
+ },
251
+ expectedResult: map[string]string{},
252
+ expectedError: false,
253
+ },
254
+ "SNMP error": {
255
+ profile: &ddsnmp.Profile{
256
+ Definition: &ddprofiledefinition.ProfileDefinition{
257
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
258
+ {
259
+ Tag: "hostname",
260
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
261
+ OID: "1.3.6.1.2.1.1.5.0",
262
+ Name: "sysName",
263
+ },
264
+ },
265
+ },
266
+ },
267
+ },
268
+ setupMock: func(m *snmpmock.MockHandler) {
269
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
270
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
271
+ nil,
272
+ errors.New("SNMP timeout"),
273
+ )
274
+ },
275
+ expectedResult: nil,
276
+ expectedError: true,
277
+ errorContains: "failed to fetch global tag values",
278
+ },
279
+ "empty tag name falls back to symbol name": {
280
+ profile: &ddsnmp.Profile{
281
+ Definition: &ddprofiledefinition.ProfileDefinition{
282
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
283
+ {
284
+ Tag: "", // Empty tag name
285
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
286
+ OID: "1.3.6.1.2.1.1.5.0",
287
+ Name: "sysName",
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ setupMock: func(m *snmpmock.MockHandler) {
294
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
295
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
296
+ &gosnmp.SnmpPacket{
297
+ Variables: []gosnmp.SnmpPDU{
298
+ {
299
+ Name: "1.3.6.1.2.1.1.5.0",
300
+ Type: gosnmp.OctetString,
301
+ Value: []byte("router-01"),
302
+ },
303
+ },
304
+ }, nil,
305
+ )
306
+ },
307
+ expectedResult: map[string]string{
308
+ "sysName": "router-01",
309
+ },
310
+ expectedError: false,
311
+ },
312
+ "chunked requests": {
313
+ profile: &ddsnmp.Profile{
314
+ Definition: &ddprofiledefinition.ProfileDefinition{
315
+ MetricTags: []ddprofiledefinition.MetricTagConfig{
316
+ {
317
+ Tag: "tag1",
318
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
319
+ OID: "1.3.6.1.2.1.1.1.0",
320
+ Name: "oid1",
321
+ },
322
+ },
323
+ {
324
+ Tag: "tag2",
325
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
326
+ OID: "1.3.6.1.2.1.1.2.0",
327
+ Name: "oid2",
328
+ },
329
+ },
330
+ {
331
+ Tag: "tag3",
332
+ Symbol: ddprofiledefinition.SymbolConfigCompat{
333
+ OID: "1.3.6.1.2.1.1.3.0",
334
+ Name: "oid3",
335
+ },
336
+ },
337
+ },
338
+ },
339
+ },
340
+ setupMock: func(m *snmpmock.MockHandler) {
341
+ m.EXPECT().MaxOids().Return(2).AnyTimes() // Force chunking
342
+ // First chunk
343
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
344
+ &gosnmp.SnmpPacket{
345
+ Variables: []gosnmp.SnmpPDU{
346
+ {
347
+ Name: "1.3.6.1.2.1.1.1.0",
348
+ Type: gosnmp.OctetString,
349
+ Value: []byte("value1"),
350
+ },
351
+ {
352
+ Name: "1.3.6.1.2.1.1.2.0",
353
+ Type: gosnmp.OctetString,
354
+ Value: []byte("value2"),
355
+ },
356
+ },
357
+ }, nil,
358
+ )
359
+ // Second chunk
360
+ m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
361
+ &gosnmp.SnmpPacket{
362
+ Variables: []gosnmp.SnmpPDU{
363
+ {
364
+ Name: "1.3.6.1.2.1.1.3.0",
365
+ Type: gosnmp.OctetString,
366
+ Value: []byte("value3"),
367
+ },
368
+ },
369
+ }, nil,
370
+ )
371
+ },
372
+ expectedResult: map[string]string{
373
+ "tag1": "value1",
374
+ "tag2": "value2",
375
+ "tag3": "value3",
376
+ },
377
+ expectedError: false,
378
+ },
379
+ }
380
+
381
+ for name, tc := range tests {
382
+ t.Run(name, func(t *testing.T) {
383
+ ctrl := gomock.NewController(t)
384
+ defer ctrl.Finish()
385
+
386
+ mockHandler := snmpmock.NewMockHandler(ctrl)
387
+ tc.setupMock(mockHandler)
388
+
389
+ missingOIDs := make(map[string]bool)
390
+ collector := newGlobalTagsCollector(mockHandler, missingOIDs, logger.New())
391
+
392
+ result, err := collector.Collect(tc.profile)
393
+
394
+ if tc.expectedError {
395
+ assert.Error(t, err)
396
+ if tc.errorContains != "" {
397
+ assert.Contains(t, err.Error(), tc.errorContains)
398
+ }
399
+ } else {
400
+ assert.NoError(t, err)
401
+ }
402
+
403
+ assert.Equal(t, tc.expectedResult, result)
404
+ })
405
+ }
406
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta_test.go
deleted
-1469
@@ -1,1469 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package ddsnmpcollector
4
-
5
-import (
6
- "errors"
7
- "testing"
8
-
9
- "github.com/golang/mock/gomock"
10
- "github.com/gosnmp/gosnmp"
11
- "github.com/stretchr/testify/assert"
12
-
13
- snmpmock "github.com/gosnmp/gosnmp/mocks"
14
-
15
- "github.com/netdata/netdata/go/plugins/logger"
16
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
17
- "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
18
-)
19
-
20
-func TestGlobalTagsCollector_Collect(t *testing.T) {
21
- tests := map[string]struct {
22
- profile *ddsnmp.Profile
23
- setupMock func(m *snmpmock.MockHandler)
24
- expectedResult map[string]string
25
- expectedError bool
26
- errorContains string
27
- }{
28
- "no tags configured": {
29
- profile: &ddsnmp.Profile{
30
- Definition: &ddprofiledefinition.ProfileDefinition{
31
- MetricTags: []ddprofiledefinition.MetricTagConfig{},
32
- StaticTags: []string{},
33
- },
34
- },
35
- setupMock: func(m *snmpmock.MockHandler) {},
36
- expectedResult: nil,
37
- expectedError: false,
38
- },
39
- "static tags only": {
40
- profile: &ddsnmp.Profile{
41
- Definition: &ddprofiledefinition.ProfileDefinition{
42
- StaticTags: []string{
43
- "environment:production",
44
- "region:us-east-1",
45
- "service:network",
46
- },
47
- },
48
- },
49
- setupMock: func(m *snmpmock.MockHandler) {},
50
- expectedResult: map[string]string{
51
- "environment": "production",
52
- "region": "us-east-1",
53
- "service": "network",
54
- },
55
- expectedError: false,
56
- },
57
- "dynamic tags only": {
58
- profile: &ddsnmp.Profile{
59
- Definition: &ddprofiledefinition.ProfileDefinition{
60
- MetricTags: []ddprofiledefinition.MetricTagConfig{
61
- {
62
- Tag: "device_vendor",
63
- Symbol: ddprofiledefinition.SymbolConfigCompat{
64
- OID: "1.3.6.1.2.1.1.1.0",
65
- Name: "sysDescr",
66
- },
67
- },
68
- {
69
- Tag: "location",
70
- Symbol: ddprofiledefinition.SymbolConfigCompat{
71
- OID: "1.3.6.1.2.1.1.6.0",
72
- Name: "sysLocation",
73
- },
74
- },
75
- },
76
- },
77
- },
78
- setupMock: func(m *snmpmock.MockHandler) {
79
- m.EXPECT().MaxOids().Return(10).AnyTimes()
80
- m.EXPECT().Get(gomock.InAnyOrder([]string{
81
- "1.3.6.1.2.1.1.1.0",
82
- "1.3.6.1.2.1.1.6.0",
83
- })).Return(
84
- &gosnmp.SnmpPacket{
85
- Variables: []gosnmp.SnmpPDU{
86
- {
87
- Name: "1.3.6.1.2.1.1.1.0",
88
- Type: gosnmp.OctetString,
89
- Value: []byte("Cisco IOS Software"),
90
- },
91
- {
92
- Name: "1.3.6.1.2.1.1.6.0",
93
- Type: gosnmp.OctetString,
94
- Value: []byte("DataCenter-1"),
95
- },
96
- },
97
- }, nil,
98
- )
99
- },
100
- expectedResult: map[string]string{
101
- "device_vendor": "Cisco IOS Software",
102
- "location": "DataCenter-1",
103
- },
104
- expectedError: false,
105
- },
106
- "mixed static and dynamic tags": {
107
- profile: &ddsnmp.Profile{
108
- Definition: &ddprofiledefinition.ProfileDefinition{
109
- StaticTags: []string{
110
- "environment:production",
111
- "managed:true",
112
- },
113
- MetricTags: []ddprofiledefinition.MetricTagConfig{
114
- {
115
- Tag: "hostname",
116
- Symbol: ddprofiledefinition.SymbolConfigCompat{
117
- OID: "1.3.6.1.2.1.1.5.0",
118
- Name: "sysName",
119
- },
120
- },
121
- },
122
- },
123
- },
124
- setupMock: func(m *snmpmock.MockHandler) {
125
- m.EXPECT().MaxOids().Return(10).AnyTimes()
126
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
127
- &gosnmp.SnmpPacket{
128
- Variables: []gosnmp.SnmpPDU{
129
- {
130
- Name: "1.3.6.1.2.1.1.5.0",
131
- Type: gosnmp.OctetString,
132
- Value: []byte("router-01"),
133
- },
134
- },
135
- }, nil,
136
- )
137
- },
138
- expectedResult: map[string]string{
139
- "environment": "production",
140
- "managed": "true",
141
- "hostname": "router-01",
142
- },
143
- expectedError: false,
144
- },
145
- "tag with mapping": {
146
- profile: &ddsnmp.Profile{
147
- Definition: &ddprofiledefinition.ProfileDefinition{
148
- MetricTags: []ddprofiledefinition.MetricTagConfig{
149
- {
150
- Tag: "device_type",
151
- Symbol: ddprofiledefinition.SymbolConfigCompat{
152
- OID: "1.3.6.1.2.1.1.2.0",
153
- Name: "sysObjectID",
154
- },
155
- Mapping: map[string]string{
156
- "1.3.6.1.4.1.9.1.1": "router",
157
- "1.3.6.1.4.1.9.1.2": "switch",
158
- "1.3.6.1.4.1.9.1.3": "firewall",
159
- },
160
- },
161
- },
162
- },
163
- },
164
- setupMock: func(m *snmpmock.MockHandler) {
165
- m.EXPECT().MaxOids().Return(10).AnyTimes()
166
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.2.0"}).Return(
167
- &gosnmp.SnmpPacket{
168
- Variables: []gosnmp.SnmpPDU{
169
- {
170
- Name: "1.3.6.1.2.1.1.2.0",
171
- Type: gosnmp.ObjectIdentifier,
172
- Value: "1.3.6.1.4.1.9.1.2",
173
- },
174
- },
175
- }, nil,
176
- )
177
- },
178
- expectedResult: map[string]string{
179
- "device_type": "switch",
180
- },
181
- expectedError: false,
182
- },
183
- "tag with pattern matching": {
184
- profile: &ddsnmp.Profile{
185
- Definition: &ddprofiledefinition.ProfileDefinition{
186
- MetricTags: []ddprofiledefinition.MetricTagConfig{
187
- {
188
- Symbol: ddprofiledefinition.SymbolConfigCompat{
189
- OID: "1.3.6.1.2.1.1.5.0",
190
- Name: "sysName",
191
- },
192
- Pattern: mustCompileRegex(`(.*)-(.*)-(.*)`),
193
- Tags: map[string]string{
194
- "site": "$1",
195
- "role": "$2",
196
- "unit_num": "$3",
197
- },
198
- },
199
- },
200
- },
201
- },
202
- setupMock: func(m *snmpmock.MockHandler) {
203
- m.EXPECT().MaxOids().Return(10).AnyTimes()
204
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
205
- &gosnmp.SnmpPacket{
206
- Variables: []gosnmp.SnmpPDU{
207
- {
208
- Name: "1.3.6.1.2.1.1.5.0",
209
- Type: gosnmp.OctetString,
210
- Value: []byte("NYC-CORE-01"),
211
- },
212
- },
213
- }, nil,
214
- )
215
- },
216
- expectedResult: map[string]string{
217
- "site": "NYC",
218
- "role": "CORE",
219
- "unit_num": "01",
220
- },
221
- expectedError: false,
222
- },
223
- "missing OID": {
224
- profile: &ddsnmp.Profile{
225
- Definition: &ddprofiledefinition.ProfileDefinition{
226
- MetricTags: []ddprofiledefinition.MetricTagConfig{
227
- {
228
- Tag: "hostname",
229
- Symbol: ddprofiledefinition.SymbolConfigCompat{
230
- OID: "1.3.6.1.2.1.1.5.0",
231
- Name: "sysName",
232
- },
233
- },
234
- },
235
- },
236
- },
237
- setupMock: func(m *snmpmock.MockHandler) {
238
- m.EXPECT().MaxOids().Return(10).AnyTimes()
239
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
240
- &gosnmp.SnmpPacket{
241
- Variables: []gosnmp.SnmpPDU{
242
- {
243
- Name: "1.3.6.1.2.1.1.5.0",
244
- Type: gosnmp.NoSuchObject,
245
- Value: nil,
246
- },
247
- },
248
- }, nil,
249
- )
250
- },
251
- expectedResult: map[string]string{},
252
- expectedError: false,
253
- },
254
- "SNMP error": {
255
- profile: &ddsnmp.Profile{
256
- Definition: &ddprofiledefinition.ProfileDefinition{
257
- MetricTags: []ddprofiledefinition.MetricTagConfig{
258
- {
259
- Tag: "hostname",
260
- Symbol: ddprofiledefinition.SymbolConfigCompat{
261
- OID: "1.3.6.1.2.1.1.5.0",
262
- Name: "sysName",
263
- },
264
- },
265
- },
266
- },
267
- },
268
- setupMock: func(m *snmpmock.MockHandler) {
269
- m.EXPECT().MaxOids().Return(10).AnyTimes()
270
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
271
- nil,
272
- errors.New("SNMP timeout"),
273
- )
274
- },
275
- expectedResult: nil,
276
- expectedError: true,
277
- errorContains: "failed to fetch global tag values",
278
- },
279
- "empty tag name falls back to symbol name": {
280
- profile: &ddsnmp.Profile{
281
- Definition: &ddprofiledefinition.ProfileDefinition{
282
- MetricTags: []ddprofiledefinition.MetricTagConfig{
283
- {
284
- Tag: "", // Empty tag name
285
- Symbol: ddprofiledefinition.SymbolConfigCompat{
286
- OID: "1.3.6.1.2.1.1.5.0",
287
- Name: "sysName",
288
- },
289
- },
290
- },
291
- },
292
- },
293
- setupMock: func(m *snmpmock.MockHandler) {
294
- m.EXPECT().MaxOids().Return(10).AnyTimes()
295
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.5.0"}).Return(
296
- &gosnmp.SnmpPacket{
297
- Variables: []gosnmp.SnmpPDU{
298
- {
299
- Name: "1.3.6.1.2.1.1.5.0",
300
- Type: gosnmp.OctetString,
301
- Value: []byte("router-01"),
302
- },
303
- },
304
- }, nil,
305
- )
306
- },
307
- expectedResult: map[string]string{
308
- "sysName": "router-01",
309
- },
310
- expectedError: false,
311
- },
312
- "chunked requests": {
313
- profile: &ddsnmp.Profile{
314
- Definition: &ddprofiledefinition.ProfileDefinition{
315
- MetricTags: []ddprofiledefinition.MetricTagConfig{
316
- {
317
- Tag: "tag1",
318
- Symbol: ddprofiledefinition.SymbolConfigCompat{
319
- OID: "1.3.6.1.2.1.1.1.0",
320
- Name: "oid1",
321
- },
322
- },
323
- {
324
- Tag: "tag2",
325
- Symbol: ddprofiledefinition.SymbolConfigCompat{
326
- OID: "1.3.6.1.2.1.1.2.0",
327
- Name: "oid2",
328
- },
329
- },
330
- {
331
- Tag: "tag3",
332
- Symbol: ddprofiledefinition.SymbolConfigCompat{
333
- OID: "1.3.6.1.2.1.1.3.0",
334
- Name: "oid3",
335
- },
336
- },
337
- },
338
- },
339
- },
340
- setupMock: func(m *snmpmock.MockHandler) {
341
- m.EXPECT().MaxOids().Return(2).AnyTimes() // Force chunking
342
- // First chunk
343
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
344
- &gosnmp.SnmpPacket{
345
- Variables: []gosnmp.SnmpPDU{
346
- {
347
- Name: "1.3.6.1.2.1.1.1.0",
348
- Type: gosnmp.OctetString,
349
- Value: []byte("value1"),
350
- },
351
- {
352
- Name: "1.3.6.1.2.1.1.2.0",
353
- Type: gosnmp.OctetString,
354
- Value: []byte("value2"),
355
- },
356
- },
357
- }, nil,
358
- )
359
- // Second chunk
360
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
361
- &gosnmp.SnmpPacket{
362
- Variables: []gosnmp.SnmpPDU{
363
- {
364
- Name: "1.3.6.1.2.1.1.3.0",
365
- Type: gosnmp.OctetString,
366
- Value: []byte("value3"),
367
- },
368
- },
369
- }, nil,
370
- )
371
- },
372
- expectedResult: map[string]string{
373
- "tag1": "value1",
374
- "tag2": "value2",
375
- "tag3": "value3",
376
- },
377
- expectedError: false,
378
- },
379
- }
380
-
381
- for name, tc := range tests {
382
- t.Run(name, func(t *testing.T) {
383
- ctrl := gomock.NewController(t)
384
- defer ctrl.Finish()
385
-
386
- mockHandler := snmpmock.NewMockHandler(ctrl)
387
- tc.setupMock(mockHandler)
388
-
389
- missingOIDs := make(map[string]bool)
390
- collector := newGlobalTagsCollector(mockHandler, missingOIDs, logger.New())
391
-
392
- result, err := collector.Collect(tc.profile)
393
-
394
- if tc.expectedError {
395
- assert.Error(t, err)
396
- if tc.errorContains != "" {
397
- assert.Contains(t, err.Error(), tc.errorContains)
398
- }
399
- } else {
400
- assert.NoError(t, err)
401
- }
402
-
403
- assert.Equal(t, tc.expectedResult, result)
404
- })
405
- }
406
-}
407
-
408
-func TestDeviceMetadataCollector_Collect(t *testing.T) {
409
- tests := map[string]struct {
410
- profile *ddsnmp.Profile
411
- setupMock func(m *snmpmock.MockHandler)
412
- sysobjectid string
413
- expectedResult map[string]string
414
- expectedError bool
415
- errorContains string
416
- }{
417
- "no metadata configured": {
418
- profile: &ddsnmp.Profile{
419
- Definition: &ddprofiledefinition.ProfileDefinition{
420
- Metadata: ddprofiledefinition.MetadataConfig{},
421
- },
422
- },
423
- setupMock: func(m *snmpmock.MockHandler) {},
424
- expectedResult: nil,
425
- expectedError: false,
426
- },
427
- "static values only": {
428
- profile: &ddsnmp.Profile{
429
- Definition: &ddprofiledefinition.ProfileDefinition{
430
- Metadata: ddprofiledefinition.MetadataConfig{
431
- "device": ddprofiledefinition.MetadataResourceConfig{
432
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
433
- "vendor": {
434
- Value: "dell",
435
- },
436
- "type": {
437
- Value: "router",
438
- },
439
- "model": {
440
- Value: "PowerEdge R740",
441
- },
442
- },
443
- },
444
- },
445
- },
446
- },
447
- setupMock: func(m *snmpmock.MockHandler) {},
448
- expectedResult: map[string]string{
449
- "vendor": "dell",
450
- "type": "router",
451
- "model": "PowerEdge R740",
452
- },
453
- expectedError: false,
454
- },
455
- "dynamic values with single symbol": {
456
- profile: &ddsnmp.Profile{
457
- Definition: &ddprofiledefinition.ProfileDefinition{
458
- Metadata: ddprofiledefinition.MetadataConfig{
459
- "device": ddprofiledefinition.MetadataResourceConfig{
460
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
461
- "vendor": {
462
- Value: "dell",
463
- },
464
- "serial_number": {
465
- Symbol: ddprofiledefinition.SymbolConfig{
466
- OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
467
- Name: "chassisSerialNumber",
468
- },
469
- },
470
- "version": {
471
- Symbol: ddprofiledefinition.SymbolConfig{
472
- OID: "1.3.6.1.2.1.1.1.0",
473
- Name: "sysDescr",
474
- },
475
- },
476
- },
477
- },
478
- },
479
- },
480
- },
481
- setupMock: func(m *snmpmock.MockHandler) {
482
- m.EXPECT().MaxOids().Return(10).AnyTimes()
483
- m.EXPECT().Get(gomock.InAnyOrder([]string{
484
- "1.3.6.1.2.1.1.1.0",
485
- "1.3.6.1.4.1.674.10892.5.1.3.2.0",
486
- })).Return(
487
- &gosnmp.SnmpPacket{
488
- Variables: []gosnmp.SnmpPDU{
489
- {
490
- Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
491
- Type: gosnmp.OctetString,
492
- Value: []byte("ABC123XYZ"),
493
- },
494
- {
495
- Name: "1.3.6.1.2.1.1.1.0",
496
- Type: gosnmp.OctetString,
497
- Value: []byte("Dell EMC Networking OS10"),
498
- },
499
- },
500
- }, nil,
501
- )
502
- },
503
- expectedResult: map[string]string{
504
- "vendor": "dell",
505
- "serial_number": "ABC123XYZ",
506
- "version": "Dell EMC Networking OS10",
507
- },
508
- expectedError: false,
509
- },
510
- "multiple symbols fallback": {
511
- profile: &ddsnmp.Profile{
512
- Definition: &ddprofiledefinition.ProfileDefinition{
513
- Metadata: ddprofiledefinition.MetadataConfig{
514
- "device": ddprofiledefinition.MetadataResourceConfig{
515
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
516
- "serial_number": {
517
- Symbols: []ddprofiledefinition.SymbolConfig{
518
- {
519
- OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
520
- Name: "chassisSerialNumber",
521
- },
522
- {
523
- OID: "1.3.6.1.4.1.674.10892.5.1.3.3.0",
524
- Name: "backupSerialNumber",
525
- },
526
- },
527
- },
528
- },
529
- },
530
- },
531
- },
532
- },
533
- setupMock: func(m *snmpmock.MockHandler) {
534
- m.EXPECT().MaxOids().Return(10).AnyTimes()
535
- m.EXPECT().Get(gomock.InAnyOrder([]string{
536
- "1.3.6.1.4.1.674.10892.5.1.3.2.0",
537
- "1.3.6.1.4.1.674.10892.5.1.3.3.0",
538
- })).Return(
539
- &gosnmp.SnmpPacket{
540
- Variables: []gosnmp.SnmpPDU{
541
- {
542
- Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
543
- Type: gosnmp.NoSuchObject,
544
- Value: nil,
545
- },
546
- {
547
- Name: "1.3.6.1.4.1.674.10892.5.1.3.3.0",
548
- Type: gosnmp.OctetString,
549
- Value: []byte("BACKUP123"),
550
- },
551
- },
552
- }, nil,
553
- )
554
- },
555
- expectedResult: map[string]string{
556
- "serial_number": "BACKUP123",
557
- },
558
- expectedError: false,
559
- },
560
- "value with match_pattern": {
561
- profile: &ddsnmp.Profile{
562
- Definition: &ddprofiledefinition.ProfileDefinition{
563
- Metadata: ddprofiledefinition.MetadataConfig{
564
- "device": ddprofiledefinition.MetadataResourceConfig{
565
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
566
- "version": {
567
- Symbol: ddprofiledefinition.SymbolConfig{
568
- OID: "1.3.6.1.2.1.1.1.0",
569
- Name: "sysDescr",
570
- MatchPatternCompiled: mustCompileRegex(`Isilon OneFS v(\S+)`),
571
- MatchValue: "$1",
572
- },
573
- },
574
- },
575
- },
576
- },
577
- },
578
- },
579
- setupMock: func(m *snmpmock.MockHandler) {
580
- m.EXPECT().MaxOids().Return(10).AnyTimes()
581
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
582
- &gosnmp.SnmpPacket{
583
- Variables: []gosnmp.SnmpPDU{
584
- {
585
- Name: "1.3.6.1.2.1.1.1.0",
586
- Type: gosnmp.OctetString,
587
- Value: []byte("device-name-3 263829375 Isilon OneFS v8.2.0.0"),
588
- },
589
- },
590
- }, nil,
591
- )
592
- },
593
- expectedResult: map[string]string{
594
- "version": "8.2.0.0",
595
- },
596
- expectedError: false,
597
- },
598
- "value with extract_value": {
599
- profile: &ddsnmp.Profile{
600
- Definition: &ddprofiledefinition.ProfileDefinition{
601
- Metadata: ddprofiledefinition.MetadataConfig{
602
- "device": ddprofiledefinition.MetadataResourceConfig{
603
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
604
- "temperature": {
605
- Symbol: ddprofiledefinition.SymbolConfig{
606
- OID: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1",
607
- Name: "temperatureProbeReading",
608
- ExtractValueCompiled: mustCompileRegex(`(\d+)C`),
609
- },
610
- },
611
- },
612
- },
613
- },
614
- },
615
- },
616
- setupMock: func(m *snmpmock.MockHandler) {
617
- m.EXPECT().MaxOids().Return(10).AnyTimes()
618
- m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1"}).Return(
619
- &gosnmp.SnmpPacket{
620
- Variables: []gosnmp.SnmpPDU{
621
- {
622
- Name: "1.3.6.1.4.1.674.10892.5.4.200.10.1.2.1.3.1",
623
- Type: gosnmp.OctetString,
624
- Value: []byte("25C"),
625
- },
626
- },
627
- }, nil,
628
- )
629
- },
630
- expectedResult: map[string]string{
631
- "temperature": "25",
632
- },
633
- expectedError: false,
634
- },
635
- "value with mapping": {
636
- profile: &ddsnmp.Profile{
637
- Definition: &ddprofiledefinition.ProfileDefinition{
638
- Metadata: ddprofiledefinition.MetadataConfig{
639
- "device": ddprofiledefinition.MetadataResourceConfig{
640
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
641
- "status": {
642
- Symbol: ddprofiledefinition.SymbolConfig{
643
- OID: "1.3.6.1.4.1.674.10892.5.2.1.0",
644
- Name: "globalSystemStatus",
645
- Mapping: map[string]string{
646
- "1": "other",
647
- "2": "unknown",
648
- "3": "ok",
649
- "4": "nonCritical",
650
- "5": "critical",
651
- "6": "nonRecoverable",
652
- },
653
- },
654
- },
655
- },
656
- },
657
- },
658
- },
659
- },
660
- setupMock: func(m *snmpmock.MockHandler) {
661
- m.EXPECT().MaxOids().Return(10).AnyTimes()
662
- m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.2.1.0"}).Return(
663
- &gosnmp.SnmpPacket{
664
- Variables: []gosnmp.SnmpPDU{
665
- {
666
- Name: "1.3.6.1.4.1.674.10892.5.2.1.0",
667
- Type: gosnmp.Integer,
668
- Value: 3,
669
- },
670
- },
671
- }, nil,
672
- )
673
- },
674
- expectedResult: map[string]string{
675
- "status": "ok",
676
- },
677
- expectedError: false,
678
- },
679
- "format mac_address": {
680
- profile: &ddsnmp.Profile{
681
- Definition: &ddprofiledefinition.ProfileDefinition{
682
- Metadata: ddprofiledefinition.MetadataConfig{
683
- "device": ddprofiledefinition.MetadataResourceConfig{
684
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
685
- "mac_address": {
686
- Symbol: ddprofiledefinition.SymbolConfig{
687
- OID: "1.3.6.1.2.1.2.2.1.6.1",
688
- Name: "ifPhysAddress",
689
- Format: "mac_address",
690
- },
691
- },
692
- },
693
- },
694
- },
695
- },
696
- },
697
- setupMock: func(m *snmpmock.MockHandler) {
698
- m.EXPECT().MaxOids().Return(10).AnyTimes()
699
- m.EXPECT().Get([]string{"1.3.6.1.2.1.2.2.1.6.1"}).Return(
700
- &gosnmp.SnmpPacket{
701
- Variables: []gosnmp.SnmpPDU{
702
- {
703
- Name: "1.3.6.1.2.1.2.2.1.6.1",
704
- Type: gosnmp.OctetString,
705
- Value: []byte{0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF},
706
- },
707
- },
708
- }, nil,
709
- )
710
- },
711
- expectedResult: map[string]string{
712
- "mac_address": "00:50:56:AB:CD:EF",
713
- },
714
- expectedError: false,
715
- },
716
- "non-device resource ignored": {
717
- profile: &ddsnmp.Profile{
718
- Definition: &ddprofiledefinition.ProfileDefinition{
719
- Metadata: ddprofiledefinition.MetadataConfig{
720
- "interface": ddprofiledefinition.MetadataResourceConfig{
721
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
722
- "name": {
723
- Value: "eth0",
724
- },
725
- },
726
- },
727
- "device": ddprofiledefinition.MetadataResourceConfig{
728
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
729
- "vendor": {
730
- Value: "cisco",
731
- },
732
- },
733
- },
734
- },
735
- },
736
- },
737
- setupMock: func(m *snmpmock.MockHandler) {},
738
- expectedResult: map[string]string{
739
- "vendor": "cisco",
740
- },
741
- expectedError: false,
742
- },
743
- "SNMP error": {
744
- profile: &ddsnmp.Profile{
745
- Definition: &ddprofiledefinition.ProfileDefinition{
746
- Metadata: ddprofiledefinition.MetadataConfig{
747
- "device": ddprofiledefinition.MetadataResourceConfig{
748
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
749
- "serial_number": {
750
- Symbol: ddprofiledefinition.SymbolConfig{
751
- OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
752
- Name: "chassisSerialNumber",
753
- },
754
- },
755
- },
756
- },
757
- },
758
- },
759
- },
760
- setupMock: func(m *snmpmock.MockHandler) {
761
- m.EXPECT().MaxOids().Return(10).AnyTimes()
762
- m.EXPECT().Get([]string{"1.3.6.1.4.1.674.10892.5.1.3.2.0"}).Return(
763
- nil,
764
- errors.New("SNMP timeout"),
765
- )
766
- },
767
- expectedResult: nil,
768
- expectedError: true,
769
- errorContains: "failed to fetch metadata values",
770
- },
771
- "missing OID continues with other fields": {
772
- profile: &ddsnmp.Profile{
773
- Definition: &ddprofiledefinition.ProfileDefinition{
774
- Metadata: ddprofiledefinition.MetadataConfig{
775
- "device": ddprofiledefinition.MetadataResourceConfig{
776
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
777
- "vendor": {
778
- Value: "dell",
779
- },
780
- "serial_number": {
781
- Symbol: ddprofiledefinition.SymbolConfig{
782
- OID: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
783
- Name: "chassisSerialNumber",
784
- },
785
- },
786
- "model": {
787
- Symbol: ddprofiledefinition.SymbolConfig{
788
- OID: "1.3.6.1.4.1.674.10892.5.1.3.1.0",
789
- Name: "chassisModelName",
790
- },
791
- },
792
- },
793
- },
794
- },
795
- },
796
- },
797
- setupMock: func(m *snmpmock.MockHandler) {
798
- m.EXPECT().MaxOids().Return(10).AnyTimes()
799
- m.EXPECT().Get(gomock.InAnyOrder([]string{
800
- "1.3.6.1.4.1.674.10892.5.1.3.1.0",
801
- "1.3.6.1.4.1.674.10892.5.1.3.2.0",
802
- })).Return(
803
- &gosnmp.SnmpPacket{
804
- Variables: []gosnmp.SnmpPDU{
805
- {
806
- Name: "1.3.6.1.4.1.674.10892.5.1.3.2.0",
807
- Type: gosnmp.NoSuchObject,
808
- Value: nil,
809
- },
810
- {
811
- Name: "1.3.6.1.4.1.674.10892.5.1.3.1.0",
812
- Type: gosnmp.OctetString,
813
- Value: []byte("PowerEdge R740"),
814
- },
815
- },
816
- }, nil,
817
- )
818
- },
819
- expectedResult: map[string]string{
820
- "vendor": "dell",
821
- "model": "PowerEdge R740",
822
- },
823
- expectedError: false,
824
- },
825
- "chunked requests": {
826
- profile: &ddsnmp.Profile{
827
- Definition: &ddprofiledefinition.ProfileDefinition{
828
- Metadata: ddprofiledefinition.MetadataConfig{
829
- "device": ddprofiledefinition.MetadataResourceConfig{
830
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
831
- "field1": {
832
- Symbol: ddprofiledefinition.SymbolConfig{
833
- OID: "1.3.6.1.2.1.1.1.0",
834
- Name: "oid1",
835
- },
836
- },
837
- "field2": {
838
- Symbol: ddprofiledefinition.SymbolConfig{
839
- OID: "1.3.6.1.2.1.1.2.0",
840
- Name: "oid2",
841
- },
842
- },
843
- "field3": {
844
- Symbol: ddprofiledefinition.SymbolConfig{
845
- OID: "1.3.6.1.2.1.1.3.0",
846
- Name: "oid3",
847
- },
848
- },
849
- },
850
- },
851
- },
852
- },
853
- },
854
- setupMock: func(m *snmpmock.MockHandler) {
855
- m.EXPECT().MaxOids().Return(2).AnyTimes() // Force chunking
856
- // First chunk
857
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0"}).Return(
858
- &gosnmp.SnmpPacket{
859
- Variables: []gosnmp.SnmpPDU{
860
- {
861
- Name: "1.3.6.1.2.1.1.1.0",
862
- Type: gosnmp.OctetString,
863
- Value: []byte("value1"),
864
- },
865
- {
866
- Name: "1.3.6.1.2.1.1.2.0",
867
- Type: gosnmp.OctetString,
868
- Value: []byte("value2"),
869
- },
870
- },
871
- }, nil,
872
- )
873
- // Second chunk
874
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.3.0"}).Return(
875
- &gosnmp.SnmpPacket{
876
- Variables: []gosnmp.SnmpPDU{
877
- {
878
- Name: "1.3.6.1.2.1.1.3.0",
879
- Type: gosnmp.OctetString,
880
- Value: []byte("value3"),
881
- },
882
- },
883
- }, nil,
884
- )
885
- },
886
- expectedResult: map[string]string{
887
- "field1": "value1",
888
- "field2": "value2",
889
- "field3": "value3",
890
- },
891
- expectedError: false,
892
- },
893
- "sysobjectid metadata override - single match": {
894
- profile: &ddsnmp.Profile{
895
- Definition: &ddprofiledefinition.ProfileDefinition{
896
- Metadata: ddprofiledefinition.MetadataConfig{
897
- "device": ddprofiledefinition.MetadataResourceConfig{
898
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
899
- "vendor": {
900
- Value: "Cisco",
901
- },
902
- "type": {
903
- Value: "Firewall",
904
- },
905
- },
906
- },
907
- },
908
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
909
- {
910
- SysobjectID: "1.3.6.1.4.1.9.1.669",
911
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
912
- "model": {
913
- Value: "ASA5510",
914
- },
915
- "series": {
916
- Value: "ASA5500",
917
- },
918
- },
919
- },
920
- {
921
- SysobjectID: "1.3.6.1.4.1.9.1.670",
922
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
923
- "model": {
924
- Value: "ASA5520",
925
- },
926
- },
927
- },
928
- },
929
- },
930
- },
931
- setupMock: func(m *snmpmock.MockHandler) {},
932
- sysobjectid: "1.3.6.1.4.1.9.1.669",
933
- expectedResult: map[string]string{
934
- "vendor": "Cisco",
935
- "type": "Firewall",
936
- "model": "ASA5510",
937
- "series": "ASA5500",
938
- },
939
- expectedError: false,
940
- },
941
- "sysobjectid metadata override - multiple matches cascade": {
942
- profile: &ddsnmp.Profile{
943
- Definition: &ddprofiledefinition.ProfileDefinition{
944
- Metadata: ddprofiledefinition.MetadataConfig{
945
- "device": ddprofiledefinition.MetadataResourceConfig{
946
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
947
- "vendor": {
948
- Value: "Cisco",
949
- },
950
- "platform": {
951
- Value: "Default Platform", // Will be overridden
952
- },
953
- },
954
- },
955
- },
956
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
957
- {
958
- SysobjectID: "1.3.6.1.4.1.9.*", // All Cisco devices
959
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
960
- "platform": {
961
- Value: "Enterprise", // Overrides base metadata
962
- },
963
- "support": {
964
- Value: "Premium",
965
- },
966
- },
967
- },
968
- {
969
- SysobjectID: "1.3.6.1.4.1.9.1.*", // Cisco ASA family
970
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
971
- "type": {
972
- Value: "Firewall",
973
- },
974
- "series": {
975
- Value: "ASA",
976
- },
977
- "support": {
978
- Value: "Standard", // Won't override "Premium" - first wins
979
- },
980
- },
981
- },
982
- {
983
- SysobjectID: "1.3.6.1.4.1.9.1.669", // Specific model
984
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
985
- "model": {
986
- Value: "ASA5510",
987
- },
988
- "series": {
989
- Value: "ASA5500", // Won't override "ASA" - first wins
990
- },
991
- },
992
- },
993
- },
994
- },
995
- },
996
- setupMock: func(m *snmpmock.MockHandler) {},
997
- sysobjectid: "1.3.6.1.4.1.9.1.669",
998
- expectedResult: map[string]string{
999
- "vendor": "Cisco", // From base metadata (not overridden)
1000
- "platform": "Enterprise", // From first sysobjectid match (overrides base "Default Platform")
1001
- "support": "Premium", // From first sysobjectid match (second match can't override)
1002
- "type": "Firewall", // From second sysobjectid match
1003
- "series": "ASA", // From second match (third match can't override)
1004
- "model": "ASA5510", // From third sysobjectid match
1005
- },
1006
- expectedError: false,
1007
- },
1008
- "sysobjectid metadata with dynamic SNMP values": {
1009
- profile: &ddsnmp.Profile{
1010
- Definition: &ddprofiledefinition.ProfileDefinition{
1011
- Metadata: ddprofiledefinition.MetadataConfig{
1012
- "device": ddprofiledefinition.MetadataResourceConfig{
1013
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1014
- "vendor": {
1015
- Value: "Cisco",
1016
- },
1017
- },
1018
- },
1019
- },
1020
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1021
- {
1022
- SysobjectID: "1.3.6.1.4.1.9.1.669",
1023
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1024
- "model": {
1025
- Value: "ASA5510",
1026
- },
1027
- "firmware": {
1028
- Symbol: ddprofiledefinition.SymbolConfig{
1029
- OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1030
- Name: "ciscoImageVersion",
1031
- },
1032
- },
1033
- },
1034
- },
1035
- },
1036
- },
1037
- },
1038
- setupMock: func(m *snmpmock.MockHandler) {
1039
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1040
- m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1041
- &gosnmp.SnmpPacket{
1042
- Variables: []gosnmp.SnmpPDU{
1043
- {
1044
- Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1045
- Type: gosnmp.OctetString,
1046
- Value: []byte("9.2(4)"),
1047
- },
1048
- },
1049
- }, nil,
1050
- )
1051
- },
1052
- sysobjectid: "1.3.6.1.4.1.9.1.669",
1053
- expectedResult: map[string]string{
1054
- "vendor": "Cisco",
1055
- "model": "ASA5510",
1056
- "firmware": "9.2(4)",
1057
- },
1058
- expectedError: false,
1059
- },
1060
- "sysobjectid metadata no match": {
1061
- profile: &ddsnmp.Profile{
1062
- Definition: &ddprofiledefinition.ProfileDefinition{
1063
- Metadata: ddprofiledefinition.MetadataConfig{
1064
- "device": ddprofiledefinition.MetadataResourceConfig{
1065
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1066
- "vendor": {
1067
- Value: "Cisco",
1068
- },
1069
- "type": {
1070
- Value: "Firewall",
1071
- },
1072
- },
1073
- },
1074
- },
1075
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1076
- {
1077
- SysobjectID: "1.3.6.1.4.1.9.1.669",
1078
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1079
- "model": {
1080
- Value: "ASA5510",
1081
- },
1082
- },
1083
- },
1084
- },
1085
- },
1086
- },
1087
- setupMock: func(m *snmpmock.MockHandler) {},
1088
- sysobjectid: "1.3.6.1.4.1.9.1.700",
1089
- expectedResult: map[string]string{
1090
- "vendor": "Cisco",
1091
- "type": "Firewall",
1092
- // No model since sysobjectid doesn't match
1093
- },
1094
- expectedError: false,
1095
- },
1096
- "sysobjectid metadata with invalid regex pattern": {
1097
- profile: &ddsnmp.Profile{
1098
- Definition: &ddprofiledefinition.ProfileDefinition{
1099
- Metadata: ddprofiledefinition.MetadataConfig{
1100
- "device": ddprofiledefinition.MetadataResourceConfig{
1101
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1102
- "vendor": {
1103
- Value: "Cisco",
1104
- },
1105
- },
1106
- },
1107
- },
1108
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1109
- {
1110
- SysobjectID: "1.3.6.1.4.1.9.1.[invalid", // Invalid regex
1111
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1112
- "model": {
1113
- Value: "ASA5510",
1114
- },
1115
- },
1116
- },
1117
- {
1118
- SysobjectID: "1.3.6.1.4.1.9.1.669", // Valid entry
1119
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1120
- "series": {
1121
- Value: "ASA5500",
1122
- },
1123
- },
1124
- },
1125
- },
1126
- },
1127
- },
1128
- setupMock: func(m *snmpmock.MockHandler) {},
1129
- sysobjectid: "1.3.6.1.4.1.9.1.669",
1130
- expectedResult: map[string]string{
1131
- "vendor": "Cisco",
1132
- "series": "ASA5500", // Valid entry still processes
1133
- },
1134
- expectedError: false,
1135
- },
1136
- "sysobjectid metadata overrides base metadata field": {
1137
- profile: &ddsnmp.Profile{
1138
- Definition: &ddprofiledefinition.ProfileDefinition{
1139
- Metadata: ddprofiledefinition.MetadataConfig{
1140
- "device": ddprofiledefinition.MetadataResourceConfig{
1141
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1142
- "vendor": {
1143
- Value: "Generic",
1144
- },
1145
- "type": {
1146
- Value: "Unknown",
1147
- },
1148
- "model": {
1149
- Value: "Generic Model",
1150
- },
1151
- },
1152
- },
1153
- },
1154
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1155
- {
1156
- SysobjectID: "1.3.6.1.4.1.9.1.669",
1157
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1158
- "vendor": {
1159
- Value: "Cisco Systems", // Override generic vendor
1160
- },
1161
- "model": {
1162
- Value: "ASA5510", // Override generic model
1163
- },
1164
- },
1165
- },
1166
- },
1167
- },
1168
- },
1169
- setupMock: func(m *snmpmock.MockHandler) {},
1170
- sysobjectid: "1.3.6.1.4.1.9.1.669",
1171
- expectedResult: map[string]string{
1172
- "vendor": "Cisco Systems", // Overridden
1173
- "type": "Unknown", // Not overridden
1174
- "model": "ASA5510", // Overridden
1175
- },
1176
- expectedError: false,
1177
- },
1178
- "sysobjectid metadata with SNMP fetch error continues": {
1179
- profile: &ddsnmp.Profile{
1180
- Definition: &ddprofiledefinition.ProfileDefinition{
1181
- Metadata: ddprofiledefinition.MetadataConfig{
1182
- "device": ddprofiledefinition.MetadataResourceConfig{
1183
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1184
- "vendor": {
1185
- Value: "Cisco",
1186
- },
1187
- },
1188
- },
1189
- },
1190
- SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1191
- {
1192
- SysobjectID: "1.3.6.1.4.1.9.1.669",
1193
- Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1194
- "model": {
1195
- Value: "ASA5510",
1196
- },
1197
- "firmware": {
1198
- Symbol: ddprofiledefinition.SymbolConfig{
1199
- OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1200
- Name: "ciscoImageVersion",
1201
- },
1202
- },
1203
- },
1204
- },
1205
- },
1206
- },
1207
- },
1208
- setupMock: func(m *snmpmock.MockHandler) {
1209
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1210
- m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1211
- nil,
1212
- errors.New("SNMP timeout"),
1213
- )
1214
- },
1215
- sysobjectid: "1.3.6.1.4.1.9.1.669",
1216
- expectedResult: map[string]string{
1217
- "vendor": "Cisco",
1218
- "model": "ASA5510", // Static value still applied despite SNMP error
1219
- },
1220
- expectedError: false, // Should continue with partial data
1221
- },
1222
- "os_name with multiple symbols and match_pattern fallback": {
1223
- profile: &ddsnmp.Profile{
1224
- Definition: &ddprofiledefinition.ProfileDefinition{
1225
- Metadata: ddprofiledefinition.MetadataConfig{
1226
- "device": ddprofiledefinition.MetadataResourceConfig{
1227
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1228
- "vendor": {
1229
- Value: "Cisco",
1230
- },
1231
- "os_name": {
1232
- Symbols: []ddprofiledefinition.SymbolConfig{
1233
- {
1234
- OID: "1.3.6.1.2.1.1.1.0",
1235
- Name: "sysDescr",
1236
- MatchPatternCompiled: mustCompileRegex(`Cisco Internetwork Operating System Software`),
1237
- MatchValue: "IOS",
1238
- },
1239
- {
1240
- OID: "1.3.6.1.2.1.1.1.0",
1241
- Name: "sysDescr",
1242
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`),
1243
- MatchValue: "IOS",
1244
- },
1245
- {
1246
- OID: "1.3.6.1.2.1.1.1.0",
1247
- Name: "sysDescr",
1248
- MatchPatternCompiled: mustCompileRegex(`Cisco NX-OS`),
1249
- MatchValue: "NXOS",
1250
- },
1251
- {
1252
- OID: "1.3.6.1.2.1.1.1.0",
1253
- Name: "sysDescr",
1254
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS XR`),
1255
- MatchValue: "IOSXR",
1256
- },
1257
- },
1258
- },
1259
- },
1260
- },
1261
- },
1262
- },
1263
- },
1264
- setupMock: func(m *snmpmock.MockHandler) {
1265
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1266
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
1267
- &gosnmp.SnmpPacket{
1268
- Variables: []gosnmp.SnmpPDU{
1269
- {
1270
- Name: "1.3.6.1.2.1.1.1.0",
1271
- Type: gosnmp.OctetString,
1272
- Value: []byte("Cisco NX-OS(tm) m9100, Software (m9100-s2ek9-mz), Version 4.1(1c), RELEASE SOFTWARE Copyright (c) 2002-2008 by Cisco Systems, Inc. Compiled 11/24/2008 18:00:00"),
1273
- },
1274
- },
1275
- }, nil,
1276
- )
1277
- },
1278
- expectedResult: map[string]string{
1279
- "vendor": "Cisco",
1280
- "os_name": "NXOS", // Should match the third pattern and use its match_value
1281
- },
1282
- expectedError: false,
1283
- },
1284
- "os_name with multiple symbols - no match fallback": {
1285
- profile: &ddsnmp.Profile{
1286
- Definition: &ddprofiledefinition.ProfileDefinition{
1287
- Metadata: ddprofiledefinition.MetadataConfig{
1288
- "device": ddprofiledefinition.MetadataResourceConfig{
1289
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1290
- "vendor": {
1291
- Value: "Juniper",
1292
- },
1293
- "os_name": {
1294
- Symbols: []ddprofiledefinition.SymbolConfig{
1295
- {
1296
- OID: "1.3.6.1.2.1.1.1.0",
1297
- Name: "sysDescr",
1298
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`),
1299
- MatchValue: "IOS",
1300
- },
1301
- {
1302
- OID: "1.3.6.1.2.1.1.1.0",
1303
- Name: "sysDescr",
1304
- MatchPatternCompiled: mustCompileRegex(`Cisco NX-OS`),
1305
- MatchValue: "NXOS",
1306
- },
1307
- },
1308
- },
1309
- },
1310
- },
1311
- },
1312
- },
1313
- },
1314
- setupMock: func(m *snmpmock.MockHandler) {
1315
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1316
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
1317
- &gosnmp.SnmpPacket{
1318
- Variables: []gosnmp.SnmpPDU{
1319
- {
1320
- Name: "1.3.6.1.2.1.1.1.0",
1321
- Type: gosnmp.OctetString,
1322
- Value: []byte("Juniper Networks, Inc. mx960 internet router, kernel JUNOS 12.3R3.4"),
1323
- },
1324
- },
1325
- }, nil,
1326
- )
1327
- },
1328
- expectedResult: map[string]string{
1329
- "vendor": "Juniper",
1330
- // os_name should not be set since no patterns match
1331
- },
1332
- expectedError: false,
1333
- },
1334
- "os_name with single symbol and match_pattern - no match": {
1335
- profile: &ddsnmp.Profile{
1336
- Definition: &ddprofiledefinition.ProfileDefinition{
1337
- Metadata: ddprofiledefinition.MetadataConfig{
1338
- "device": ddprofiledefinition.MetadataResourceConfig{
1339
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1340
- "vendor": {
1341
- Value: "Generic",
1342
- },
1343
- "os_name": {
1344
- Symbol: ddprofiledefinition.SymbolConfig{
1345
- OID: "1.3.6.1.2.1.1.1.0",
1346
- Name: "sysDescr",
1347
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`),
1348
- MatchValue: "IOS",
1349
- },
1350
- },
1351
- },
1352
- },
1353
- },
1354
- },
1355
- },
1356
- setupMock: func(m *snmpmock.MockHandler) {
1357
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1358
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
1359
- &gosnmp.SnmpPacket{
1360
- Variables: []gosnmp.SnmpPDU{
1361
- {
1362
- Name: "1.3.6.1.2.1.1.1.0",
1363
- Type: gosnmp.OctetString,
1364
- Value: []byte("Some other device description"),
1365
- },
1366
- },
1367
- }, nil,
1368
- )
1369
- },
1370
- expectedResult: map[string]string{
1371
- "vendor": "Generic",
1372
- // os_name should not be set since pattern doesn't match
1373
- },
1374
- expectedError: false,
1375
- },
1376
- "mixed fields with match_pattern and extract_value": {
1377
- profile: &ddsnmp.Profile{
1378
- Definition: &ddprofiledefinition.ProfileDefinition{
1379
- Metadata: ddprofiledefinition.MetadataConfig{
1380
- "device": ddprofiledefinition.MetadataResourceConfig{
1381
- Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1382
- "vendor": {
1383
- Value: "Cisco",
1384
- },
1385
- "version": {
1386
- Symbol: ddprofiledefinition.SymbolConfig{
1387
- OID: "1.3.6.1.2.1.1.1.0",
1388
- Name: "sysDescr",
1389
- ExtractValueCompiled: mustCompileRegex(`Version\s+([a-zA-Z0-9.()\[\]]+)`),
1390
- },
1391
- },
1392
- "model": {
1393
- Symbol: ddprofiledefinition.SymbolConfig{
1394
- OID: "1.3.6.1.2.1.1.1.0",
1395
- Name: "sysDescr",
1396
- ExtractValueCompiled: mustCompileRegex(`Software\s+\(([-a-zA-Z0-9_ ]+)\)`),
1397
- },
1398
- },
1399
- "os_name": {
1400
- Symbols: []ddprofiledefinition.SymbolConfig{
1401
- {
1402
- OID: "1.3.6.1.2.1.1.1.0",
1403
- Name: "sysDescr",
1404
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS Software`),
1405
- MatchValue: "IOS",
1406
- },
1407
- {
1408
- OID: "1.3.6.1.2.1.1.1.0",
1409
- Name: "sysDescr",
1410
- MatchPatternCompiled: mustCompileRegex(`Cisco IOS XR Software`),
1411
- MatchValue: "IOSXR",
1412
- },
1413
- },
1414
- },
1415
- },
1416
- },
1417
- },
1418
- },
1419
- },
1420
- setupMock: func(m *snmpmock.MockHandler) {
1421
- m.EXPECT().MaxOids().Return(10).AnyTimes()
1422
- m.EXPECT().Get([]string{"1.3.6.1.2.1.1.1.0"}).Return(
1423
- &gosnmp.SnmpPacket{
1424
- Variables: []gosnmp.SnmpPDU{
1425
- {
1426
- Name: "1.3.6.1.2.1.1.1.0",
1427
- Type: gosnmp.OctetString,
1428
- Value: []byte("Cisco IOS XR Software (Cisco ASR9K Series), Version 4.2.3[Default] Copyright (c) 2013 by Cisco Systems, Inc."),
1429
- },
1430
- },
1431
- }, nil,
1432
- )
1433
- },
1434
- expectedResult: map[string]string{
1435
- "vendor": "Cisco",
1436
- "version": "4.2.3[Default]", // Extracted using extract_value
1437
- "model": "Cisco ASR9K Series", // Extracted using extract_value
1438
- "os_name": "IOSXR", // Matched second pattern
1439
- },
1440
- expectedError: false,
1441
- },
1442
- }
1443
-
1444
- for name, tc := range tests {
1445
- t.Run(name, func(t *testing.T) {
1446
- ctrl := gomock.NewController(t)
1447
- defer ctrl.Finish()
1448
-
1449
- mockHandler := snmpmock.NewMockHandler(ctrl)
1450
- tc.setupMock(mockHandler)
1451
-
1452
- missingOIDs := make(map[string]bool)
1453
- collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New(), tc.sysobjectid)
1454
-
1455
- result, err := collector.Collect(tc.profile)
1456
-
1457
- if tc.expectedError {
1458
- assert.Error(t, err)
1459
- if tc.errorContains != "" {
1460
- assert.Contains(t, err.Error(), tc.errorContains)
1461
- }
1462
- } else {
1463
- assert.NoError(t, err)
1464
- }
1465
-
1466
- assert.Equal(t, tc.expectedResult, result)
1467
- })
1468
- }
1469
-}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/tag_processor.go
+9
-3
@@ -12,9 +12,15 @@ type tagAdder struct {
12
tags map[string]string
13
}
14
15
-func (m *tagAdder) addTag(key, value string) {
16
- if existing, ok := m.tags[key]; !ok || existing == "" {
17
- m.tags[key] = value
15
+func (ta *tagAdder) addTags(tags map[string]string) {
16
+ for k, v := range tags {
17
+ ta.addTag(k, v)
18
+ }
19
+}
20
+
21
+func (ta *tagAdder) addTag(key, value string) {
22
+ if existing, ok := ta.tags[key]; !ok || existing == "" {
23
+ ta.tags[key] = value
24
}
25
}
26
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/utils.go
+4
-5
@@ -12,6 +12,7 @@ import (
12
13
"github.com/gosnmp/gosnmp"
14
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
16
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
17
)
18
@@ -214,10 +215,8 @@ func isInt(s string) bool {
215
return err == nil
216
}
217
217
-func mergeTagsIfAbsent(dest, src map[string]string) {
218
- for k, v := range src {
219
- if existing, ok := dest[k]; !ok || existing == "" {
220
- dest[k] = v
221
- }
218
+func mergeMetaTagIfAbsent(dest map[string]ddsnmp.MetaTag, key string, tag ddsnmp.MetaTag) {
219
+ if existing, ok := dest[key]; !ok || existing.Value == "" || (!existing.IsExactMatch && tag.IsExactMatch) {
220
+ dest[key] = tag
221
}
222
}
src/go/plugin/go.d/collector/snmp/ddsnmp/metric.go
+6
-1
@@ -6,7 +6,7 @@ import (
6
7
type ProfileMetrics struct {
8
Source string
9
- DeviceMetadata map[string]string
9
+ DeviceMetadata map[string]MetaTag
10
Tags map[string]string
11
Metrics []Metric
12
}
@@ -26,3 +26,8 @@ type Metric struct {
26
27
IsTable bool
28
}
29
+
30
+type MetaTag struct {
31
+ Value string
32
+ IsExactMatch bool // whether this value is from an exact match context
33
+}
src/go/plugin/go.d/config/go.d/snmp.profiles/meta_overrides.yaml
new
+172
@@ -0,0 +1,172 @@
1
+enterprisenumbers:
2
+ # Map PEN org names -> canonical vendor names
3
+ # (used to derive vendor from the PEN database, not from sysobjectids.json)
4
+ org_to_vendor:
5
+ "Alcatel-Lucent TMC (formerly 'Alcatel SOC')": "Alcatel-Lucent"
6
+ "allied networks GmbH": "Allied"
7
+ "Allied Data Technologies": "Allied"
8
+ "Allied Telesis, Inc.": "Allied"
9
+ "American Power Conversion Corp.": "APC"
10
+ "Arista Networks, Inc. (formerly 'Arastra, Inc.')": "Arista"
11
+ "Aruba PEC S.p.A.": "Aruba"
12
+ "Aruba S.r.l.": "Aruba"
13
+ "Aruba, a Hewlett Packard Enterprise company": "Aruba"
14
+ "AVAYA": "Avaya"
15
+ "Avaya Atlanta Lab": "Avaya"
16
+ "Avaya Communication": "Avaya"
17
+ "Barracuda Networks AG (previous was 'phion Information Technologies')": "Barracuda"
18
+ "Barracuda Networks, Inc.": "Barracuda"
19
+ "barracuda digitale agentur GmbH": "Barracuda"
20
+ "Blade Network Technologies, Inc.": "IBM"
21
+ "Brocade Communication Systems, Inc. (formerly 'Foundry Networks, Inc.')": "Brocade"
22
+ "Brocade Communication Systems, Inc. (formerly 'Rhapsody Networks Inc.')": "Brocade"
23
+ "Brocade Communications Systems, Inc.": "Brocade"
24
+ "Brocade Communications Systems, Inc. (formerly 'McDATA Corp.')": "Brocade"
25
+ "Brocade Communications Systems, Inc. (formerly 'McDATA Corporation')": "Brocade"
26
+ "Brocade Communications Systems, Inc. (formerly 'McDATA,Inc')": "Brocade"
27
+ "Brocade Communications Systems, Inc. (formerly 'NuView Inc.')": "Brocade"
28
+ "CIENA Corporation (formerly 'ONI Systems Corp.')": "Ciena"
29
+ "Ciena (formerly 'Akara Inc.')": "Ciena"
30
+ "Ciena Corporation": "Ciena"
31
+ "Ciena Corporation (formerly 'Catena Networks')": "Ciena"
32
+ "Cisco Flex Platform": "Cisco"
33
+ "Cisco Sera": "Cisco"
34
+ "Cisco SolutionsLab": "Cisco"
35
+ "Cisco Sytems, Inc.": "Cisco"
36
+ "Cisco Systems": "Cisco"
37
+ "Cisco Systems Inc": "Cisco"
38
+ "Cisco Systems India Private Limited": "Cisco"
39
+ "Cisco Systems, Inc.": "Cisco"
40
+ "Cisco Systems, Inc. (formerly 'Arch Rock Corporation')": "Cisco"
41
+ "ciscoSystems": "Cisco"
42
+ "Citrix Systems Inc.": "Citrix"
43
+ "Dialogic Corporation": "Dialogic"
44
+ "D-Link Systems, Inc.": "D-Link"
45
+ "Dell Inc.": "Dell"
46
+ "Eaton Energy Automation Solutions (EAS) Division": "Eaton"
47
+ "EATON Wireless": "Eaton"
48
+ "Ericsson AB": "Ericsson"
49
+ "Ericsson AB - 4G5G (formerly 'Ellemtel Telecommunication Systems Laboratories')": "Ericsson"
50
+ "Ericsson AB - Packet Core Networks": "Ericsson"
51
+ "Ericsson Ahead Communications Systems GmbH": "Ericsson"
52
+ "Ericsson Communications Ltd.": "Ericsson"
53
+ "Ericsson Denmark A/S, Telebit Division": "Ericsson"
54
+ "Ericsson Inc. (formerly 'BelAir Networks')": "Ericsson"
55
+ "Ericsson Mobile Platforms AB": "Ericsson"
56
+ "Ericsson Nikola Tesla d.d.": "Ericsson"
57
+ "Ericsson Research Montreal (LMC)": "Ericsson"
58
+ "Ericsson Wireless LAN Systems": "Ericsson"
59
+ "ERICSSON FIBER ACCESS": "Ericsson"
60
+ "Extreme Networks": "Extreme"
61
+ "Extreme Networks (formerly 'Ipanema Technologies')": "Extreme"
62
+ "F5 Labs, Inc.": "F5"
63
+ "F5 Networks Inc": "F5"
64
+ "Fortinet, Inc.": "Fortinet"
65
+ "Fortinet. Inc.": "Fortinet"
66
+ "Gigamon Systems LLC": "Gigamon"
67
+ "Hewlett-Packard": "HP"
68
+ "Hewlett-Packard (Schweiz) GmbH": "HP"
69
+ "Hewlett-Packard Slovakia": "HP"
70
+ "Hewlett Packard Enterprise": "HPE"
71
+ "HUAWEI Technology Co.,Ltd": "Huawei"
72
+ "Huawei Symantec Technologies Co.,Ltd": "Huawei"
73
+ "Infinera Corp.": "Infinera"
74
+ "InfoBlox Inc.": "Infoblox"
75
+ "Infoblox, WinConnect (formerly 'Ipanto')": "Infoblox"
76
+ "Juniper Financial Corp.": "Juniper"
77
+ "Juniper Networks, Inc.": "Juniper"
78
+ "Juniper Networks/Funk Software": "Juniper"
79
+ "Juniper Networks/Unisphere": "Juniper"
80
+ "KYOCERA Corporation": "Kyocera"
81
+ "Kyocera Communication Systems Co.Ltd": "Kyocera"
82
+ "McAFee Associates Inc.": "McAfee"
83
+ "McAfee (formerly 'Secure Computing Corporation')": "McAfee"
84
+ "McAfee Inc. (formerly 'Network Associates, Inc.')": "McAfee"
85
+ "McAfee, Inc. (formerly 'Securify, Inc.')": "McAfee"
86
+ "McAfee Inc. (formerly 'Reconnex Corporation')": "McAfee"
87
+ "McAfee, LLC": "McAfee"
88
+ "Meraki Networks, Inc.": "Meraki"
89
+ "Nasuni Corporation": "Nasuni"
90
+ "NEC Corporation": "NEC"
91
+ "NEC Eluminant Technologies, Inc.": "NEC"
92
+ "NEC Platforms, Ltd.": "NEC"
93
+ "NEC Telenetworx,Ltd": "NEC"
94
+ "NEC COMPUTERS INTERNATIONAL B.V.": "NEC"
95
+ "NEC informatec systems,ltd.": "NEC"
96
+ "NEC Electronics Corporation": "NEC"
97
+ "NEC Unified Solutions": "NEC"
98
+ "NEC Enterprise Communication Technologies": "NEC"
99
+ "Network Appliance Corporation": "NetApp"
100
+ "Nokia (formerly 'Alcatel-Lucent')": "Nokia"
101
+ "Nokia (formerly 'Novarra, Inc.')": "Nokia"
102
+ "Nokia Distributed Access": "Nokia"
103
+ "Nokia Networks (formerly 'Nokia Siemens Networks')": "Nokia"
104
+ "Nokia Shanghai Bell": "Nokia"
105
+ "NVIDIA Corporation": "NVIDIA"
106
+ "PALO ALTO NETWORKS": "Palo Alto"
107
+ "Palo Alto Research Center, Inc.": "Palo Alto"
108
+ "Palo Alto Software, Inc.": "Palo Alto"
109
+ "Ruckus Wireless, Inc.": "Ruckus"
110
+ "SINETICA": "Panduit"
111
+ "Sophos Plc": "Sophos"
112
+ "SVTO Hewlett-Packard": "HP"
113
+ "Synology Inc.": "Synology"
114
+ "Tejas Networks": "Tejas"
115
+ "TP-Link Systems Inc.": "TP-Link"
116
+ "Ubiquiti Networks, Inc.": "Ubiquiti"
117
+ "Velocloud Networks, Inc.": "VeloCloud"
118
+ "Vertiv (formerly 'Emerson Computer Power')": "Vertiv"
119
+ "Vertiv (formerly 'Emerson Energy Systems')": "Vertiv"
120
+ "Vertiv Tech Co.,Ltd. (formerly 'Emerson Network Power Co.,Ltd.')": "Vertiv"
121
+ "Vertiv (formerly 'Geist Manufacturing, Inc')": "Vertiv"
122
+ "Vertiv Co": "Vertiv"
123
+ "VMware Inc.": "VMware"
124
+ "WatchGuard Technologies Inc.": "WatchGuard"
125
+ "Western Digital Corporation": "Western Digital"
126
+ "Yokogawa-Hewlett-Packard": "HP"
127
+ "Zebra Technologies Corporation": "Zebra"
128
+ "ZyXEL Communications Corp.": "Zyxel"
129
+
130
+sysobjectids:
131
+ # Exact OID overrides (create-or-update)
132
+ oid_overrides:
133
+ # https://mibs.observium.org/mib/NET-SNMP-TC/
134
+ "1.3.6.1.4.1.8072.3.2.8":
135
+ category: Server
136
+ model: "FreeBSD"
137
+ "1.3.6.1.4.1.8072.3.2.10":
138
+ category: Server
139
+ model: "Linux"
140
+ "1.3.6.1.4.1.8072.3.2.16":
141
+ category: Server
142
+ model: "macOS"
143
+
144
+ "1.3.6.1.4.1.26543.1.7.7":
145
+ category: Switch
146
+ model: RackSwitch G8052
147
+ "1.3.6.1.4.1.6889.1.69.4.3":
148
+ category: IP Phone
149
+ model: AVX571040
150
+ "1.3.6.1.4.1.3955.1.1":
151
+ category: Firewall
152
+ model: BEFSX41
153
+
154
+ # TODO: Brocade: add all from https://docs.microfocus.com/NOM/2018.05/Content/common/nom_device_support_matrix/DSD_Foundry_JS.html
155
+ "1.3.6.1.4.1.1991.1.3.55.1.2":
156
+ category: Router
157
+ model: MLXe-16
158
+ "1.3.6.1.4.1.1991.1.3.55.2.2":
159
+ category: Router
160
+ model: MLXe-8
161
+ "1.3.6.1.4.1.1991.1.3.55.3.2":
162
+ category: Router
163
+ model: MLXe-4
164
+ "1.3.6.1.4.1.1991.1.3.55.4.2":
165
+ category: Router
166
+ model: MLXe-32
167
+ "1.3.6.1.4.1.1991.1.3.56.1.3.1.1":
168
+ category: Switch
169
+ model: Stackable ICX6610-24F
170
+
171
+ category_map:
172
+ "Self-contained NAS": "NAS"
src/go/plugin/go.d/pkg/snmputils/enterprise-numbers.txt
renamed
src/go/plugin/go.d/pkg/snmputils/overrides.go
new
+78
@@ -0,0 +1,78 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package snmputils
4
+
5
+import (
6
+ "errors"
7
+ "io/fs"
8
+ "os"
9
+ "path/filepath"
10
+ "sync"
11
+
12
+ "gopkg.in/yaml.v2"
13
+
14
+ "github.com/netdata/netdata/go/plugins/pkg/executable"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pluginconfig"
16
+)
17
+
18
+var (
19
+ overridesData *overrides
20
+ loadOverridesOnce sync.Once
21
+)
22
+
23
+type (
24
+ overrides struct {
25
+ EnterpriseNumbers enterpriseNumbersOverrides `yaml:"enterprisenumbers"`
26
+ SysObjectIDs sysObjectIDOverrides `yaml:"sysobjectids"`
27
+ }
28
+ enterpriseNumbersOverrides struct {
29
+ // Map PEN org names -> canonical vendor names
30
+ OrgToVendor map[string]string `yaml:"org_to_vendor"`
31
+ }
32
+ sysObjectIDOverrides struct {
33
+ // Exact OID overrides
34
+ OIDOverrides map[string]sysObjectIDOverride `yaml:"oid_overrides"`
35
+ // Category normalization
36
+ CategoryMap map[string]string `yaml:"category_map"`
37
+ }
38
+ sysObjectIDOverride struct {
39
+ // If non-empty, overrides the value; empty string => no change
40
+ Category string `yaml:"category,omitempty"`
41
+ Model string `yaml:"model,omitempty"`
42
+ }
43
+)
44
+
45
+func loadOverrides() {
46
+ loadOverridesOnce.Do(func() {
47
+ bs, err := os.ReadFile(getOverridesPath())
48
+ if err != nil {
49
+ log.Errorf("failed to read snmp overrides file: %v", err)
50
+ return
51
+ }
52
+ var o overrides
53
+ if err := yaml.UnmarshalStrict(bs, &o); err != nil {
54
+ log.Errorf("failed to unmarshal snmp overrides file: %v", err)
55
+ return
56
+ }
57
+ overridesData = &o
58
+ })
59
+}
60
+
61
+func getOverridesPath() string {
62
+ if executable.Name == "test" {
63
+ dir, _ := filepath.Abs("../../../config/go.d/snmp.profiles/meta_overrides.yaml")
64
+ return dir
65
+ }
66
+ if path := filepath.Join(executable.Directory, "..", "config", executable.Name, "snmp.profiles", "meta_overrides.yaml"); isFileExists(path) {
67
+ return path
68
+ }
69
+ return filepath.Join(pluginconfig.CollectorsStockDir(), "snmp.profiles", "meta_overrides.yaml")
70
+}
71
+
72
+func isFileExists(path string) bool {
73
+ fi, err := os.Stat(path)
74
+ if err != nil {
75
+ return !errors.Is(err, fs.ErrNotExist)
76
+ }
77
+ return fi.Mode().IsRegular()
78
+}
src/go/plugin/go.d/pkg/snmputils/overrides_test.go
new
+261
@@ -0,0 +1,261 @@
1
+package snmputils
2
+
3
+import (
4
+ "testing"
5
+
6
+ "github.com/gosnmp/gosnmp"
7
+ "github.com/stretchr/testify/assert"
8
+ "github.com/stretchr/testify/require"
9
+)
10
+
11
+func withOverrides(t *testing.T, o *overrides) func() {
12
+ t.Helper()
13
+ prev := overridesData
14
+ overridesData = o
15
+ return func() { overridesData = prev }
16
+}
17
+
18
+func TestBaseMeta_FromEmbeddedDB(t *testing.T) {
19
+ cases := map[string]struct {
20
+ oid string
21
+ wantCat string
22
+ wantModel string
23
+ }{
24
+ "hp_vc_40gb_module": {
25
+ oid: "1.3.6.1.2.1.11.5.7.5.8",
26
+ wantCat: "SANSwitch",
27
+ wantModel: "Virtual Connect SE 40Gb F8 Module",
28
+ },
29
+ "hp_vc_100gb_module": {
30
+ oid: "1.3.6.1.2.1.11.5.7.5.9",
31
+ wantCat: "SANSwitch",
32
+ wantModel: "Virtual Connect SE 100Gb F32 Module",
33
+ },
34
+ "abb_ups_dpa": {
35
+ oid: "1.3.6.1.2.1.33",
36
+ wantCat: "UPS",
37
+ wantModel: "DPA",
38
+ },
39
+ "nx_gtx_1000": {
40
+ oid: "1.3.6.1.4.1.1.1.1.66",
41
+ wantCat: "Router",
42
+ wantModel: "GTX 1000",
43
+ },
44
+ "nx_vpn_router_1000": {
45
+ oid: "1.3.6.1.4.1.2505.3",
46
+ wantCat: "Firewall",
47
+ wantModel: "Nortel Networks VPN Router 1000",
48
+ },
49
+ }
50
+
51
+ for name, tc := range cases {
52
+ tc := tc
53
+ t.Run(name, func(t *testing.T) {
54
+ defer withOverrides(t, nil)() // ensure no YAML overrides
55
+
56
+ si := &SysInfo{SysObjectID: tc.oid}
57
+ updateMetadata(si)
58
+
59
+ assert.Equal(t, tc.wantCat, si.Category, "category mismatch for %s", tc.oid)
60
+ assert.Equal(t, tc.wantModel, si.Model, "model mismatch for %s", tc.oid)
61
+ })
62
+ }
63
+}
64
+
65
+func TestOverrides_OnExistingOID(t *testing.T) {
66
+ cases := map[string]struct {
67
+ oid string
68
+ overrides *overrides
69
+ wantCat string
70
+ wantModel string
71
+ }{
72
+ "override_then_normalize_category": {
73
+ oid: "1.3.6.1.2.1.11.5.7.5.8", // base: SANSwitch / VC 40Gb
74
+ overrides: &overrides{
75
+ EnterpriseNumbers: enterpriseNumbersOverrides{
76
+ OrgToVendor: map[string]string{},
77
+ },
78
+ SysObjectIDs: sysObjectIDOverrides{
79
+ OIDOverrides: map[string]sysObjectIDOverride{
80
+ "1.3.6.1.2.1.11.5.7.5.8": {
81
+ Category: "L3 Switch", // will normalize
82
+ Model: "Renamed Model",
83
+ },
84
+ },
85
+ CategoryMap: map[string]string{
86
+ "L3 Switch": "Switch",
87
+ },
88
+ },
89
+ },
90
+ wantCat: "Switch",
91
+ wantModel: "Renamed Model",
92
+ },
93
+ "override_model_only_keep_base_category": {
94
+ oid: "1.3.6.1.2.1.33", // base: UPS / DPA
95
+ overrides: &overrides{
96
+ EnterpriseNumbers: enterpriseNumbersOverrides{OrgToVendor: map[string]string{}},
97
+ SysObjectIDs: sysObjectIDOverrides{
98
+ OIDOverrides: map[string]sysObjectIDOverride{
99
+ "1.3.6.1.2.1.33": {Model: "DPA-X"},
100
+ },
101
+ CategoryMap: map[string]string{}, // no normalization
102
+ },
103
+ },
104
+ wantCat: "UPS",
105
+ wantModel: "DPA-X",
106
+ },
107
+ }
108
+
109
+ for name, tc := range cases {
110
+ tc := tc
111
+ t.Run(name, func(t *testing.T) {
112
+ defer withOverrides(t, tc.overrides)()
113
+
114
+ si := &SysInfo{SysObjectID: tc.oid}
115
+ updateMetadata(si)
116
+
117
+ assert.Equal(t, tc.wantCat, si.Category)
118
+ assert.Equal(t, tc.wantModel, si.Model)
119
+ assert.Empty(t, si.Vendor, "Vendor should remain empty without org_to_vendor mapping")
120
+ })
121
+ }
122
+}
123
+
124
+func TestOverrides_OnUnknownOID(t *testing.T) {
125
+ cases := map[string]struct {
126
+ oid string
127
+ overrides *overrides
128
+ wantCat string
129
+ wantModel string
130
+ }{
131
+ "create_from_override_with_normalization": {
132
+ oid: "1.3.6.1.4.1.99999.42", // not in base DB
133
+ overrides: &overrides{
134
+ EnterpriseNumbers: enterpriseNumbersOverrides{OrgToVendor: map[string]string{}},
135
+ SysObjectIDs: sysObjectIDOverrides{
136
+ OIDOverrides: map[string]sysObjectIDOverride{
137
+ "1.3.6.1.4.1.99999.42": {
138
+ Category: "UTM", // will normalize
139
+ Model: "XR-1000", // set model
140
+ },
141
+ },
142
+ CategoryMap: map[string]string{
143
+ "UTM": "Firewall",
144
+ },
145
+ },
146
+ },
147
+ wantCat: "Firewall",
148
+ wantModel: "XR-1000",
149
+ },
150
+ }
151
+
152
+ for name, tc := range cases {
153
+ tc := tc
154
+ t.Run(name, func(t *testing.T) {
155
+ defer withOverrides(t, tc.overrides)()
156
+
157
+ si := &SysInfo{SysObjectID: tc.oid}
158
+ updateMetadata(si)
159
+
160
+ assert.Equal(t, tc.wantCat, si.Category)
161
+ assert.Equal(t, tc.wantModel, si.Model)
162
+ })
163
+ }
164
+}
165
+
166
+func TestOrgToVendorMapping(t *testing.T) {
167
+ cases := map[string]struct {
168
+ oid string
169
+ }{
170
+ "maps_org_to_vendor_when_override_present": {oid: "1.3.6.1.4.1.2505.3"},
171
+ }
172
+
173
+ for name, tc := range cases {
174
+ tc := tc
175
+ t.Run(name, func(t *testing.T) {
176
+ rawOrg := lookupEnterpriseNumber(tc.oid)
177
+ if rawOrg == "" {
178
+ t.Skip("no organization resolved for test OID; skipping")
179
+ }
180
+
181
+ defer withOverrides(t, &overrides{
182
+ EnterpriseNumbers: enterpriseNumbersOverrides{
183
+ OrgToVendor: map[string]string{
184
+ rawOrg: "CanonicalVendor",
185
+ },
186
+ },
187
+ SysObjectIDs: sysObjectIDOverrides{
188
+ OIDOverrides: map[string]sysObjectIDOverride{},
189
+ CategoryMap: map[string]string{},
190
+ },
191
+ })()
192
+
193
+ si := &SysInfo{SysObjectID: tc.oid}
194
+ updateMetadata(si)
195
+
196
+ assert.Equal(t, "CanonicalVendor", si.Vendor, "vendor mapping via org_to_vendor failed")
197
+ })
198
+ }
199
+}
200
+
201
+func TestLookupEnterpriseNumber(t *testing.T) {
202
+ cases := map[string]struct {
203
+ oid string
204
+ wantNonEmpty bool
205
+ }{
206
+ "known_pen_should_resolve_org": {oid: "1.3.6.1.4.1.2505.3", wantNonEmpty: true},
207
+ "not_under_enterprise_returns_empty": {oid: "1.3.6.1.2.1.1.2.0", wantNonEmpty: false},
208
+ "too_short_returns_empty": {oid: "1.3.6.1.4.1", wantNonEmpty: false},
209
+ "trailing_dot_returns_empty": {oid: "1.3.6.1.4.1.", wantNonEmpty: false},
210
+ }
211
+
212
+ for name, tc := range cases {
213
+ tc := tc
214
+ t.Run(name, func(t *testing.T) {
215
+ got := lookupEnterpriseNumber(tc.oid)
216
+ if tc.wantNonEmpty {
217
+ require.NotEmpty(t, got, "expected non-empty org for oid %s", tc.oid)
218
+ } else {
219
+ assert.Empty(t, got, "expected empty org for oid %s, but got %s", tc.oid, got)
220
+ }
221
+ })
222
+ }
223
+}
224
+
225
+func TestPduToString(t *testing.T) {
226
+ cases := map[string]struct {
227
+ pdu gosnmp.SnmpPDU
228
+ want string
229
+ wantErr bool
230
+ }{
231
+ "octet_string": {
232
+ pdu: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte("hello\nworld")},
233
+ want: "hello\nworld",
234
+ },
235
+ "integer": {
236
+ pdu: gosnmp.SnmpPDU{Type: gosnmp.Integer, Value: int(42)},
237
+ want: "42",
238
+ },
239
+ "counter32": {
240
+ pdu: gosnmp.SnmpPDU{Type: gosnmp.Counter32, Value: uint32(7)},
241
+ want: "7",
242
+ },
243
+ "object_identifier_trims_dot": {
244
+ pdu: gosnmp.SnmpPDU{Type: gosnmp.ObjectIdentifier, Value: ".1.3.6.1.2.1.1.1.0"},
245
+ want: "1.3.6.1.2.1.1.1.0",
246
+ },
247
+ }
248
+
249
+ for name, tc := range cases {
250
+ tc := tc
251
+ t.Run(name, func(t *testing.T) {
252
+ got, err := PduToString(tc.pdu)
253
+ if tc.wantErr {
254
+ require.Error(t, err)
255
+ return
256
+ }
257
+ require.NoError(t, err)
258
+ assert.Equal(t, tc.want, got)
259
+ })
260
+ }
261
+}
src/go/plugin/go.d/pkg/snmputils/sysObjectIDs.json
renamed
src/go/plugin/go.d/pkg/snmputils/sysinfo.go
new
+251
@@ -0,0 +1,251 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package snmputils
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ _ "embed"
9
+ "encoding/json"
10
+ "fmt"
11
+ "strconv"
12
+ "strings"
13
+
14
+ "github.com/gosnmp/gosnmp"
15
+
16
+ "github.com/netdata/netdata/go/plugins/logger"
17
+)
18
+
19
+var log = logger.New().With("component", "snmp/sysinfo")
20
+
21
+const (
22
+ RootOidMibSystem = "1.3.6.1.2.1.1"
23
+ OidSysDescr = "1.3.6.1.2.1.1.1.0"
24
+ OidSysObject = "1.3.6.1.2.1.1.2.0"
25
+ OidSysContact = "1.3.6.1.2.1.1.4.0"
26
+ OidSysName = "1.3.6.1.2.1.1.5.0"
27
+ OidSysLocation = "1.3.6.1.2.1.1.6.0"
28
+)
29
+
30
+type SysInfo struct {
31
+ SysObjectID string `json:"-"`
32
+
33
+ Descr string `json:"description"`
34
+ Contact string `json:"contact"`
35
+ Name string `json:"name"`
36
+ Location string `json:"location"`
37
+
38
+ Organization string `json:"organization"`
39
+ Vendor string `json:"vendor"`
40
+ Category string `json:"category"`
41
+ Model string `json:"model"`
42
+}
43
+
44
+func GetSysInfo(client gosnmp.Handler) (*SysInfo, error) {
45
+ pdus, err := client.WalkAll(RootOidMibSystem)
46
+ if err != nil {
47
+ return nil, err
48
+ }
49
+
50
+ si := &SysInfo{
51
+ Name: "unknown",
52
+ Organization: "Unknown",
53
+ }
54
+
55
+ loadOverrides()
56
+
57
+ for _, pdu := range pdus {
58
+ oid := strings.TrimPrefix(pdu.Name, ".")
59
+
60
+ switch oid {
61
+ case OidSysDescr:
62
+ si.Descr, err = PduToString(pdu)
63
+ si.Descr = valueSanitizer.Replace(si.Descr)
64
+ case OidSysObject:
65
+ var sysObj string
66
+ if sysObj, err = PduToString(pdu); err == nil {
67
+ si.SysObjectID = sysObj
68
+ }
69
+ case OidSysContact:
70
+ si.Contact, err = PduToString(pdu)
71
+ si.Contact = valueSanitizer.Replace(si.Contact)
72
+ case OidSysName:
73
+ si.Name, err = PduToString(pdu)
74
+ si.Name = valueSanitizer.Replace(si.Name)
75
+ case OidSysLocation:
76
+ si.Location, err = PduToString(pdu)
77
+ si.Location = valueSanitizer.Replace(si.Location)
78
+ }
79
+ if err != nil {
80
+ return nil, fmt.Errorf("OID '%s': %v", pdu.Name, err)
81
+ }
82
+ }
83
+
84
+ updateMetadata(si)
85
+
86
+ return si, nil
87
+}
88
+
89
+var valueSanitizer = strings.NewReplacer(
90
+ "'", "",
91
+ "\n", " ",
92
+ "\r", " ",
93
+ "\x00", "",
94
+ "\"", "",
95
+ "`", "",
96
+ "\\", "",
97
+)
98
+
99
+// updateMetadata enriches a SysInfo struct with metadata based on its SysObjectID.
100
+// It populates the Organization, Vendor, Category, and Model fields.
101
+func updateMetadata(si *SysInfo) {
102
+ // 1. Guard clauses: Validate input early to prevent panics and wasted work.
103
+ if si == nil || si.SysObjectID == "" {
104
+ return
105
+ }
106
+
107
+ // 2. Gather all necessary data first.
108
+ rawOrg := lookupEnterpriseNumber(si.SysObjectID)
109
+ baseMeta, _ := lookupSysObjectIDMeta(si.SysObjectID)
110
+
111
+ // 3. Process the data and determine the final values.
112
+ finalCategory := baseMeta.Category
113
+ finalModel := baseMeta.Model
114
+ finalVendor := ""
115
+
116
+ if overridesData != nil {
117
+ // Apply specific OID overrides for category and model.
118
+ if override, found := overridesData.SysObjectIDs.OIDOverrides[si.SysObjectID]; found {
119
+ if override.Category != "" {
120
+ finalCategory = override.Category
121
+ }
122
+ if override.Model != "" {
123
+ finalModel = override.Model
124
+ }
125
+ }
126
+
127
+ // Normalize the category *after* applying the specific override.
128
+ if normalized, found := overridesData.SysObjectIDs.CategoryMap[finalCategory]; found {
129
+ finalCategory = normalized
130
+ }
131
+
132
+ // Map the raw organization name to a standardized vendor name.
133
+ if vendor, found := overridesData.EnterpriseNumbers.OrgToVendor[rawOrg]; found {
134
+ finalVendor = vendor
135
+ }
136
+ }
137
+
138
+ // 4. Assign all computed values to the struct in one final, clean step.
139
+ si.Organization = valueSanitizer.Replace(rawOrg) // Sanitize at the last moment.
140
+ si.Category = finalCategory
141
+ si.Model = finalModel
142
+ si.Vendor = finalVendor
143
+}
144
+
145
+var (
146
+ // https://www.iana.org/assignments/enterprise-numbers.txt
147
+ //go:embed "enterprise-numbers.txt"
148
+ enterpriseNumberTxt []byte
149
+ // https://github.com/parthiganesh/snmp-sysObjectID
150
+ //go:embed "sysObjectIDs.json"
151
+ sysObjectIDsJson []byte
152
+
153
+ enterpriseNumbers = func() map[string]string {
154
+ if len(enterpriseNumberTxt) == 0 {
155
+ panic("snmp: enterprise-numbers.txt is empty")
156
+ }
157
+
158
+ mapping := make(map[string]string, 65000)
159
+
160
+ var id string
161
+
162
+ sc := bufio.NewScanner(bytes.NewReader(enterpriseNumberTxt))
163
+
164
+ for sc.Scan() {
165
+ line := strings.TrimSpace(sc.Text())
166
+ if line == "" {
167
+ continue
168
+ }
169
+
170
+ if _, err := strconv.Atoi(line); err == nil {
171
+ if id == "" {
172
+ id = line
173
+ if _, ok := mapping[id]; ok {
174
+ panic("snmp: duplicate entry number: " + line)
175
+ }
176
+ }
177
+ continue
178
+ }
179
+ if id != "" {
180
+ if line == "---none---" || line == "Reserved" {
181
+ id = ""
182
+ continue
183
+ }
184
+ mapping[id] = line
185
+ id = ""
186
+ }
187
+ }
188
+
189
+ if len(mapping) == 0 {
190
+ panic("snmp: enterprise-numbers mapping is empty after reading enterprise-numbers.txt")
191
+ }
192
+
193
+ return mapping
194
+ }()
195
+ sysObjectIDs = func() map[string]sysObjectIDMeta {
196
+ if len(sysObjectIDsJson) == 0 {
197
+ panic("snmp: sysObjectIDs.json is empty")
198
+ }
199
+
200
+ var ids = map[string]sysObjectIDMeta{}
201
+ if err := json.Unmarshal(sysObjectIDsJson, &ids); err != nil {
202
+ panic(fmt.Sprintf("snmp: invalid sysObjectIDs.json: %v", err))
203
+ }
204
+ return ids
205
+ }()
206
+)
207
+
208
+func lookupEnterpriseNumber(sysObject string) string {
209
+ const rootOidIanaPEN = "1.3.6.1.4.1"
210
+ v, ok := strings.CutPrefix(sysObject, rootOidIanaPEN+".") // .1.3.6.1.4.1.14988.1 => 14988.1
211
+ if !ok {
212
+ return ""
213
+ }
214
+ num, _, ok := strings.Cut(v, ".")
215
+ if !ok {
216
+ return ""
217
+ }
218
+ return enterpriseNumbers[num]
219
+}
220
+
221
+func lookupSysObjectIDMeta(sysObject string) (sysObjectIDMeta, bool) {
222
+ meta, ok := sysObjectIDs[sysObject]
223
+ return meta, ok
224
+}
225
+
226
+type sysObjectIDMeta struct {
227
+ Category string
228
+ Model string
229
+}
230
+
231
+func PduToString(pdu gosnmp.SnmpPDU) (string, error) {
232
+ switch pdu.Type {
233
+ case gosnmp.OctetString:
234
+ // TODO: this isn't reliable (e.g. physAddress we need hex.EncodeToString())
235
+ bs, ok := pdu.Value.([]byte)
236
+ if !ok {
237
+ return "", fmt.Errorf("OctetString is not a []byte but %T", pdu.Value)
238
+ }
239
+ return strings.ToValidUTF8(string(bs), "�"), nil
240
+ case gosnmp.Counter32, gosnmp.Counter64, gosnmp.Integer, gosnmp.Gauge32:
241
+ return gosnmp.ToBigInt(pdu.Value).String(), nil
242
+ case gosnmp.ObjectIdentifier:
243
+ v, ok := pdu.Value.(string)
244
+ if !ok {
245
+ return "", fmt.Errorf("ObjectIdentifier is not a string but %T", pdu.Value)
246
+ }
247
+ return strings.TrimPrefix(v, "."), nil
248
+ default:
249
+ return "", fmt.Errorf("unsupported type: '%v'", pdu.Type)
250
+ }
251
+}