feat(go.d/ddsnmp): sysobjectid-based metadata override support for SNMP profiles (#20803)
Ilya Mashchenko committed
Aug 12, 2025 at 07:42 UTC
994bdde38770154a2be7d28334718f7eb55a5707
9 files changed
+460
-38
src/go/plugin/go.d/collector/snmp/collect.go
+1
-1
@@ -34,7 +34,7 @@ func (c *Collector) collect() (map[string]int64, error) {
34
}
35
36
if c.ddSnmpColl == nil {
37
- c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger)
37
+ c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger, si.SysObjectID)
38
c.ddSnmpColl.DoTableMetrics = c.EnableProfilesTableMetrics
39
}
40
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metadata.go
+12
@@ -73,3 +73,15 @@ func NewMetadataResourceConfig() MetadataResourceConfig {
73
func IsMetadataResourceWithScalarOids(resource string) bool {
74
return resource == MetadataDeviceResource
75
}
76
+
77
+type SysobjectIDMetadataEntryConfig struct {
78
+ SysobjectID string `yaml:"sysobjectid"`
79
+ Metadata ListMap[MetadataField] `yaml:"metadata"`
80
+}
81
+
82
+func (e SysobjectIDMetadataEntryConfig) Clone() SysobjectIDMetadataEntryConfig {
83
+ return SysobjectIDMetadataEntryConfig{
84
+ SysobjectID: e.SysobjectID,
85
+ Metadata: CloneMap(e.Metadata),
86
+ }
87
+}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/profile_definition.go
+19
-17
@@ -20,14 +20,15 @@ type DeviceMeta struct {
20
// 2/ Datadog backend: the profiles are in json format, they are used to store profiles created via UI.
21
// The serialisation of json profiles are defined by the json annotation.
22
type ProfileDefinition struct {
23
- Name string `yaml:"name,omitempty" json:"name,omitempty"`
24
- Description string `yaml:"description,omitempty" json:"description,omitempty"`
25
- SysObjectIDs StringArray `yaml:"sysobjectid,omitempty" json:"sysobjectid,omitempty"`
26
- Extends []string `yaml:"extends,omitempty" json:"extends,omitempty"`
27
- Metadata MetadataConfig `yaml:"metadata,omitempty" json:"metadata,omitempty"`
28
- MetricTags []MetricTagConfig `yaml:"metric_tags,omitempty" json:"metric_tags,omitempty"`
29
- StaticTags []string `yaml:"static_tags,omitempty" json:"static_tags,omitempty"`
30
- Metrics []MetricsConfig `yaml:"metrics,omitempty" json:"metrics,omitempty"`
23
+ Name string `yaml:"name,omitempty" json:"name,omitempty"`
24
+ Description string `yaml:"description,omitempty" json:"description,omitempty"`
25
+ SysObjectIDs StringArray `yaml:"sysobjectid,omitempty" json:"sysobjectid,omitempty"`
26
+ Extends []string `yaml:"extends,omitempty" json:"extends,omitempty"`
27
+ Metadata MetadataConfig `yaml:"metadata,omitempty" json:"metadata,omitempty"`
28
+ SysobjectIDMetadata []SysobjectIDMetadataEntryConfig `yaml:"sysobjectid_metadata,omitempty"`
29
+ MetricTags []MetricTagConfig `yaml:"metric_tags,omitempty" json:"metric_tags,omitempty"`
30
+ StaticTags []string `yaml:"static_tags,omitempty" json:"static_tags,omitempty"`
31
+ Metrics []MetricsConfig `yaml:"metrics,omitempty" json:"metrics,omitempty"`
32
33
VirtualMetrics []VirtualMetricConfig `yaml:"virtual_metrics,omitempty" json:"virtual_metrics,omitempty"`
34
@@ -65,15 +66,16 @@ func (p *ProfileDefinition) Clone() *ProfileDefinition {
66
return nil
67
}
68
return &ProfileDefinition{
68
- Name: p.Name,
69
- Description: p.Description,
70
- SysObjectIDs: slices.Clone(p.SysObjectIDs),
71
- Extends: slices.Clone(p.Extends),
72
- Metadata: CloneMap(p.Metadata),
73
- MetricTags: CloneSlice(p.MetricTags),
74
- StaticTags: slices.Clone(p.StaticTags),
75
- Metrics: CloneSlice(p.Metrics),
76
- VirtualMetrics: CloneSlice(p.VirtualMetrics),
69
+ Name: p.Name,
70
+ Description: p.Description,
71
+ SysObjectIDs: slices.Clone(p.SysObjectIDs),
72
+ Extends: slices.Clone(p.Extends),
73
+ Metadata: CloneMap(p.Metadata),
74
+ SysobjectIDMetadata: CloneSlice(p.SysobjectIDMetadata),
75
+ MetricTags: CloneSlice(p.MetricTags),
76
+ StaticTags: slices.Clone(p.StaticTags),
77
+ Metrics: CloneSlice(p.Metrics),
78
+ VirtualMetrics: CloneSlice(p.VirtualMetrics),
79
Device: DeviceMeta{
80
Vendor: p.Device.Vendor,
81
},
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/validation.go
+56
@@ -160,6 +160,62 @@ func ValidateEnrichMetadata(metadata MetadataConfig) []string {
160
return errors
161
}
162
163
+func ValidateEnrichSysobjectIDMetadata(entries []SysobjectIDMetadataEntryConfig) []string {
164
+ var errors []string
165
+
166
+ // Track seen sysobjectids to detect duplicates
167
+ seenOIDs := make(map[string]int) // OID -> first occurrence index
168
+
169
+ for i, entry := range entries {
170
+ // Validate sysobjectid is not empty
171
+ if entry.SysobjectID == "" {
172
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d]: missing sysobjectid", i))
173
+ continue
174
+ }
175
+
176
+ // Check for duplicate sysobjectids
177
+ if firstIdx, exists := seenOIDs[entry.SysobjectID]; exists {
178
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d]: duplicate sysobjectid %s (first occurrence at index %d)",
179
+ i, entry.SysobjectID, firstIdx))
180
+ } else {
181
+ seenOIDs[entry.SysobjectID] = i
182
+ }
183
+
184
+ // Validate metadata fields
185
+ for fieldName, field := range entry.Metadata {
186
+ // Validate the field must have either value or symbol(s)
187
+ if field.Value == "" && field.Symbol.OID == "" && len(field.Symbols) == 0 {
188
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s: must have either value or symbol(s)", i, fieldName))
189
+ }
190
+
191
+ // Can't have both value and symbols
192
+ if field.Value != "" && (field.Symbol.OID != "" || len(field.Symbols) > 0) {
193
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s: cannot have both value and symbol(s)", i, fieldName))
194
+ }
195
+
196
+ // Validate symbols if present
197
+ for j := range field.Symbols {
198
+ symbolErrors := validateEnrichSymbol(&field.Symbols[j], MetadataSymbol)
199
+ for _, err := range symbolErrors {
200
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s.symbols[%d]: %s", i, fieldName, j, err))
201
+ }
202
+ }
203
+
204
+ // Validate single symbol if present
205
+ if field.Symbol.OID != "" {
206
+ symbolErrors := validateEnrichSymbol(&field.Symbol, MetadataSymbol)
207
+ for _, err := range symbolErrors {
208
+ errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s.symbol: %s", i, fieldName, err))
209
+ }
210
+ }
211
+
212
+ entry.Metadata[fieldName] = field
213
+ }
214
+ }
215
+
216
+ return errors
217
+}
218
+
219
func validateEnrichSymbol(symbol *SymbolConfig, symbolContext SymbolContext) []string {
220
var errors []string
221
if symbol.Name == "" {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+4
-4
@@ -18,7 +18,7 @@ import (
18
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19
)
20
21
-func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
21
+func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger, sysobjectid string) *Collector {
22
coll := &Collector{
23
log: log.With(slog.String("ddsnmp", "collector")),
24
snmpClient: snmpClient,
@@ -34,7 +34,7 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
34
}
35
36
coll.globalTagsCollector = newGlobalTagsCollector(snmpClient, coll.missingOIDs, coll.log)
37
- coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log)
37
+ coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log, sysobjectid)
38
coll.scalarCollector = newScalarCollector(snmpClient, coll.missingOIDs, coll.log)
39
coll.tableCollector = newTableCollector(snmpClient, coll.missingOIDs, coll.tableCache, coll.log)
40
coll.vmetricsCollector = newVirtualMetricsCollector(coll.log)
@@ -70,11 +70,11 @@ func (c *Collector) CollectDeviceMetadata() (map[string]map[string]string, error
70
meta := make(map[string]map[string]string)
71
72
for _, prof := range c.profiles {
73
- dm, err := c.deviceMetadataCollector.Collect(prof.profile)
73
+ deviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
74
if err != nil {
75
return nil, err
76
}
77
- meta[prof.profile.SourceFile] = dm
77
+ meta[prof.profile.SourceFile] = deviceMeta
78
}
79
80
return meta, nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+1
-1
@@ -4422,7 +4422,7 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4422
mockHandler := snmpmock.NewMockHandler(ctrl)
4423
tc.setupMock(mockHandler)
4424
4425
- collector := New(mockHandler, tc.profiles, logger.New())
4425
+ collector := New(mockHandler, tc.profiles, logger.New(), "")
4426
collector.DoTableMetrics = true
4427
4428
// Configure cache based on test requirements
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta.go
+39
-14
@@ -10,6 +10,7 @@ import (
10
"github.com/gosnmp/gosnmp"
11
12
"github.com/netdata/netdata/go/plugins/logger"
13
+ "github.com/netdata/netdata/go/plugins/pkg/matcher"
14
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
16
)
@@ -145,13 +146,15 @@ type deviceMetadataCollector struct {
146
snmpClient gosnmp.Handler
147
missingOIDs map[string]bool
148
log *logger.Logger
149
+ sysobjectid string
150
}
151
150
-func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *deviceMetadataCollector {
152
+func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger, sysobjectid string) *deviceMetadataCollector {
153
return &deviceMetadataCollector{
154
snmpClient: snmpClient,
155
missingOIDs: missingOIDs,
156
log: log,
157
+ sysobjectid: sysobjectid,
158
}
159
}
160
@@ -160,25 +163,47 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
163
return nil, nil
164
}
165
166
+ resName := ddprofiledefinition.MetadataDeviceResource
167
+ cfg, ok := prof.Definition.Metadata[resName]
168
+ if !ok {
169
+ return nil, nil
170
+ }
171
+
172
meta := make(map[string]string)
173
165
- for resName, cfg := range prof.Definition.Metadata {
166
- if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
174
+ if err := dc.processMetadataFields(cfg.Fields, meta); err != nil {
175
+ return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
176
+ }
177
+
178
+ if dc.sysobjectid == "" || len(prof.Definition.SysobjectIDMetadata) == 0 {
179
+ return meta, nil
180
+ }
181
+
182
+ for i, entry := range prof.Definition.SysobjectIDMetadata {
183
+ m, err := matcher.NewRegExpMatcher(entry.SysobjectID)
184
+ if err != nil {
185
+ dc.log.Warningf("sysobjectid_metadata[%d]: failed to compile regular expression from sysobjectid '%s': %v",
186
+ i, entry.SysobjectID, err)
187
continue
188
}
169
-
170
- if err := dc.processResource(cfg, meta); err != nil {
171
- return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
189
+ if m.MatchString(dc.sysobjectid) {
190
+ if err := dc.processMetadataFields(entry.Metadata, meta); err != nil {
191
+ dc.log.Warningf("sysobjectid_metadata[%d]: failed to process metadata fields for sysobjectid '%s': %v",
192
+ i, entry.SysobjectID, err)
193
+ continue
194
+ }
195
+ dc.log.Debugf("sysobjectid_metadata[%d]: matched sysobjectid '%s' with device OID '%s', applying metadata overrides",
196
+ i, entry.SysobjectID, dc.sysobjectid)
197
}
198
}
199
200
return meta, nil
201
}
202
178
-// processResource processes a single metadata resource
179
-func (dc *deviceMetadataCollector) processResource(cfg ddprofiledefinition.MetadataResourceConfig, metadata map[string]string) error {
203
+// processMetadataFields processes a single metadata resource
204
+func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) error {
205
staticValues := make(map[string]string)
181
- oids := dc.collectStaticAndIdentifyOIDs(cfg, staticValues)
206
+ oids := dc.collectStaticAndIdentifyOIDs(fields, staticValues)
207
208
for k, v := range staticValues {
209
metadata[k] = v
@@ -193,14 +218,14 @@ func (dc *deviceMetadataCollector) processResource(cfg ddprofiledefinition.Metad
218
return fmt.Errorf("failed to fetch metadata values: %w", err)
219
}
220
196
- return dc.processDynamicFields(cfg, pdus, metadata)
221
+ return dc.processDynamicFields(fields, pdus, metadata)
222
}
223
224
// collectStaticAndIdentifyOIDs collects static values and returns OIDs to fetch
200
-func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(cfg ddprofiledefinition.MetadataResourceConfig, staticValues map[string]string) []string {
225
+func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, staticValues map[string]string) []string {
226
var oids []string
227
203
- for name, field := range cfg.Fields {
228
+ for name, field := range fields {
229
switch {
230
case field.Value != "":
231
staticValues[name] = field.Value
@@ -246,10 +271,10 @@ func (dc *deviceMetadataCollector) fetchMetadataValues(oids []string) (map[strin
271
return pdus, nil
272
}
273
249
-func (dc *deviceMetadataCollector) processDynamicFields(cfg ddprofiledefinition.MetadataResourceConfig, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
274
+func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprofiledefinition.MetadataField, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
275
var errs []error
276
252
- for name, field := range cfg.Fields {
277
+ for name, field := range fields {
278
switch {
279
case field.Symbol.OID != "":
280
// Single symbol
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta_test.go
+325
-1
@@ -409,6 +409,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
409
tests := map[string]struct {
410
profile *ddsnmp.Profile
411
setupMock func(m *snmpmock.MockHandler)
412
+ sysobjectid string
413
expectedResult map[string]string
414
expectedError bool
415
errorContains string
@@ -889,6 +890,329 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
890
},
891
expectedError: false,
892
},
893
+ "sysobjectid metadata override - single match": {
894
+ profile: &ddsnmp.Profile{
895
+ Definition: &ddprofiledefinition.ProfileDefinition{
896
+ Metadata: ddprofiledefinition.MetadataConfig{
897
+ "device": ddprofiledefinition.MetadataResourceConfig{
898
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
899
+ "vendor": {
900
+ Value: "Cisco",
901
+ },
902
+ "type": {
903
+ Value: "Firewall",
904
+ },
905
+ },
906
+ },
907
+ },
908
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
909
+ {
910
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
911
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
912
+ "model": {
913
+ Value: "ASA5510",
914
+ },
915
+ "series": {
916
+ Value: "ASA5500",
917
+ },
918
+ },
919
+ },
920
+ {
921
+ SysobjectID: "1.3.6.1.4.1.9.1.670",
922
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
923
+ "model": {
924
+ Value: "ASA5520",
925
+ },
926
+ },
927
+ },
928
+ },
929
+ },
930
+ },
931
+ setupMock: func(m *snmpmock.MockHandler) {},
932
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
933
+ expectedResult: map[string]string{
934
+ "vendor": "Cisco",
935
+ "type": "Firewall",
936
+ "model": "ASA5510",
937
+ "series": "ASA5500",
938
+ },
939
+ expectedError: false,
940
+ },
941
+ "sysobjectid metadata override - multiple matches cascade": {
942
+ profile: &ddsnmp.Profile{
943
+ Definition: &ddprofiledefinition.ProfileDefinition{
944
+ Metadata: ddprofiledefinition.MetadataConfig{
945
+ "device": ddprofiledefinition.MetadataResourceConfig{
946
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
947
+ "vendor": {
948
+ Value: "Cisco",
949
+ },
950
+ },
951
+ },
952
+ },
953
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
954
+ {
955
+ SysobjectID: "1.3.6.1.4.1.9.*", // All Cisco devices
956
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
957
+ "platform": {
958
+ Value: "Enterprise",
959
+ },
960
+ "support": {
961
+ Value: "Premium",
962
+ },
963
+ },
964
+ },
965
+ {
966
+ SysobjectID: "1.3.6.1.4.1.9.1.*", // Cisco ASA family
967
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
968
+ "type": {
969
+ Value: "Firewall",
970
+ },
971
+ "series": {
972
+ Value: "ASA",
973
+ },
974
+ },
975
+ },
976
+ {
977
+ SysobjectID: "1.3.6.1.4.1.9.1.669", // Specific model
978
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
979
+ "model": {
980
+ Value: "ASA5510",
981
+ },
982
+ "series": {
983
+ Value: "ASA5500", // Override the previous series value
984
+ },
985
+ },
986
+ },
987
+ },
988
+ },
989
+ },
990
+ setupMock: func(m *snmpmock.MockHandler) {},
991
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
992
+ expectedResult: map[string]string{
993
+ "vendor": "Cisco",
994
+ "platform": "Enterprise",
995
+ "support": "Premium",
996
+ "type": "Firewall",
997
+ "series": "ASA5500", // Last match wins
998
+ "model": "ASA5510",
999
+ },
1000
+ expectedError: false,
1001
+ },
1002
+ "sysobjectid metadata with dynamic SNMP values": {
1003
+ profile: &ddsnmp.Profile{
1004
+ Definition: &ddprofiledefinition.ProfileDefinition{
1005
+ Metadata: ddprofiledefinition.MetadataConfig{
1006
+ "device": ddprofiledefinition.MetadataResourceConfig{
1007
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1008
+ "vendor": {
1009
+ Value: "Cisco",
1010
+ },
1011
+ },
1012
+ },
1013
+ },
1014
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1015
+ {
1016
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
1017
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1018
+ "model": {
1019
+ Value: "ASA5510",
1020
+ },
1021
+ "firmware": {
1022
+ Symbol: ddprofiledefinition.SymbolConfig{
1023
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1024
+ Name: "ciscoImageVersion",
1025
+ },
1026
+ },
1027
+ },
1028
+ },
1029
+ },
1030
+ },
1031
+ },
1032
+ setupMock: func(m *snmpmock.MockHandler) {
1033
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1034
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1035
+ &gosnmp.SnmpPacket{
1036
+ Variables: []gosnmp.SnmpPDU{
1037
+ {
1038
+ Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1039
+ Type: gosnmp.OctetString,
1040
+ Value: []byte("9.2(4)"),
1041
+ },
1042
+ },
1043
+ }, nil,
1044
+ )
1045
+ },
1046
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
1047
+ expectedResult: map[string]string{
1048
+ "vendor": "Cisco",
1049
+ "model": "ASA5510",
1050
+ "firmware": "9.2(4)",
1051
+ },
1052
+ expectedError: false,
1053
+ },
1054
+ "sysobjectid metadata no match": {
1055
+ profile: &ddsnmp.Profile{
1056
+ Definition: &ddprofiledefinition.ProfileDefinition{
1057
+ Metadata: ddprofiledefinition.MetadataConfig{
1058
+ "device": ddprofiledefinition.MetadataResourceConfig{
1059
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1060
+ "vendor": {
1061
+ Value: "Cisco",
1062
+ },
1063
+ "type": {
1064
+ Value: "Firewall",
1065
+ },
1066
+ },
1067
+ },
1068
+ },
1069
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1070
+ {
1071
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
1072
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1073
+ "model": {
1074
+ Value: "ASA5510",
1075
+ },
1076
+ },
1077
+ },
1078
+ },
1079
+ },
1080
+ },
1081
+ setupMock: func(m *snmpmock.MockHandler) {},
1082
+ sysobjectid: "1.3.6.1.4.1.9.1.700",
1083
+ expectedResult: map[string]string{
1084
+ "vendor": "Cisco",
1085
+ "type": "Firewall",
1086
+ // No model since sysobjectid doesn't match
1087
+ },
1088
+ expectedError: false,
1089
+ },
1090
+ "sysobjectid metadata with invalid regex pattern": {
1091
+ profile: &ddsnmp.Profile{
1092
+ Definition: &ddprofiledefinition.ProfileDefinition{
1093
+ Metadata: ddprofiledefinition.MetadataConfig{
1094
+ "device": ddprofiledefinition.MetadataResourceConfig{
1095
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1096
+ "vendor": {
1097
+ Value: "Cisco",
1098
+ },
1099
+ },
1100
+ },
1101
+ },
1102
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1103
+ {
1104
+ SysobjectID: "1.3.6.1.4.1.9.1.[invalid", // Invalid regex
1105
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1106
+ "model": {
1107
+ Value: "ASA5510",
1108
+ },
1109
+ },
1110
+ },
1111
+ {
1112
+ SysobjectID: "1.3.6.1.4.1.9.1.669", // Valid entry
1113
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1114
+ "series": {
1115
+ Value: "ASA5500",
1116
+ },
1117
+ },
1118
+ },
1119
+ },
1120
+ },
1121
+ },
1122
+ setupMock: func(m *snmpmock.MockHandler) {},
1123
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
1124
+ expectedResult: map[string]string{
1125
+ "vendor": "Cisco",
1126
+ "series": "ASA5500", // Valid entry still processes
1127
+ },
1128
+ expectedError: false,
1129
+ },
1130
+ "sysobjectid metadata overrides base metadata field": {
1131
+ profile: &ddsnmp.Profile{
1132
+ Definition: &ddprofiledefinition.ProfileDefinition{
1133
+ Metadata: ddprofiledefinition.MetadataConfig{
1134
+ "device": ddprofiledefinition.MetadataResourceConfig{
1135
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1136
+ "vendor": {
1137
+ Value: "Generic",
1138
+ },
1139
+ "type": {
1140
+ Value: "Unknown",
1141
+ },
1142
+ "model": {
1143
+ Value: "Generic Model",
1144
+ },
1145
+ },
1146
+ },
1147
+ },
1148
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1149
+ {
1150
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
1151
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1152
+ "vendor": {
1153
+ Value: "Cisco Systems", // Override generic vendor
1154
+ },
1155
+ "model": {
1156
+ Value: "ASA5510", // Override generic model
1157
+ },
1158
+ },
1159
+ },
1160
+ },
1161
+ },
1162
+ },
1163
+ setupMock: func(m *snmpmock.MockHandler) {},
1164
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
1165
+ expectedResult: map[string]string{
1166
+ "vendor": "Cisco Systems", // Overridden
1167
+ "type": "Unknown", // Not overridden
1168
+ "model": "ASA5510", // Overridden
1169
+ },
1170
+ expectedError: false,
1171
+ },
1172
+ "sysobjectid metadata with SNMP fetch error continues": {
1173
+ profile: &ddsnmp.Profile{
1174
+ Definition: &ddprofiledefinition.ProfileDefinition{
1175
+ Metadata: ddprofiledefinition.MetadataConfig{
1176
+ "device": ddprofiledefinition.MetadataResourceConfig{
1177
+ Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1178
+ "vendor": {
1179
+ Value: "Cisco",
1180
+ },
1181
+ },
1182
+ },
1183
+ },
1184
+ SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1185
+ {
1186
+ SysobjectID: "1.3.6.1.4.1.9.1.669",
1187
+ Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1188
+ "model": {
1189
+ Value: "ASA5510",
1190
+ },
1191
+ "firmware": {
1192
+ Symbol: ddprofiledefinition.SymbolConfig{
1193
+ OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1194
+ Name: "ciscoImageVersion",
1195
+ },
1196
+ },
1197
+ },
1198
+ },
1199
+ },
1200
+ },
1201
+ },
1202
+ setupMock: func(m *snmpmock.MockHandler) {
1203
+ m.EXPECT().MaxOids().Return(10).AnyTimes()
1204
+ m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1205
+ nil,
1206
+ errors.New("SNMP timeout"),
1207
+ )
1208
+ },
1209
+ sysobjectid: "1.3.6.1.4.1.9.1.669",
1210
+ expectedResult: map[string]string{
1211
+ "vendor": "Cisco",
1212
+ "model": "ASA5510", // Static value still applied despite SNMP error
1213
+ },
1214
+ expectedError: false, // Should continue with partial data
1215
+ },
1216
}
1217
1218
for name, tc := range tests {
@@ -900,7 +1224,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
1224
tc.setupMock(mockHandler)
1225
1226
missingOIDs := make(map[string]bool)
903
- collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New())
1227
+ collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New(), tc.sysobjectid)
1228
1229
result, err := collector.Collect(tc.profile)
1230
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+3
@@ -203,6 +203,9 @@ func (p *Profile) validate() error {
203
for _, err := range ddprofiledefinition.ValidateEnrichMetadata(p.Definition.Metadata) {
204
errs = append(errs, errors.New(err))
205
}
206
+ for _, err := range ddprofiledefinition.ValidateEnrichSysobjectIDMetadata(p.Definition.SysobjectIDMetadata) {
207
+ errs = append(errs, errors.New(err))
208
+ }
209
for _, err := range ddprofiledefinition.ValidateEnrichMetrics(p.Definition.Metrics) {
210
errs = append(errs, errors.New(err))
211
}