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  }