@cryptotaxi247 / netdata-1 / commits / 188ae084f

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 +}