master
go 315 lines 7.18 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "maps"
10 "slices"
11 "strconv"
12 "syscall"
13
14 "github.com/google/uuid"
15 "github.com/gosnmp/gosnmp"
16 "github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
17 "golang.org/x/sync/errgroup"
18
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
21 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
22 )
23
24 func (c *Collector) collect(ctx context.Context) (map[string]int64, error) {
25 if ctx == nil {
26 ctx = context.Background()
27 }
28
29 if err := c.ensureInitialized(); err != nil {
30 return nil, err
31 }
32
33 if c.PingOnly {
34 return c.collectPingOnly(ctx)
35 }
36 return c.collectDeviceMetrics(ctx)
37 }
38
39 func (c *Collector) collectPingOnly(ctx context.Context) (map[string]int64, error) {
40 mx := make(map[string]int64)
41
42 if err := c.collectPing(ctx, mx); err != nil {
43 return nil, err
44 }
45
46 return mx, nil
47 }
48
49 func (c *Collector) collectDeviceMetrics(ctx context.Context) (map[string]int64, error) {
50 var (
51 snmpMx map[string]int64
52 pingMx map[string]int64
53 )
54
55 g, groupCtx := errgroup.WithContext(ctx)
56
57 g.Go(func() error {
58 m := make(map[string]int64)
59 if err := c.collectSNMP(m); err != nil {
60 return err
61 }
62 snmpMx = m
63 return nil
64 })
65
66 if c.Ping.Enabled && c.pingClient != nil {
67 g.Go(func() error {
68 m := make(map[string]int64)
69 if err := c.collectPing(groupCtx, m); err != nil {
70 c.Errorf("ping: %v", err)
71 if isPingUnrecoverableError(err) {
72 c.pingClient = nil
73 }
74 return nil
75 }
76 pingMx = m
77 return nil
78 })
79 }
80
81 if err := g.Wait(); err != nil {
82 return nil, err
83 }
84
85 mx := make(map[string]int64, len(snmpMx)+len(pingMx))
86
87 maps.Copy(mx, snmpMx)
88 maps.Copy(mx, pingMx)
89
90 return mx, nil
91 }
92
93 func (c *Collector) ensureInitialized() error {
94 if c.snmpClient == nil {
95 return errors.New("snmp client not initialized")
96 }
97
98 if c.sysInfo != nil {
99 return nil
100 }
101
102 si, err := snmputils.GetSysInfo(c.snmpClient)
103 if err != nil {
104 return err
105 }
106
107 if c.snmpProfiles == nil {
108 c.snmpProfiles = c.setupProfiles(si)
109 }
110
111 if c.ddSnmpColl == nil && len(c.snmpProfiles) > 0 {
112 c.ddSnmpColl = c.newDdSnmpColl(ddsnmpcollector.Config{
113 SnmpClient: c.snmpClient,
114 Profiles: c.snmpProfiles,
115 Log: c.Logger,
116 SysObjectID: si.SysObjectID,
117 DisableBulkWalk: c.disableBulkWalk,
118 })
119 }
120
121 if c.ddSnmpColl == nil && !c.PingOnly && !c.Ping.Enabled {
122 return errors.New("no profiles found and ping disabled")
123 }
124
125 if c.CreateVnode {
126 if c.ddSnmpColl == nil {
127 c.vnode = c.setupVnode(si, nil)
128 } else {
129 deviceMeta, err := c.ddSnmpColl.CollectDeviceMetadata()
130 if err != nil {
131 return err
132 }
133 c.vnode = c.setupVnode(si, deviceMeta)
134 }
135 }
136
137 c.sysInfo = si
138
139 if c.PingOnly || c.Ping.Enabled {
140 c.addPingCharts()
141 }
142
143 c.registerDeviceForTopology(si)
144
145 return nil
146 }
147
148 func (c *Collector) setupVnode(si *snmputils.SysInfo, deviceMeta map[string]ddsnmp.MetaTag) *vnodes.VirtualNode {
149 if c.Vnode.GUID == "" {
150 c.Vnode.GUID = uuid.NewSHA1(uuid.NameSpaceDNS, []byte(c.Hostname)).String()
151 }
152
153 hostnames := []string{
154 c.Vnode.Hostname,
155 si.Name,
156 "snmp-device",
157 }
158 i := slices.IndexFunc(hostnames, func(s string) bool { return s != "" })
159 c.Vnode.Hostname = hostnames[i]
160
161 labels := map[string]string{
162 "_vnode_type": "snmp",
163 "_net_default_iface_ip": c.Hostname,
164 "address": c.Hostname,
165 }
166
167 if c.UpdateEvery >= 1 && c.VnodeDeviceDownThreshold >= 1 {
168 // Add 2 seconds buffer to account for collection/transmission delays
169 v := c.VnodeDeviceDownThreshold*c.UpdateEvery + 2
170 labels["_node_stale_after_seconds"] = strconv.Itoa(v)
171 }
172
173 labels["sys_object_id"] = si.SysObjectID
174 labels["name"] = si.Name
175 labels["description"] = si.Descr
176 labels["contact"] = si.Contact
177 labels["location"] = si.Location
178 if si.Vendor != "" {
179 labels["vendor"] = si.Vendor
180 } else if si.Organization != "" {
181 labels["vendor"] = si.Organization
182 }
183 if si.Category != "" {
184 labels["type"] = si.Category
185 }
186 if si.Model != "" {
187 labels["model"] = si.Model
188 }
189
190 for k, val := range deviceMeta {
191 if v, ok := labels[k]; !ok || v == "" || val.IsExactMatch {
192 labels[k] = val.Value
193 }
194 }
195
196 maps.Copy(labels, c.Vnode.Labels)
197
198 return &vnodes.VirtualNode{
199 GUID: c.Vnode.GUID,
200 Hostname: c.Vnode.Hostname,
201 Labels: labels,
202 }
203 }
204 func (c *Collector) initAndConnectSNMPClient() (gosnmp.Handler, error) {
205 snmpClient, err := c.initSNMPClient()
206 if err != nil {
207 return nil, fmt.Errorf("init: %w", err)
208 }
209
210 if err := snmpClient.Connect(); err != nil {
211 return nil, fmt.Errorf("connect: %w", err)
212 }
213
214 if snmpClient.Version() == gosnmp.Version1 {
215 return snmpClient, nil
216 }
217
218 if c.Options.MaxRepetitions == 0 {
219 c.disableBulkWalk = true
220 return snmpClient, nil
221 }
222
223 if c.adjMaxRepetitions != 0 {
224 snmpClient.SetMaxRepetitions(c.adjMaxRepetitions)
225 } else {
226 ok, err := c.adjustMaxRepetitions(snmpClient)
227 if err != nil {
228 return nil, fmt.Errorf("re-adjust max repetitions SNMP client: %w", err)
229 }
230 if !ok {
231 c.Warningf("SNMP bulk walk disabled (device may not support GETBULK or max-repetitions adjustment failed)")
232 c.disableBulkWalk = true
233 }
234 c.adjMaxRepetitions = snmpClient.MaxRepetitions()
235 }
236
237 return snmpClient, nil
238 }
239
240 func (c *Collector) adjustMaxRepetitions(snmpClient gosnmp.Handler) (bool, error) {
241 ok, err := c.detectBulkWalkSupport(snmpClient)
242 if err != nil {
243 c.Warningf("bulk support probe error: %v", err)
244 return false, nil
245 }
246 if !ok {
247 return false, nil
248 }
249
250 orig := c.Options.MaxRepetitions
251 maxReps := c.Options.MaxRepetitions
252 attempts := 0
253 const maxAttempts = 20 // Prevent infinite loops
254
255 for maxReps > 0 && attempts < maxAttempts {
256 attempts++
257
258 v, err := snmpClient.BulkWalkAll(snmputils.RootOidMibSystem)
259 if err != nil {
260 return false, err
261 }
262
263 if len(v) > 0 {
264 //c.Config.OptionsConfig.MaxRepetitions = maxReps
265 if orig != maxReps {
266 c.Infof("adjusted max_repetitions: %d → %d (took %d attempts)", orig, maxReps, attempts)
267 }
268 return true, nil
269 }
270
271 // Adaptive decrease strategy
272 prevMaxReps := maxReps
273 if maxReps > 50 {
274 maxReps -= 10
275 } else if maxReps > 10 {
276 maxReps -= 5
277 } else if maxReps > 5 {
278 maxReps -= 2
279 } else {
280 maxReps--
281 }
282
283 maxReps = max(0, maxReps) // Ensure non-negative
284
285 c.Debugf("max_repetitions=%d returned no data, trying %d", prevMaxReps, maxReps)
286 snmpClient.SetMaxRepetitions(uint32(maxReps))
287 }
288
289 // Restore original value since nothing worked
290 snmpClient.SetMaxRepetitions(uint32(orig))
291 c.Debugf("unable to find working max_repetitions value after %d attempts", attempts)
292 return false, nil
293 }
294
295 func isPingUnrecoverableError(err error) bool {
296 var errno syscall.Errno
297 return errors.As(err, &errno) && (errors.Is(errno, syscall.EPERM) || errors.Is(errno, syscall.EACCES))
298 }
299
300 func (c *Collector) detectBulkWalkSupport(snmpClient gosnmp.Handler) (bool, error) {
301 if snmpClient.Version() == gosnmp.Version1 {
302 return false, nil
303 }
304
305 // Use a very small max-reps for the probe to be gentle
306 orig := snmpClient.MaxRepetitions()
307 defer snmpClient.SetMaxRepetitions(orig)
308 snmpClient.SetMaxRepetitions(5)
309
310 oids, err := snmpClient.BulkWalkAll(snmputils.RootOidMibSystem)
311 if err != nil {
312 return false, err
313 }
314 return len(oids) > 0, nil
315 }