github.com/netdata/go.d.plugin@v0.58.1/modules/snmp/snmp_test.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package snmp 4 5 import ( 6 "errors" 7 "fmt" 8 "strings" 9 "testing" 10 11 "github.com/golang/mock/gomock" 12 "github.com/gosnmp/gosnmp" 13 snmpmock "github.com/gosnmp/gosnmp/mocks" 14 "github.com/netdata/go.d.plugin/agent/module" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func TestNew(t *testing.T) { 20 assert.IsType(t, (*SNMP)(nil), New()) 21 } 22 23 func TestSNMP_Init(t *testing.T) { 24 tests := map[string]struct { 25 prepareSNMP func() *SNMP 26 wantFail bool 27 }{ 28 "fail with default config": { 29 wantFail: true, 30 prepareSNMP: func() *SNMP { 31 return New() 32 }, 33 }, 34 "fail when 'charts' not set": { 35 wantFail: true, 36 prepareSNMP: func() *SNMP { 37 snmp := New() 38 snmp.Config = prepareV2Config() 39 snmp.ChartsInput = nil 40 return snmp 41 }, 42 }, 43 "fail when using SNMPv3 but 'user.name' not set": { 44 wantFail: true, 45 prepareSNMP: func() *SNMP { 46 snmp := New() 47 snmp.Config = prepareV3Config() 48 snmp.User.Name = "" 49 return snmp 50 }, 51 }, 52 "fail when using SNMPv3 but 'user.level' is invalid": { 53 wantFail: true, 54 prepareSNMP: func() *SNMP { 55 snmp := New() 56 snmp.Config = prepareV3Config() 57 snmp.User.SecurityLevel = "invalid" 58 return snmp 59 }, 60 }, 61 "fail when using SNMPv3 but 'user.auth_proto' is invalid": { 62 wantFail: true, 63 prepareSNMP: func() *SNMP { 64 snmp := New() 65 snmp.Config = prepareV3Config() 66 snmp.User.AuthProto = "invalid" 67 return snmp 68 }, 69 }, 70 "fail when using SNMPv3 but 'user.priv_proto' is invalid": { 71 wantFail: true, 72 prepareSNMP: func() *SNMP { 73 snmp := New() 74 snmp.Config = prepareV3Config() 75 snmp.User.PrivProto = "invalid" 76 return snmp 77 }, 78 }, 79 "success when using SNMPv1 with valid config": { 80 wantFail: false, 81 prepareSNMP: func() *SNMP { 82 snmp := New() 83 snmp.Config = prepareV1Config() 84 return snmp 85 }, 86 }, 87 "success when using SNMPv2 with valid config": { 88 wantFail: false, 89 prepareSNMP: func() *SNMP { 90 snmp := New() 91 snmp.Config = prepareV2Config() 92 return snmp 93 }, 94 }, 95 "success when using SNMPv3 with valid config": { 96 wantFail: false, 97 prepareSNMP: func() *SNMP { 98 snmp := New() 99 snmp.Config = prepareV3Config() 100 return snmp 101 }, 102 }, 103 } 104 105 for name, test := range tests { 106 t.Run(name, func(t *testing.T) { 107 snmp := test.prepareSNMP() 108 109 if test.wantFail { 110 assert.False(t, snmp.Init()) 111 } else { 112 assert.True(t, snmp.Init()) 113 } 114 }) 115 } 116 } 117 118 func TestSNMP_Check(t *testing.T) { 119 tests := map[string]struct { 120 prepareSNMP func(m *snmpmock.MockHandler) *SNMP 121 wantFail bool 122 }{ 123 "success when 'max_request_size' > returned OIDs": { 124 wantFail: false, 125 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 126 snmp := New() 127 snmp.Config = prepareV2Config() 128 129 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 130 Variables: []gosnmp.SnmpPDU{ 131 {Value: 10, Type: gosnmp.Gauge32}, 132 {Value: 20, Type: gosnmp.Gauge32}, 133 }, 134 }, nil).Times(1) 135 136 return snmp 137 }, 138 }, 139 "success when 'max_request_size' < returned OIDs": { 140 wantFail: false, 141 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 142 snmp := New() 143 snmp.Config = prepareV2Config() 144 snmp.Config.Options.MaxOIDs = 1 145 146 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 147 Variables: []gosnmp.SnmpPDU{ 148 {Value: 10, Type: gosnmp.Gauge32}, 149 {Value: 20, Type: gosnmp.Gauge32}, 150 }, 151 }, nil).Times(2) 152 153 return snmp 154 }, 155 }, 156 "success when using 'multiply_range'": { 157 wantFail: false, 158 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 159 snmp := New() 160 snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 1) 161 162 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 163 Variables: []gosnmp.SnmpPDU{ 164 {Value: 10, Type: gosnmp.Gauge32}, 165 {Value: 20, Type: gosnmp.Gauge32}, 166 {Value: 30, Type: gosnmp.Gauge32}, 167 {Value: 40, Type: gosnmp.Gauge32}, 168 }, 169 }, nil).Times(1) 170 171 return snmp 172 }, 173 }, 174 "fail when snmp client Get fails": { 175 wantFail: true, 176 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 177 snmp := New() 178 snmp.Config = prepareV2Config() 179 180 m.EXPECT().Get(gomock.Any()).Return(nil, errors.New("mock Get() error")).Times(1) 181 182 return snmp 183 }, 184 }, 185 "fail when all OIDs type is unsupported": { 186 wantFail: true, 187 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 188 snmp := New() 189 snmp.Config = prepareV2Config() 190 191 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 192 Variables: []gosnmp.SnmpPDU{ 193 {Value: nil, Type: gosnmp.NoSuchInstance}, 194 {Value: nil, Type: gosnmp.NoSuchInstance}, 195 }, 196 }, nil).Times(1) 197 198 return snmp 199 }, 200 }, 201 } 202 203 for name, test := range tests { 204 t.Run(name, func(t *testing.T) { 205 mockSNMP, cleanup := mockInit(t) 206 defer cleanup() 207 208 newSNMPClient = func() gosnmp.Handler { return mockSNMP } 209 defaultMockExpects(mockSNMP) 210 211 snmp := test.prepareSNMP(mockSNMP) 212 require.True(t, snmp.Init()) 213 214 if test.wantFail { 215 assert.False(t, snmp.Check()) 216 } else { 217 assert.True(t, snmp.Check()) 218 } 219 }) 220 } 221 } 222 223 func TestSNMP_Collect(t *testing.T) { 224 tests := map[string]struct { 225 prepareSNMP func(m *snmpmock.MockHandler) *SNMP 226 wantCollected map[string]int64 227 }{ 228 "success when collecting supported type": { 229 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 230 snmp := New() 231 snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 3) 232 233 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 234 Variables: []gosnmp.SnmpPDU{ 235 {Value: 10, Type: gosnmp.Counter32}, 236 {Value: 20, Type: gosnmp.Counter64}, 237 {Value: 30, Type: gosnmp.Gauge32}, 238 {Value: 1, Type: gosnmp.Boolean}, 239 {Value: 40, Type: gosnmp.Gauge32}, 240 {Value: 50, Type: gosnmp.TimeTicks}, 241 {Value: 60, Type: gosnmp.Uinteger32}, 242 {Value: 70, Type: gosnmp.Integer}, 243 }, 244 }, nil).Times(1) 245 246 return snmp 247 }, 248 wantCollected: map[string]int64{ 249 "1.3.6.1.2.1.2.2.1.10.0": 10, 250 "1.3.6.1.2.1.2.2.1.16.0": 20, 251 "1.3.6.1.2.1.2.2.1.10.1": 30, 252 "1.3.6.1.2.1.2.2.1.16.1": 1, 253 "1.3.6.1.2.1.2.2.1.10.2": 40, 254 "1.3.6.1.2.1.2.2.1.16.2": 50, 255 "1.3.6.1.2.1.2.2.1.10.3": 60, 256 "1.3.6.1.2.1.2.2.1.16.3": 70, 257 }, 258 }, 259 "success when collecting supported and unsupported type": { 260 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 261 snmp := New() 262 snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 2) 263 264 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 265 Variables: []gosnmp.SnmpPDU{ 266 {Value: 10, Type: gosnmp.Counter32}, 267 {Value: 20, Type: gosnmp.Counter64}, 268 {Value: 30, Type: gosnmp.Gauge32}, 269 {Value: nil, Type: gosnmp.NoSuchInstance}, 270 {Value: nil, Type: gosnmp.NoSuchInstance}, 271 {Value: nil, Type: gosnmp.NoSuchInstance}, 272 }, 273 }, nil).Times(1) 274 275 return snmp 276 }, 277 wantCollected: map[string]int64{ 278 "1.3.6.1.2.1.2.2.1.10.0": 10, 279 "1.3.6.1.2.1.2.2.1.16.0": 20, 280 "1.3.6.1.2.1.2.2.1.10.1": 30, 281 }, 282 }, 283 "fails when collecting unsupported type": { 284 prepareSNMP: func(m *snmpmock.MockHandler) *SNMP { 285 snmp := New() 286 snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 2) 287 288 m.EXPECT().Get(gomock.Any()).Return(&gosnmp.SnmpPacket{ 289 Variables: []gosnmp.SnmpPDU{ 290 {Value: nil, Type: gosnmp.NoSuchInstance}, 291 {Value: nil, Type: gosnmp.NoSuchInstance}, 292 {Value: nil, Type: gosnmp.NoSuchObject}, 293 {Value: "192.0.2.0", Type: gosnmp.NsapAddress}, 294 {Value: []uint8{118, 101, 116}, Type: gosnmp.OctetString}, 295 {Value: ".1.3.6.1.2.1.4.32.1.5.2.1.4.10.19.0.0.16", Type: gosnmp.ObjectIdentifier}, 296 }, 297 }, nil).Times(1) 298 299 return snmp 300 }, 301 wantCollected: nil, 302 }, 303 } 304 305 for name, test := range tests { 306 t.Run(name, func(t *testing.T) { 307 mockSNMP, cleanup := mockInit(t) 308 defer cleanup() 309 310 newSNMPClient = func() gosnmp.Handler { return mockSNMP } 311 defaultMockExpects(mockSNMP) 312 313 snmp := test.prepareSNMP(mockSNMP) 314 require.True(t, snmp.Init()) 315 316 collected := snmp.Collect() 317 318 assert.Equal(t, test.wantCollected, collected) 319 }) 320 } 321 } 322 323 func TestSNMP_Cleanup(t *testing.T) { 324 tests := map[string]struct { 325 prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *SNMP 326 }{ 327 "cleanup call if snmpClient initialized": { 328 prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { 329 snmp := New() 330 snmp.Config = prepareV2Config() 331 require.True(t, snmp.Init()) 332 333 m.EXPECT().Close().Times(1) 334 335 return snmp 336 }, 337 }, 338 "cleanup call does not panic if snmpClient not initialized": { 339 prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { 340 snmp := New() 341 snmp.Config = prepareV2Config() 342 require.True(t, snmp.Init()) 343 snmp.snmpClient = nil 344 345 return snmp 346 }, 347 }, 348 } 349 350 for name, test := range tests { 351 t.Run(name, func(t *testing.T) { 352 mockSNMP, cleanup := mockInit(t) 353 defer cleanup() 354 355 newSNMPClient = func() gosnmp.Handler { return mockSNMP } 356 defaultMockExpects(mockSNMP) 357 358 snmp := test.prepareSNMP(t, mockSNMP) 359 assert.NotPanics(t, snmp.Cleanup) 360 }) 361 } 362 } 363 364 func TestSNMP_Charts(t *testing.T) { 365 tests := map[string]struct { 366 prepareSNMP func(t *testing.T, m *snmpmock.MockHandler) *SNMP 367 wantNumCharts int 368 }{ 369 "without 'multiply_range': got expected number of charts": { 370 wantNumCharts: 1, 371 prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { 372 snmp := New() 373 snmp.Config = prepareV2Config() 374 require.True(t, snmp.Init()) 375 376 return snmp 377 }, 378 }, 379 "with 'multiply_range': got expected number of charts": { 380 wantNumCharts: 10, 381 prepareSNMP: func(t *testing.T, m *snmpmock.MockHandler) *SNMP { 382 snmp := New() 383 snmp.Config = prepareConfigWithIndexRange(prepareV2Config, 0, 9) 384 require.True(t, snmp.Init()) 385 386 return snmp 387 }, 388 }, 389 } 390 391 for name, test := range tests { 392 t.Run(name, func(t *testing.T) { 393 mockSNMP, cleanup := mockInit(t) 394 defer cleanup() 395 396 newSNMPClient = func() gosnmp.Handler { return mockSNMP } 397 defaultMockExpects(mockSNMP) 398 399 snmp := test.prepareSNMP(t, mockSNMP) 400 assert.Equal(t, test.wantNumCharts, len(*snmp.Charts())) 401 }) 402 } 403 } 404 405 func mockInit(t *testing.T) (*snmpmock.MockHandler, func()) { 406 mockCtl := gomock.NewController(t) 407 cleanup := func() { mockCtl.Finish() } 408 mockSNMP := snmpmock.NewMockHandler(mockCtl) 409 410 return mockSNMP, cleanup 411 } 412 413 func defaultMockExpects(m *snmpmock.MockHandler) { 414 m.EXPECT().Target().AnyTimes() 415 m.EXPECT().Port().AnyTimes() 416 m.EXPECT().Retries().AnyTimes() 417 m.EXPECT().Timeout().AnyTimes() 418 m.EXPECT().MaxOids().AnyTimes() 419 m.EXPECT().Version().AnyTimes() 420 m.EXPECT().Community().AnyTimes() 421 m.EXPECT().SetTarget(gomock.Any()).AnyTimes() 422 m.EXPECT().SetPort(gomock.Any()).AnyTimes() 423 m.EXPECT().SetRetries(gomock.Any()).AnyTimes() 424 m.EXPECT().SetMaxOids(gomock.Any()).AnyTimes() 425 m.EXPECT().SetLogger(gomock.Any()).AnyTimes() 426 m.EXPECT().SetTimeout(gomock.Any()).AnyTimes() 427 m.EXPECT().SetCommunity(gomock.Any()).AnyTimes() 428 m.EXPECT().SetVersion(gomock.Any()).AnyTimes() 429 m.EXPECT().SetSecurityModel(gomock.Any()).AnyTimes() 430 m.EXPECT().SetMsgFlags(gomock.Any()).AnyTimes() 431 m.EXPECT().SetSecurityParameters(gomock.Any()).AnyTimes() 432 m.EXPECT().Connect().Return(nil).AnyTimes() 433 } 434 435 func prepareConfigWithIndexRange(p func() Config, start, end int) Config { 436 if start > end || start < 0 || end < 1 { 437 panic(fmt.Sprintf("invalid index range ('%d'-'%d')", start, end)) 438 } 439 cfg := p() 440 for i := range cfg.ChartsInput { 441 cfg.ChartsInput[i].IndexRange = []int{start, end} 442 } 443 return cfg 444 } 445 446 func prepareV3Config() Config { 447 cfg := prepareV2Config() 448 cfg.Options.Version = gosnmp.Version3.String() 449 cfg.User = User{ 450 Name: "name", 451 SecurityLevel: "authPriv", 452 AuthProto: strings.ToLower(gosnmp.MD5.String()), 453 AuthKey: "auth_key", 454 PrivProto: strings.ToLower(gosnmp.AES.String()), 455 PrivKey: "priv_key", 456 } 457 return cfg 458 } 459 460 func prepareV2Config() Config { 461 cfg := prepareV1Config() 462 cfg.Options.Version = gosnmp.Version2c.String() 463 return cfg 464 } 465 466 func prepareV1Config() Config { 467 return Config{ 468 UpdateEvery: defaultUpdateEvery, 469 Hostname: defaultHostname, 470 Community: defaultCommunity, 471 Options: Options{ 472 Port: defaultPort, 473 Retries: defaultRetries, 474 Timeout: defaultTimeout, 475 Version: gosnmp.Version1.String(), 476 MaxOIDs: defaultMaxOIDs, 477 }, 478 ChartsInput: []ChartConfig{ 479 { 480 ID: "test_chart1", 481 Title: "This is Test Chart1", 482 Units: "kilobits/s", 483 Family: "family", 484 Type: module.Area.String(), 485 Priority: module.Priority, 486 Dimensions: []DimensionConfig{ 487 { 488 OID: "1.3.6.1.2.1.2.2.1.10", 489 Name: "in", 490 Algorithm: module.Incremental.String(), 491 Multiplier: 8, 492 Divisor: 1000, 493 }, 494 { 495 OID: "1.3.6.1.2.1.2.2.1.16", 496 Name: "out", 497 Algorithm: module.Incremental.String(), 498 Multiplier: 8, 499 Divisor: 1000, 500 }, 501 }, 502 }, 503 }, 504 } 505 }