master
go 1,020 lines 33.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "context"
7 "errors"
8 "os"
9 "slices"
10 "strings"
11 "sync"
12 "syscall"
13 "testing"
14 "time"
15
16 "github.com/netdata/netdata/go/plugins/logger"
17 "github.com/netdata/netdata/go/plugins/pkg/confopt"
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector"
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
21 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pinger"
22 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils"
23
24 "github.com/golang/mock/gomock"
25 "github.com/gosnmp/gosnmp"
26 snmpmock "github.com/gosnmp/gosnmp/mocks"
27 "github.com/stretchr/testify/assert"
28 "github.com/stretchr/testify/require"
29 )
30
31 var (
32 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
33 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
34 )
35
36 func Test_testDataIsValid(t *testing.T) {
37 for name, data := range map[string][]byte{
38 "dataConfigJSON": dataConfigJSON,
39 "dataConfigYAML": dataConfigYAML,
40 } {
41 require.NotNil(t, data, name)
42 }
43 }
44
45 func TestCollector_ConfigurationSerialize(t *testing.T) {
46 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
47 }
48
49 func TestCollector_Init(t *testing.T) {
50 tests := map[string]struct {
51 prepareSNMP func() *Collector
52 wantFail bool
53 }{
54 "fail with default config": {
55 wantFail: true,
56 prepareSNMP: func() *Collector {
57 return New()
58 },
59 },
60 "fail when using SNMPv3 but 'user.name' not set": {
61 wantFail: true,
62 prepareSNMP: func() *Collector {
63 collr := New()
64 collr.Config = prepareV3Config()
65 collr.User.Name = ""
66 return collr
67 },
68 },
69 "success when using SNMPv1 with valid config": {
70 wantFail: false,
71 prepareSNMP: func() *Collector {
72 collr := New()
73 collr.Config = prepareV1Config()
74 return collr
75 },
76 },
77 "success when using SNMPv2 with valid config": {
78 wantFail: false,
79 prepareSNMP: func() *Collector {
80 collr := New()
81 collr.Config = prepareV2Config()
82 return collr
83 },
84 },
85 "success when using SNMPv3 with valid config": {
86 wantFail: false,
87 prepareSNMP: func() *Collector {
88 collr := New()
89 collr.Config = prepareV3Config()
90 return collr
91 },
92 },
93 }
94
95 for name, test := range tests {
96 t.Run(name, func(t *testing.T) {
97 collr := test.prepareSNMP()
98
99 if test.wantFail {
100 assert.Error(t, collr.Init(context.Background()))
101 } else {
102 assert.NoError(t, collr.Init(context.Background()))
103 }
104 })
105 }
106 }
107
108 func TestCollector_InitPassesSharedPingerConfig(t *testing.T) {
109 var gotCfg pinger.Config
110
111 collr := New()
112 collr.Config = prepareV2Config()
113 collr.PingOnly = true
114 collr.Ping.Network = "ip6"
115 collr.Ping.Interface = "eth0"
116 collr.Ping.Privileged = false
117 collr.Ping.Packets = 4
118 collr.Ping.Interval = confopt.Duration(250 * time.Millisecond)
119 collr.newPinger = func(cfg pinger.Config, _ *logger.Logger) (pinger.Client, error) {
120 gotCfg = cfg
121 return &mockPingClient{}, nil
122 }
123
124 require.NoError(t, collr.Init(context.Background()))
125
126 assert.Equal(t, pinger.Config{
127 Probe: pinger.ProbeConfig{
128 Network: "ip6",
129 Interface: "eth0",
130 Privileged: false,
131 Packets: 4,
132 Interval: confopt.Duration(250 * time.Millisecond),
133 Timeout: time.Second,
134 },
135 }, gotCfg)
136 }
137
138 func TestCollector_Cleanup(t *testing.T) {
139 tests := map[string]struct {
140 prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *Collector
141 }{
142 "cleanup call does not panic if snmpClient not initialized": {
143 prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *Collector {
144 collr := New()
145 collr.Config = prepareV2Config()
146 collr.newSnmpClient = func() gosnmp.Handler { return m }
147 setMockClientInitExpect(m)
148
149 require.NoError(t, collr.Init(context.Background()))
150
151 collr.snmpClient = nil
152
153 return collr
154 },
155 },
156 }
157
158 for name, test := range tests {
159 t.Run(name, func(t *testing.T) {
160 mockSNMP, cleanup := mockInit(t)
161 defer cleanup()
162
163 collr := test.prepareSNMP(t, mockSNMP)
164
165 assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
166 })
167 }
168 }
169
170 func TestCollector_Check(t *testing.T) {
171 tests := map[string]struct {
172 prepare func(m *snmpmock.MockHandler) *Collector
173 wantErr bool
174 }{
175 "success: connects and reads sysInfo": {
176 wantErr: false,
177 prepare: func(m *snmpmock.MockHandler) *Collector {
178 setMockClientInitExpect(m)
179 setMockClientSysInfoExpect(m)
180
181 c := New()
182 c.Config = prepareV2Config()
183 c.CreateVnode = false
184 c.Ping.Enabled = false
185 c.newSnmpClient = func() gosnmp.Handler { return m }
186 return c
187 },
188 },
189
190 "failure: SNMP connect error": {
191 wantErr: true,
192 prepare: func(m *snmpmock.MockHandler) *Collector {
193 setMockClientSetterExpect(m)
194 m.EXPECT().Connect().Return(errors.New("connect failed")).AnyTimes()
195
196 c := New()
197 c.Config = prepareV2Config()
198 c.CreateVnode = false
199 c.Ping.Enabled = false
200 c.newSnmpClient = func() gosnmp.Handler { return m }
201 return c
202 },
203 },
204
205 "failure: sysInfo walk error": {
206 wantErr: true,
207 prepare: func(m *snmpmock.MockHandler) *Collector {
208 // Normal init succeeds
209 setMockClientInitExpect(m)
210 // But sysInfo retrieval (WalkAll on system tree) fails
211 // If your helper is too opinionated, stub directly:
212 // The collector ultimately calls WalkAll on the system OID tree.
213 m.EXPECT().
214 WalkAll(gomock.Any()).
215 Return(nil, errors.New("walk failed"))
216
217 c := New()
218 c.Config = prepareV2Config()
219 c.CreateVnode = false
220 c.Ping.Enabled = false
221 c.newSnmpClient = func() gosnmp.Handler { return m }
222 return c
223 },
224 },
225
226 "success: ping_only with successful ping": {
227 wantErr: false,
228 prepare: func(m *snmpmock.MockHandler) *Collector {
229 setMockClientInitExpect(m)
230 setMockClientSysInfoExpect(m)
231
232 c := New()
233 c.Config = prepareV2Config()
234 c.PingOnly = true
235 c.CreateVnode = false
236 c.newSnmpClient = func() gosnmp.Handler { return m }
237 c.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
238 return &mockPingClient{sample: pingSuccessSample(c.Hostname)}, nil
239 }
240 return c
241 },
242 },
243
244 "success: ping_only with recoverable ping error": {
245 wantErr: false,
246 prepare: func(m *snmpmock.MockHandler) *Collector {
247 setMockClientInitExpect(m)
248 setMockClientSysInfoExpect(m)
249
250 c := New()
251 c.Config = prepareV2Config()
252 c.PingOnly = true
253 c.CreateVnode = false
254 c.newSnmpClient = func() gosnmp.Handler { return m }
255 c.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
256 return &mockPingClient{probeErr: errors.New("host unreachable")}, nil
257 }
258 return c
259 },
260 },
261
262 "failure: ping_only with unrecoverable ping error": {
263 wantErr: true,
264 prepare: func(m *snmpmock.MockHandler) *Collector {
265 setMockClientInitExpect(m)
266 setMockClientSysInfoExpect(m)
267
268 c := New()
269 c.Config = prepareV2Config()
270 c.PingOnly = true
271 c.CreateVnode = false
272 c.newSnmpClient = func() gosnmp.Handler { return m }
273 c.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
274 return &mockPingClient{
275 probeErr: &pinger.ProbeError{Host: c.Hostname, Stage: "run", Err: syscall.EPERM},
276 }, nil
277 }
278 return c
279 },
280 },
281 }
282
283 for name, tc := range tests {
284 t.Run(name, func(t *testing.T) {
285 ctrl := gomock.NewController(t)
286 defer ctrl.Finish()
287
288 mockSNMP := snmpmock.NewMockHandler(ctrl)
289
290 collr := tc.prepare(mockSNMP)
291 require.NoError(t, collr.Init(context.Background()))
292
293 err := collr.Check(context.Background())
294 if tc.wantErr {
295 assert.Error(t, err)
296 } else {
297 assert.NoError(t, err)
298 }
299 })
300 }
301 }
302
303 func TestCollector_CheckPingOnlyUsesReadOnlyProbing(t *testing.T) {
304 ctrl := gomock.NewController(t)
305 defer ctrl.Finish()
306
307 mockSNMP := snmpmock.NewMockHandler(ctrl)
308 setMockClientInitExpect(mockSNMP)
309 setMockClientSysInfoExpect(mockSNMP)
310
311 pingClient := &mockPingClient{sample: pingSuccessSample("192.0.2.1")}
312
313 collr := New()
314 collr.Config = prepareV2Config()
315 collr.PingOnly = true
316 collr.CreateVnode = false
317 collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
318 collr.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
319 return pingClient, nil
320 }
321
322 require.NoError(t, collr.Init(context.Background()))
323 type ctxKey struct{}
324 ctx := context.WithValue(context.Background(), ctxKey{}, "check")
325 require.NoError(t, collr.Check(ctx))
326
327 calls := pingClient.probeCalls()
328 require.Len(t, calls, 1)
329 assert.Equal(t, "probe", calls[0].method)
330 assert.Equal(t, "check", calls[0].ctx.Value(ctxKey{}))
331 }
332
333 func TestCollector_Collect(t *testing.T) {
334 tests := map[string]struct {
335 prepare func(m *snmpmock.MockHandler) *Collector
336 want map[string]int64
337 }{
338 "collects scalar metric": {
339 prepare: func(m *snmpmock.MockHandler) *Collector {
340 setMockClientInitExpect(m)
341 setMockClientSysInfoExpect(m)
342
343 collr := New()
344 collr.Config = prepareV2Config()
345 collr.CreateVnode = false
346 collr.Ping.Enabled = false
347 collr.snmpProfiles = []*ddsnmp.Profile{{}} // non-empty to enable collectSNMP()
348 collr.newSnmpClient = func() gosnmp.Handler { return m }
349 collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
350 return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
351 {
352 Source: "test",
353 Metrics: []ddsnmp.Metric{
354 {
355 Name: "uptime",
356 IsTable: false,
357 Value: 123,
358 Unit: "s",
359 Tags: map[string]string{},
360 Profile: &ddsnmp.ProfileMetrics{Tags: map[string]string{}},
361 },
362 },
363 },
364 }}
365 }
366 return collr
367 },
368 want: map[string]int64{
369 // scalar → "snmp_device_prof_<name>"
370 "snmp_device_prof_test_stats_errors_processing_scalar": 0,
371 "snmp_device_prof_test_stats_errors_processing_table": 0,
372 "snmp_device_prof_test_stats_errors_processing_licensing": 0,
373 "snmp_device_prof_test_stats_errors_processing_bgp": 0,
374 "snmp_device_prof_test_stats_errors_snmp": 0,
375 "snmp_device_prof_test_stats_metrics_rows": 0,
376 "snmp_device_prof_test_stats_metrics_licensing": 0,
377 "snmp_device_prof_test_stats_metrics_bgp": 0,
378 "snmp_device_prof_test_stats_metrics_scalar": 0,
379 "snmp_device_prof_test_stats_metrics_table": 0,
380 "snmp_device_prof_test_stats_metrics_tables": 0,
381 "snmp_device_prof_test_stats_metrics_virtual": 0,
382 "snmp_device_prof_test_stats_snmp_get_oids": 0,
383 "snmp_device_prof_test_stats_snmp_get_requests": 0,
384 "snmp_device_prof_test_stats_snmp_tables_cached": 0,
385 "snmp_device_prof_test_stats_snmp_tables_walked": 0,
386 "snmp_device_prof_test_stats_snmp_walk_pdus": 0,
387 "snmp_device_prof_test_stats_snmp_walk_requests": 0,
388 "snmp_device_prof_test_stats_table_cache_hits": 0,
389 "snmp_device_prof_test_stats_table_cache_misses": 0,
390 "snmp_device_prof_test_stats_timings_scalar": 0,
391 "snmp_device_prof_test_stats_timings_table": 0,
392 "snmp_device_prof_test_stats_timings_licensing": 0,
393 "snmp_device_prof_test_stats_timings_bgp": 0,
394 "snmp_device_prof_test_stats_timings_virtual": 0,
395 "snmp_device_prof_uptime": 123,
396 },
397 },
398 "collects table multivalue metric": {
399 prepare: func(m *snmpmock.MockHandler) *Collector {
400 setMockClientInitExpect(m)
401 setMockClientSysInfoExpect(m)
402
403 collr := New()
404 collr.Config = prepareV2Config()
405 collr.CreateVnode = false
406 collr.Ping.Enabled = false
407 collr.snmpProfiles = []*ddsnmp.Profile{{}}
408 collr.newSnmpClient = func() gosnmp.Handler { return m }
409 collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
410 return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
411 {
412 Source: "test",
413 Metrics: []ddsnmp.Metric{
414 {
415 Name: "if_octets",
416 IsTable: true,
417 Unit: "bit/s",
418 Tags: map[string]string{"ifName": "eth0"},
419 Profile: &ddsnmp.ProfileMetrics{Tags: map[string]string{}},
420 MultiValue: map[string]int64{
421 "in": 1,
422 "out": 2,
423 },
424 },
425 },
426 },
427 }}
428 }
429 return collr
430 },
431 want: map[string]int64{
432 // table key: "snmp_device_prof_<name>_<sorted tag values>_<subkey>"
433 // here tags = {"ifName":"eth0"} → key part becomes "_eth0"
434 "snmp_device_prof_test_stats_errors_processing_scalar": 0,
435 "snmp_device_prof_test_stats_errors_processing_table": 0,
436 "snmp_device_prof_test_stats_errors_processing_licensing": 0,
437 "snmp_device_prof_test_stats_errors_processing_bgp": 0,
438 "snmp_device_prof_test_stats_errors_snmp": 0,
439 "snmp_device_prof_test_stats_metrics_rows": 0,
440 "snmp_device_prof_test_stats_metrics_licensing": 0,
441 "snmp_device_prof_test_stats_metrics_bgp": 0,
442 "snmp_device_prof_test_stats_metrics_scalar": 0,
443 "snmp_device_prof_test_stats_metrics_table": 0,
444 "snmp_device_prof_test_stats_metrics_tables": 0,
445 "snmp_device_prof_test_stats_metrics_virtual": 0,
446 "snmp_device_prof_test_stats_snmp_get_oids": 0,
447 "snmp_device_prof_test_stats_snmp_get_requests": 0,
448 "snmp_device_prof_test_stats_snmp_tables_cached": 0,
449 "snmp_device_prof_test_stats_snmp_tables_walked": 0,
450 "snmp_device_prof_test_stats_snmp_walk_pdus": 0,
451 "snmp_device_prof_test_stats_snmp_walk_requests": 0,
452 "snmp_device_prof_test_stats_table_cache_hits": 0,
453 "snmp_device_prof_test_stats_table_cache_misses": 0,
454 "snmp_device_prof_test_stats_timings_scalar": 0,
455 "snmp_device_prof_test_stats_timings_table": 0,
456 "snmp_device_prof_test_stats_timings_licensing": 0,
457 "snmp_device_prof_test_stats_timings_bgp": 0,
458 "snmp_device_prof_test_stats_timings_virtual": 0,
459 "snmp_device_prof_if_octets_eth0_in": 1,
460 "snmp_device_prof_if_octets_eth0_out": 2,
461 },
462 },
463 }
464 tests["collects ping only metrics"] = struct {
465 prepare func(m *snmpmock.MockHandler) *Collector
466 want map[string]int64
467 }{
468 prepare: func(m *snmpmock.MockHandler) *Collector {
469 setMockClientInitExpect(m)
470 setMockClientSysInfoExpect(m)
471
472 collr := New()
473 collr.Config = prepareV2Config()
474 collr.PingOnly = true
475 collr.CreateVnode = false
476 collr.newSnmpClient = func() gosnmp.Handler { return m }
477 collr.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
478 return &mockPingClient{sample: pingSuccessSample(collr.Hostname)}, nil
479 }
480
481 return collr
482 },
483 want: map[string]int64{
484 "ping_rtt_min": (10 * time.Millisecond).Microseconds(),
485 "ping_rtt_max": (20 * time.Millisecond).Microseconds(),
486 "ping_rtt_avg": (15 * time.Millisecond).Microseconds(),
487 "ping_rtt_stddev": (5 * time.Millisecond).Microseconds(),
488 },
489 }
490 tests["collects no ping metrics when probe gets no replies"] = struct {
491 prepare func(m *snmpmock.MockHandler) *Collector
492 want map[string]int64
493 }{
494 prepare: func(m *snmpmock.MockHandler) *Collector {
495 setMockClientInitExpect(m)
496 setMockClientSysInfoExpect(m)
497
498 collr := New()
499 collr.Config = prepareV2Config()
500 collr.PingOnly = true
501 collr.CreateVnode = false
502 collr.newSnmpClient = func() gosnmp.Handler { return m }
503 collr.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
504 return &mockPingClient{sample: pingNoReplySample(collr.Hostname)}, nil
505 }
506
507 return collr
508 },
509 want: nil,
510 }
511
512 for name, tc := range tests {
513 t.Run(name, func(t *testing.T) {
514 mockCtl := gomock.NewController(t)
515 defer mockCtl.Finish()
516
517 mockSNMP := snmpmock.NewMockHandler(mockCtl)
518
519 collr := tc.prepare(mockSNMP)
520 require.NoError(t, collr.Init(context.Background()))
521
522 _ = collr.Check(context.Background())
523
524 got := collr.Collect(context.Background())
525 assert.Equal(t, tc.want, got)
526 })
527 }
528 }
529
530 func TestCollector_CollectPingOnlyUsesTrackingProbing(t *testing.T) {
531 ctrl := gomock.NewController(t)
532 defer ctrl.Finish()
533
534 mockSNMP := snmpmock.NewMockHandler(ctrl)
535 setMockClientInitExpect(mockSNMP)
536 setMockClientSysInfoExpect(mockSNMP)
537
538 pingClient := &mockPingClient{sample: pingSuccessSample("192.0.2.1")}
539
540 collr := New()
541 collr.Config = prepareV2Config()
542 collr.PingOnly = true
543 collr.CreateVnode = false
544 collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
545 collr.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
546 return pingClient, nil
547 }
548
549 require.NoError(t, collr.Init(context.Background()))
550 type checkKey struct{}
551 type collectKey struct{}
552 checkCtx := context.WithValue(context.Background(), checkKey{}, "check")
553 collectCtx := context.WithValue(context.Background(), collectKey{}, "collect")
554 _ = collr.Check(checkCtx)
555 got := collr.Collect(collectCtx)
556
557 assert.Equal(t, map[string]int64{
558 "ping_rtt_min": (10 * time.Millisecond).Microseconds(),
559 "ping_rtt_max": (20 * time.Millisecond).Microseconds(),
560 "ping_rtt_avg": (15 * time.Millisecond).Microseconds(),
561 "ping_rtt_stddev": (5 * time.Millisecond).Microseconds(),
562 }, got)
563
564 calls := pingClient.probeCalls()
565 require.Len(t, calls, 2)
566 assert.Equal(t, "probe", calls[0].method)
567 assert.Equal(t, "check", calls[0].ctx.Value(checkKey{}))
568 assert.Equal(t, "probe_and_track", calls[1].method)
569 assert.Equal(t, "collect", calls[1].ctx.Value(collectKey{}))
570 }
571
572 func TestCollector_CollectMixedModeCollectsSNMPAndPingMetrics(t *testing.T) {
573 ctrl := gomock.NewController(t)
574 defer ctrl.Finish()
575
576 mockSNMP := snmpmock.NewMockHandler(ctrl)
577 setMockClientInitExpect(mockSNMP)
578 setMockClientSysInfoExpect(mockSNMP)
579
580 pingClient := &mockPingClient{sample: pingSuccessSample("192.0.2.1")}
581
582 collr := New()
583 collr.Config = prepareV2Config()
584 collr.CreateVnode = false
585 collr.Ping.Enabled = true
586 collr.snmpProfiles = []*ddsnmp.Profile{{}}
587 collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
588 collr.newPinger = func(cfg pinger.Config, log *logger.Logger) (pinger.Client, error) {
589 return pingClient, nil
590 }
591 collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
592 return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{
593 {
594 Source: "test",
595 Metrics: []ddsnmp.Metric{
596 {
597 Name: "uptime",
598 IsTable: false,
599 Value: 123,
600 Unit: "s",
601 Tags: map[string]string{},
602 Profile: &ddsnmp.ProfileMetrics{Tags: map[string]string{}},
603 },
604 },
605 },
606 }}
607 }
608
609 require.NoError(t, collr.Init(context.Background()))
610 require.NoError(t, collr.Check(context.Background()))
611
612 got := collr.Collect(context.Background())
613
614 assert.Equal(t, map[string]int64{
615 "snmp_device_prof_test_stats_errors_processing_scalar": 0,
616 "snmp_device_prof_test_stats_errors_processing_table": 0,
617 "snmp_device_prof_test_stats_errors_processing_licensing": 0,
618 "snmp_device_prof_test_stats_errors_processing_bgp": 0,
619 "snmp_device_prof_test_stats_errors_snmp": 0,
620 "snmp_device_prof_test_stats_metrics_rows": 0,
621 "snmp_device_prof_test_stats_metrics_licensing": 0,
622 "snmp_device_prof_test_stats_metrics_bgp": 0,
623 "snmp_device_prof_test_stats_metrics_scalar": 0,
624 "snmp_device_prof_test_stats_metrics_table": 0,
625 "snmp_device_prof_test_stats_metrics_tables": 0,
626 "snmp_device_prof_test_stats_metrics_virtual": 0,
627 "snmp_device_prof_test_stats_snmp_get_oids": 0,
628 "snmp_device_prof_test_stats_snmp_get_requests": 0,
629 "snmp_device_prof_test_stats_snmp_tables_cached": 0,
630 "snmp_device_prof_test_stats_snmp_tables_walked": 0,
631 "snmp_device_prof_test_stats_snmp_walk_pdus": 0,
632 "snmp_device_prof_test_stats_snmp_walk_requests": 0,
633 "snmp_device_prof_test_stats_table_cache_hits": 0,
634 "snmp_device_prof_test_stats_table_cache_misses": 0,
635 "snmp_device_prof_test_stats_timings_scalar": 0,
636 "snmp_device_prof_test_stats_timings_table": 0,
637 "snmp_device_prof_test_stats_timings_licensing": 0,
638 "snmp_device_prof_test_stats_timings_bgp": 0,
639 "snmp_device_prof_test_stats_timings_virtual": 0,
640 "snmp_device_prof_uptime": 123,
641 "ping_rtt_min": (10 * time.Millisecond).Microseconds(),
642 "ping_rtt_max": (20 * time.Millisecond).Microseconds(),
643 "ping_rtt_avg": (15 * time.Millisecond).Microseconds(),
644 "ping_rtt_stddev": (5 * time.Millisecond).Microseconds(),
645 }, got)
646 }
647
648 type probeCall struct {
649 host string
650 method string
651 ctx context.Context
652 }
653
654 type mockPingClient struct {
655 mu sync.Mutex
656 sample pinger.Sample
657 probeErr error
658 calls []probeCall
659 }
660
661 func (m *mockPingClient) Probe(ctx context.Context, host string) (pinger.Sample, error) {
662 return m.recordedProbe(ctx, host, "probe")
663 }
664
665 func (m *mockPingClient) ProbeAndTrack(ctx context.Context, host string) (pinger.Sample, error) {
666 return m.recordedProbe(ctx, host, "probe_and_track")
667 }
668
669 func (m *mockPingClient) recordedProbe(ctx context.Context, host, method string) (pinger.Sample, error) {
670 m.mu.Lock()
671 defer m.mu.Unlock()
672
673 m.calls = append(m.calls, probeCall{host: host, method: method, ctx: ctx})
674 if m.probeErr != nil {
675 return pinger.Sample{}, m.probeErr
676 }
677
678 sample := m.sample
679 if sample.Host == "" {
680 sample.Host = host
681 }
682 return sample, nil
683 }
684
685 func (m *mockPingClient) probeCalls() []probeCall {
686 m.mu.Lock()
687 defer m.mu.Unlock()
688 return slices.Clone(m.calls)
689 }
690
691 func pingSuccessSample(host string) pinger.Sample {
692 return pinger.Sample{
693 Host: host,
694 PacketsRecv: 5,
695 PacketsSent: 5,
696 RTT: pinger.RTTSummary{
697 Valid: true,
698 Min: 10 * time.Millisecond,
699 Max: 20 * time.Millisecond,
700 Avg: 15 * time.Millisecond,
701 StdDev: 5 * time.Millisecond,
702 },
703 }
704 }
705
706 func pingNoReplySample(host string) pinger.Sample {
707 return pinger.Sample{
708 Host: host,
709 PacketsRecv: 0,
710 PacketsSent: 5,
711 }
712 }
713
714 func TestCollector_Collect_LicensingAggregation(t *testing.T) {
715 tests := map[string]struct {
716 source string
717 rows func(time.Time) []ddsnmp.LicenseRow
718 assert func(*testing.T, map[string]int64, time.Time)
719 }{
720 "checkpoint degraded row with expiry and usage": {
721 source: "checkpoint.yaml",
722 rows: func(now time.Time) []ddsnmp.LicenseRow {
723 expiry := now.Add(48 * time.Hour).Unix()
724 return []ddsnmp.LicenseRow{
725 typedLicenseRow("17", "Application Control",
726 withState(1, "about-to-expire"),
727 withExpiry(expiry),
728 withUsage(95),
729 withCapacity(100),
730 ),
731 }
732 },
733 assert: func(t *testing.T, got map[string]int64, start time.Time) {
734 assert.EqualValues(t, 0, got[metricIDLicenseStateHealthy])
735 assert.EqualValues(t, 0, got[metricIDLicenseStateInformational])
736 assert.EqualValues(t, 1, got[metricIDLicenseStateDegraded])
737 assert.EqualValues(t, 0, got[metricIDLicenseStateBroken])
738 assert.EqualValues(t, 0, got[metricIDLicenseStateIgnored])
739 assert.EqualValues(t, 95, got[metricIDLicenseUsagePercent])
740 expectedRemaining := start.Add(48*time.Hour).Unix() - start.Unix()
741 assert.GreaterOrEqual(t, got[metricIDLicenseRemainingTime], expectedRemaining-30)
742 assert.LessOrEqual(t, got[metricIDLicenseRemainingTime], expectedRemaining)
743 assert.Contains(t, got, "snmp_device_prof_checkpoint_stats_metrics_table")
744 },
745 },
746 "cisco smart partial data": {
747 source: "cisco.yaml",
748 rows: func(now time.Time) []ddsnmp.LicenseRow {
749 authExpiry := now.Add(48 * time.Hour).Unix()
750 certExpiry := now.Add(72 * time.Hour).Unix()
751 return []ddsnmp.LicenseRow{
752 typedLicenseRow("smart_authorization_state", "Smart Licensing authorization state",
753 withState(0, ""),
754 ),
755 typedLicenseRow("smart_authorization_expiry", "Smart Licensing authorization",
756 func(row *ddsnmp.LicenseRow) {
757 row.Authorization.Has = true
758 row.Authorization.Timestamp = authExpiry
759 row.Authorization.SourceOID = "ciscoSlaAuthExpireTime"
760 },
761 ),
762 typedLicenseRow("smart_id_certificate_expiry", "Smart Licensing ID certificate",
763 func(row *ddsnmp.LicenseRow) {
764 row.Certificate.Has = true
765 row.Certificate.Timestamp = certExpiry
766 row.Certificate.SourceOID = "ciscoSlaNextCertificateExpireTime"
767 },
768 ),
769 typedLicenseRow("dna_advantage", "network-advantage",
770 withState(2, "authorization_expired"),
771 withUsage(42),
772 ),
773 }
774 },
775 assert: func(t *testing.T, got map[string]int64, _ time.Time) {
776 assert.EqualValues(t, 3, got[metricIDLicenseStateHealthy])
777 assert.EqualValues(t, 0, got[metricIDLicenseStateInformational])
778 assert.EqualValues(t, 0, got[metricIDLicenseStateDegraded])
779 assert.EqualValues(t, 1, got[metricIDLicenseStateBroken])
780 assert.EqualValues(t, 0, got[metricIDLicenseStateIgnored])
781 assert.GreaterOrEqual(t, got[metricIDLicenseAuthorizationRemainingTime], int64((48*time.Hour/time.Second)-5))
782 assert.LessOrEqual(t, got[metricIDLicenseAuthorizationRemainingTime], int64(48*time.Hour/time.Second))
783 assert.GreaterOrEqual(t, got[metricIDLicenseCertificateRemainingTime], int64((72*time.Hour/time.Second)-5))
784 assert.LessOrEqual(t, got[metricIDLicenseCertificateRemainingTime], int64(72*time.Hour/time.Second))
785 assert.NotContains(t, got, metricIDLicenseRemainingTime)
786 assert.NotContains(t, got, metricIDLicenseGraceRemainingTime)
787 assert.NotContains(t, got, metricIDLicenseUsagePercent)
788 },
789 },
790 "cisco traditional usage and grace": {
791 source: "cisco.yaml",
792 rows: func(now time.Time) []ddsnmp.LicenseRow {
793 securityExpiry := now.Add(72 * time.Hour).Unix()
794 return []ddsnmp.LicenseRow{
795 typedLicenseRow("17", "SECURITYK9",
796 withRawState("in_use"),
797 withExpiry(securityExpiry),
798 withCapacity(100),
799 withAvailable(15),
800 ),
801 typedLicenseRow("23", "APPXK9",
802 withState(2, "usage_count_consumed"),
803 withGraceRemaining(3600),
804 withCapacity(10),
805 withAvailable(0),
806 ),
807 }
808 },
809 assert: func(t *testing.T, got map[string]int64, _ time.Time) {
810 assert.EqualValues(t, 1, got[metricIDLicenseStateHealthy])
811 assert.EqualValues(t, 0, got[metricIDLicenseStateInformational])
812 assert.EqualValues(t, 0, got[metricIDLicenseStateDegraded])
813 assert.EqualValues(t, 1, got[metricIDLicenseStateBroken])
814 assert.EqualValues(t, 0, got[metricIDLicenseStateIgnored])
815 assert.EqualValues(t, 100, got[metricIDLicenseUsagePercent])
816 assert.GreaterOrEqual(t, got[metricIDLicenseRemainingTime], int64((72*time.Hour/time.Second)-5))
817 assert.LessOrEqual(t, got[metricIDLicenseRemainingTime], int64(72*time.Hour/time.Second))
818 assert.GreaterOrEqual(t, got[metricIDLicenseGraceRemainingTime], int64((time.Hour/time.Second)-5))
819 assert.LessOrEqual(t, got[metricIDLicenseGraceRemainingTime], int64(time.Hour/time.Second))
820 },
821 },
822 "mixed rows select worst aggregate signals": {
823 source: "mixed-licensing.yaml",
824 rows: func(now time.Time) []ddsnmp.LicenseRow {
825 perpetualExpiry := now.Add(30 * time.Minute).Unix()
826 earliestRealExpiry := now.Add(6 * time.Hour).Unix()
827 authExpiry := now.Add(30 * time.Hour).Unix()
828 certExpiry := now.Add(20 * time.Hour).Unix()
829 graceExpiry := now.Add(10 * time.Hour).Unix()
830
831 return []ddsnmp.LicenseRow{
832 typedLicenseRow("perpetual", "Perpetual base",
833 withRawState("active"),
834 withExpiry(perpetualExpiry),
835 withUsage(50),
836 withCapacity(100),
837 withPerpetual(),
838 ),
839 typedLicenseRow("soonest_expiring", "Threat prevention",
840 withRawState("about-to-expire"),
841 withExpiry(earliestRealExpiry),
842 withUsage(90),
843 withCapacity(100),
844 ),
845 typedLicenseRow("auth", "Smart auth",
846 func(row *ddsnmp.LicenseRow) {
847 row.Authorization.Has = true
848 row.Authorization.Timestamp = authExpiry
849 row.Authorization.SourceOID = "auth_timer"
850 },
851 ),
852 typedLicenseRow("cert", "Smart cert",
853 func(row *ddsnmp.LicenseRow) {
854 row.Certificate.Has = true
855 row.Certificate.Timestamp = certExpiry
856 row.Certificate.SourceOID = "cert_timer"
857 },
858 ),
859 typedLicenseRow("grace", "Eval grace",
860 withRawState("evaluation"),
861 func(row *ddsnmp.LicenseRow) {
862 row.Grace.Has = true
863 row.Grace.Timestamp = graceExpiry
864 },
865 ),
866 typedLicenseRow("broken", "Broken feature", withState(2, "")),
867 typedLicenseRow("unlimited", "Unlimited pool", withUsagePercent(100), withUnlimited()),
868 }
869 },
870 assert: func(t *testing.T, got map[string]int64, _ time.Time) {
871 assert.EqualValues(t, 4, got[metricIDLicenseStateHealthy])
872 assert.EqualValues(t, 0, got[metricIDLicenseStateInformational])
873 assert.EqualValues(t, 2, got[metricIDLicenseStateDegraded])
874 assert.EqualValues(t, 1, got[metricIDLicenseStateBroken])
875 assert.EqualValues(t, 0, got[metricIDLicenseStateIgnored])
876 assert.EqualValues(t, 90, got[metricIDLicenseUsagePercent])
877 assert.GreaterOrEqual(t, got[metricIDLicenseRemainingTime], int64((6*time.Hour/time.Second)-5))
878 assert.LessOrEqual(t, got[metricIDLicenseRemainingTime], int64(6*time.Hour/time.Second))
879 assert.GreaterOrEqual(t, got[metricIDLicenseAuthorizationRemainingTime], int64((30*time.Hour/time.Second)-5))
880 assert.LessOrEqual(t, got[metricIDLicenseAuthorizationRemainingTime], int64(30*time.Hour/time.Second))
881 assert.GreaterOrEqual(t, got[metricIDLicenseCertificateRemainingTime], int64((20*time.Hour/time.Second)-5))
882 assert.LessOrEqual(t, got[metricIDLicenseCertificateRemainingTime], int64(20*time.Hour/time.Second))
883 assert.GreaterOrEqual(t, got[metricIDLicenseGraceRemainingTime], int64((10*time.Hour/time.Second)-5))
884 assert.LessOrEqual(t, got[metricIDLicenseGraceRemainingTime], int64(10*time.Hour/time.Second))
885 },
886 },
887 }
888
889 for name, tc := range tests {
890 t.Run(name, func(t *testing.T) {
891 mockCtl := gomock.NewController(t)
892 defer mockCtl.Finish()
893
894 mockSNMP := snmpmock.NewMockHandler(mockCtl)
895 setMockClientInitExpect(mockSNMP)
896 setMockClientSysInfoExpect(mockSNMP)
897
898 now := time.Now().UTC()
899 collr := New()
900 collr.Config = prepareV2Config()
901 collr.CreateVnode = false
902 collr.Ping.Enabled = false
903 collr.snmpProfiles = []*ddsnmp.Profile{{}}
904 collr.newSnmpClient = func() gosnmp.Handler { return mockSNMP }
905 collr.newDdSnmpColl = func(ddsnmpcollector.Config) ddCollector {
906 pm := &ddsnmp.ProfileMetrics{
907 Source: tc.source,
908 LicenseRows: tc.rows(now),
909 }
910 return &mockDdSnmpCollector{pms: []*ddsnmp.ProfileMetrics{pm}}
911 }
912
913 require.NoError(t, collr.Init(context.Background()))
914 _ = collr.Check(context.Background())
915
916 start := time.Now().UTC()
917 got := collr.Collect(context.Background())
918 require.NotNil(t, got)
919 tc.assert(t, got, start)
920 })
921 }
922 }
923
924 type mockDdSnmpCollector struct {
925 pms []*ddsnmp.ProfileMetrics
926 meta map[string]ddsnmp.MetaTag
927 err error
928
929 collectCalls int
930 }
931
932 func (m *mockDdSnmpCollector) Collect() ([]*ddsnmp.ProfileMetrics, error) {
933 m.collectCalls++
934 return m.pms, m.err
935 }
936
937 func (m *mockDdSnmpCollector) CollectDeviceMetadata() (map[string]ddsnmp.MetaTag, error) {
938 return m.meta, nil
939 }
940
941 func prepareV3Config() Config {
942 cfg := prepareV2Config()
943 cfg.Options.Version = gosnmp.Version3.String()
944 cfg.User = UserConfig{
945 Name: "name",
946 SecurityLevel: "authPriv",
947 AuthProto: strings.ToLower(gosnmp.MD5.String()),
948 AuthKey: "auth_key",
949 PrivProto: strings.ToLower(gosnmp.AES.String()),
950 PrivKey: "priv_key",
951 ContextName: "test-context",
952 }
953 return cfg
954 }
955
956 func prepareV2Config() Config {
957 cfg := prepareV1Config()
958 cfg.Options.Version = gosnmp.Version2c.String()
959 return cfg
960 }
961
962 func prepareV1Config() Config {
963 return Config{
964 UpdateEvery: 1,
965 Hostname: "192.0.2.1",
966 Community: "public",
967 Options: OptionsConfig{
968 Port: 161,
969 Retries: 1,
970 Timeout: 5,
971 Version: gosnmp.Version1.String(),
972 MaxOIDs: 20,
973 MaxRepetitions: 25,
974 },
975 }
976 }
977
978 func mockInit(t *testing.T) (*snmpmock.MockHandler, func()) {
979 mockCtl := gomock.NewController(t)
980 cleanup := func() { mockCtl.Finish() }
981 mockSNMP := snmpmock.NewMockHandler(mockCtl)
982
983 return mockSNMP, cleanup
984 }
985
986 func setMockClientInitExpect(m *snmpmock.MockHandler) {
987 setMockClientSetterExpect(m)
988 m.EXPECT().Connect().Return(nil).AnyTimes()
989 }
990
991 func setMockClientSetterExpect(m *snmpmock.MockHandler) {
992 m.EXPECT().Target().AnyTimes()
993 m.EXPECT().Port().AnyTimes()
994 m.EXPECT().Version().AnyTimes()
995 m.EXPECT().Community().AnyTimes()
996 m.EXPECT().SetTarget(gomock.Any()).AnyTimes()
997 m.EXPECT().SetPort(gomock.Any()).AnyTimes()
998 m.EXPECT().SetRetries(gomock.Any()).AnyTimes()
999 m.EXPECT().SetMaxRepetitions(gomock.Any()).AnyTimes()
1000 m.EXPECT().SetMaxOids(gomock.Any()).AnyTimes()
1001 m.EXPECT().SetLogger(gomock.Any()).AnyTimes()
1002 m.EXPECT().SetTimeout(gomock.Any()).AnyTimes()
1003 m.EXPECT().SetCommunity(gomock.Any()).AnyTimes()
1004 m.EXPECT().SetVersion(gomock.Any()).AnyTimes()
1005 m.EXPECT().SetSecurityModel(gomock.Any()).AnyTimes()
1006 m.EXPECT().SetMsgFlags(gomock.Any()).AnyTimes()
1007 m.EXPECT().SetSecurityParameters(gomock.Any()).AnyTimes()
1008 m.EXPECT().SetContextName(gomock.Any()).AnyTimes()
1009 m.EXPECT().MaxRepetitions().Return(uint32(25)).AnyTimes()
1010 }
1011
1012 func setMockClientSysInfoExpect(m *snmpmock.MockHandler) {
1013 m.EXPECT().WalkAll(snmputils.RootOidMibSystem).Return([]gosnmp.SnmpPDU{
1014 {Name: snmputils.OidSysDescr, Value: []uint8("mock sysDescr"), Type: gosnmp.OctetString},
1015 {Name: snmputils.OidSysObject, Value: ".1.3.6.1.4.1.14988.1", Type: gosnmp.ObjectIdentifier},
1016 {Name: snmputils.OidSysContact, Value: []uint8("mock sysContact"), Type: gosnmp.OctetString},
1017 {Name: snmputils.OidSysName, Value: []uint8("mock sysName"), Type: gosnmp.OctetString},
1018 {Name: snmputils.OidSysLocation, Value: []uint8("mock sysLocation"), Type: gosnmp.OctetString},
1019 }, nil).MinTimes(1)
1020 }