master
go 221 lines 5.11 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmputils
4
5 import (
6 "bufio"
7 "bytes"
8 _ "embed"
9 "fmt"
10 "strconv"
11 "strings"
12
13 "github.com/gosnmp/gosnmp"
14
15 "github.com/netdata/netdata/go/plugins/logger"
16 )
17
18 var log = logger.New().With("component", "snmp/sysinfo")
19
20 const (
21 RootOidMibSystem = "1.3.6.1.2.1.1"
22 OidSysDescr = "1.3.6.1.2.1.1.1.0"
23 OidSysObject = "1.3.6.1.2.1.1.2.0"
24 OidSysContact = "1.3.6.1.2.1.1.4.0"
25 OidSysName = "1.3.6.1.2.1.1.5.0"
26 OidSysLocation = "1.3.6.1.2.1.1.6.0"
27 )
28
29 type SysInfo struct {
30 SysObjectID string `json:"-"`
31
32 Descr string `json:"description"`
33 Contact string `json:"contact"`
34 Name string `json:"name"`
35 Location string `json:"location"`
36
37 Organization string `json:"organization"`
38 Vendor string `json:"vendor"`
39 Category string `json:"category"`
40 Model string `json:"model"`
41 }
42
43 func GetSysInfo(client gosnmp.Handler) (*SysInfo, error) {
44 pdus, err := client.WalkAll(RootOidMibSystem)
45 if err != nil {
46 return nil, err
47 }
48
49 si := &SysInfo{
50 Name: "unknown",
51 Organization: "Unknown",
52 }
53
54 loadOverrides()
55
56 for _, pdu := range pdus {
57 oid := strings.TrimPrefix(pdu.Name, ".")
58
59 switch oid {
60 case OidSysDescr:
61 si.Descr, err = PduToString(pdu)
62 si.Descr = valueSanitizer.Replace(si.Descr)
63 case OidSysObject:
64 var sysObj string
65 if sysObj, err = PduToString(pdu); err == nil {
66 si.SysObjectID = sysObj
67 }
68 case OidSysContact:
69 si.Contact, err = PduToString(pdu)
70 si.Contact = valueSanitizer.Replace(si.Contact)
71 case OidSysName:
72 si.Name, err = PduToString(pdu)
73 si.Name = valueSanitizer.Replace(si.Name)
74 case OidSysLocation:
75 si.Location, err = PduToString(pdu)
76 si.Location = valueSanitizer.Replace(si.Location)
77 }
78 if err != nil {
79 return nil, fmt.Errorf("OID '%s': %v", pdu.Name, err)
80 }
81 }
82
83 updateMetadata(si)
84
85 return si, nil
86 }
87
88 var valueSanitizer = strings.NewReplacer(
89 "'", "",
90 "\n", " ",
91 "\r", " ",
92 "\x00", "",
93 "\"", "",
94 "`", "",
95 "\\", "",
96 )
97
98 // updateMetadata enriches a SysInfo struct with metadata based on its SysObjectID.
99 // It populates the Organization, Vendor, Category, and Model fields.
100 func updateMetadata(si *SysInfo) {
101 if si == nil || si.SysObjectID == "" {
102 return
103 }
104
105 rawOrg := lookupEnterpriseNumber(si.SysObjectID)
106
107 var finalCategory string
108 var finalModel string
109 var finalVendor string
110
111 if overridesData != nil {
112 // Apply specific OID overrides for category and model.
113 if override, found := overridesData.SysObjectIDs.OIDOverrides[si.SysObjectID]; found {
114 if override.Category != "" {
115 finalCategory = override.Category
116 }
117 if override.Model != "" {
118 finalModel = override.Model
119 }
120 }
121
122 // Normalize the category *after* applying the specific override.
123 if normalized, found := overridesData.SysObjectIDs.CategoryMap[finalCategory]; found {
124 finalCategory = normalized
125 }
126
127 // Map the raw organization name to a standardized vendor name.
128 if vendor, found := overridesData.EnterpriseNumbers.OrgToVendor[rawOrg]; found {
129 finalVendor = vendor
130 }
131 }
132
133 si.Organization = valueSanitizer.Replace(rawOrg)
134 si.Category = finalCategory
135 si.Model = finalModel
136 si.Vendor = finalVendor
137 }
138
139 var (
140 // https://www.iana.org/assignments/enterprise-numbers.txt
141 //go:embed "enterprise-numbers.txt"
142 enterpriseNumberTxt []byte
143
144 enterpriseNumbers = func() map[string]string {
145 if len(enterpriseNumberTxt) == 0 {
146 panic("snmp: enterprise-numbers.txt is empty")
147 }
148
149 mapping := make(map[string]string, 65000)
150
151 var id string
152
153 sc := bufio.NewScanner(bytes.NewReader(enterpriseNumberTxt))
154
155 for sc.Scan() {
156 line := strings.TrimSpace(sc.Text())
157 if line == "" {
158 continue
159 }
160
161 if _, err := strconv.Atoi(line); err == nil {
162 if id == "" {
163 id = line
164 if _, ok := mapping[id]; ok {
165 panic("snmp: duplicate entry number: " + line)
166 }
167 }
168 continue
169 }
170 if id != "" {
171 if line == "---none---" || line == "Reserved" {
172 id = ""
173 continue
174 }
175 mapping[id] = line
176 id = ""
177 }
178 }
179
180 if len(mapping) == 0 {
181 panic("snmp: enterprise-numbers mapping is empty after reading enterprise-numbers.txt")
182 }
183
184 return mapping
185 }()
186 )
187
188 func lookupEnterpriseNumber(sysObject string) string {
189 const rootOidIanaPEN = "1.3.6.1.4.1"
190 v, ok := strings.CutPrefix(sysObject, rootOidIanaPEN+".") // .1.3.6.1.4.1.14988.1 => 14988.1
191 if !ok {
192 return ""
193 }
194 num, _, ok := strings.Cut(v, ".")
195 if !ok {
196 return ""
197 }
198 return enterpriseNumbers[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 }